CoroutineScope를 활용한 적 있다.fun main(): Unit = runBlocking {
  val job1 = CoroutineScope(Dispatchers.Default).launch {
    delay(1_000L)
    printWithThread("Job 1")
  }
}
launch와 async는 사실 CoroutineScope의 확장함수이다.runBlocking이 코루틴과 루틴 세계를 이어주며 CoroutineScope를 제공해준 것이다.
    CoroutineScope를 만들면 runBlocking은 필요하지 않다.fun main() {
  val job = CoroutineScope(Dispatchers.Default).launch {
    delay(1_000L)
    printWithThread("Job 1")
  }
  
  Thread.sleep(1_500L) // job이 새 스레드에서 루트 코루틴으로 실행되기에 기다려야 Job1이 출력될 것이다.
}
CoroutineScope는 코루틴이 탄생할 수 있는 영역이다.
    CoroutineScope는 CoroutineContext 데이터 보관한다.CoroutineContext는 코루틴과 관련된 데이터를 보관
    CoroutineExceptionHandler, 코루틴 이름, 코루틴 그 자체, CoroutineDispatcher 등Dispatcher는 코루틴이 어떤 스레드에 배정될지 관리한다.public interface CoroutineScope {
  public val coroutineContext: CoroutineContext
}
CoroutineScope에서 생성된다.context에서 필요한 정보를 덮어 써 새로운 자신의 context를 만든다.
    class AsyncLogic {
  private val scope = CoroutineScope(Dispatchers.Default)
  
  fun doSomething() {
    scope.launch {
      // 어떤 작업을 수행
    }
  }
  
  fun destroy() {
    scope.cancel()
  } 
}
val asyncLogic = AsyncLogic()
asyncLogic.doSomething()
asyncLogic.destory() // 필요 없어지면 모두 정리
Map + Set을 합쳐놓은 상태
    + 기호를 이용해 Element들을 합칠 수도 있다.fun main() {
  // 각각이 Element이다.
  CoroutineName("나만의 코루틴") + Dispatchers.Default
  
  // context 자체에 Element를 추가할 수도 있다.
  coroutineContext + CoroutineName("나만의 코루틴")
  
  // Element를 제거할 수도 있다.
  coroutineContext.minusKey(CoroutineName.key)
}
Dispatchers.Default
    Dispatchers.IO
    Dispatchers.Main
    ExecutorService를 디스패처로 변환
    asCoroutineDispatcher 확장 함수 사용fun main() {
  val threadPool = Executors.newSingleThreadExecutor()
  CoroutineScope(threadPool.asCoroutineDispatcher()).launch {
    printwithThread("새로운 코루틴")
  }
}