title: How to correctly join all jobs launched in a CoroutineScope
body: <p>I'm refactoring some Kotlin code that currently launches coroutines on the <code>GlobalScope</code> to a structured concurrency-based approach. I need to join all of the jobs launched in my code before the JVM exits. My class can be broken down to the following interface:</p>
  <pre class="lang-kotlin prettyprint-override"><code>interface AsyncTasker {
      fun spawnJob(arg: Long)
      suspend fun joinAll()
  }
  </code></pre>
  <p>Usage:</p>
  <pre class="lang-kotlin prettyprint-override"><code>fun main(args: Array&lt;String&gt;) {
      val asyncTasker = createAsyncTasker()
  
      asyncTasker.spawnJob(100)
      asyncTasker.spawnJob(200)
      asyncTasker.spawnJob(300)
      asyncTasker.spawnJob(500)
  
      // join all jobs as they'd be killed when the JVM exits
      runBlocking {
          asyncTasker.joinAll()
      }
  }
  </code></pre>
  <p>My <code>GlobalScope</code>-based implementation looks as follows:</p>
  <pre class="lang-kotlin prettyprint-override"><code>class GlobalScopeAsyncTasker : AsyncTasker {
      private val pendingJobs = mutableSetOf&lt;Job&gt;()
  
      override fun spawnJob(arg: Long) {
          var job: Job? = null
          job = GlobalScope.launch(Dispatchers.IO) {
              someSuspendFun(arg)
              pendingJobs.remove(job)
          }
          pendingJobs.add(job)
      }
  
      override suspend fun joinAll() {
          // iterate over a copy of the set as the
          // jobs remove themselves from the set when we join them
          pendingJobs.toSet().joinAll()
      }
  }
  </code></pre>
  <p>Clearly, this is not ideal, as keeping track of every pending job isn't very elegant and a remnant of old thread-based coding paradigms.</p>
  <p>As a better approach, I'm creating my own <code>CoroutineScope</code> which is used to launch all children, providing a <code>SupervisorJob</code>.</p>
  <pre class="lang-kotlin prettyprint-override"><code>class StructuredConcurrencyAsyncTasker : AsyncTasker {
  
      private val parentJob = SupervisorJob()
      private val scope = CoroutineScope(Dispatchers.IO + parentJob)
  
      override fun spawnJob(arg: Long) {
          scope.launch {
              someSuspendFun(arg)
          }
      }
  
      override suspend fun joinAll() {
          parentJob.complete() // &lt;-- why is this needed??
          parentJob.join()
      }
  }
  </code></pre>
  <p>When initially developing this solution, I omitted the call to <code>parentJob.complete()</code>, which caused <code>join()</code> to <strong>suspend indefinitely</strong>. This feels very unintuitive, so I'm looking for confirmation/input whether this is the correct way to solve this kind of problem. Why do I have to manually <code>complete()</code> the parent job? Is there an even simpler way to solve this?</p>
  <p><a href="https://pl.kotl.in/eXQL1re4p" rel="nofollow noreferrer">Kotlin playground with the code</a></p>