Developing Myself Everyday

지금까지 살펴본 코루틴은 GlobalScope에서만 실행되었다. 경우에 따라서는 어떤 연산을 수행하는 도중에만 실행되기를 원할 수 있다. 어떤 코루틴을 다른 코루틴의 문맥에서 실행하면 후자가 전자의 부모가 되고 이런 관계로 인해 이런 실행 시간 제한이 가능하게 된다. 자식의 실행이 모두 끝나야 부모가 끝날 수 있도록 자식의 생명 주기가 연관된다. 

 

이런 기능을 구조적 동시성이라고 부르며, 블럭이나 서브루틴을 사용하는 경우 구조적 동시성을 비교할 수 있다.

 

다음은 이런 구조적 동시성을 나타낸 예제이다. 

 

import kotlinx.coroutines.*

fun main() {
    println("메인 스레드 시작")

    runBlocking {
       println("부모 task 시작")

       launch {
           println("A task 시작")
           delay(200)
           println("A task 종료")
       }

       launch {
           println("B task 시작")
           delay(200)
           println("B task 종료")
       }

       delay(100)
       println("부모 task 종료")
   }
    println("메인 스레드 종료")
}

 

위를 실행한 결과는 아래와 같다.

 

메인 스레드 시작
부모 task 시작
A task 시작
B task 시작
부모 task 종료
A task 종료
B task 종료
메인 스레드 종료

 

지연을 100만 줬기 때문에 runBlocking()의 일시 중단 람다로 이뤄진 부모 코틀린의 주 본문이 더 빨리 끝난다. 하지만 부모 코루틴 자체는 이 시점에 실행이 끝나지 않고 일시 중단 상태로 두 자식이 끝날 때까지 기다린다. runBlocking() 이 메인 스레드를 막고 있었기 때문에 부모 스레드가 끝나야 메인 스레드의 블럭이 풀리고 마지막 메시지가 출력된다.

 

 

coroutineScope() 호출로 커스텀 영역을 도입할 수도 있다. runBlocking()과 마찬가지로 coroutineScope() 호출은 람다의 결과를 반환하고 자식들이 완료되기 전까지 실행이 완료되지 않는다. 

coroutineScope()와 runBlocking()의 가장 큰 차이는 coroutineScope()는 현재 스레드를 블럭시키지 않는다는 점이다.

 

import kotlinx.coroutines.*

fun main() {
    println("메인 스레드 시작")

    runBlocking {
        println("커스텀 scope 시작")

        coroutineScope {
            launch {
                println("A task 시작")
                delay(100)
                println("A task 종료")
            }

            launch {
                println("B task 시작")
                delay(100)
                println("B task 종료")
            }
        }

        println("커스텀 scope 종료")
    }
    println("메인 스레드 종료")
}

 

위의 코드를 실행시키면 아래와 같은 결과를 얻을 수 있다.

 

메인 스레드 시작
커스텀 scope 시작
A task 시작
B task 시작
A task 종료
B task 종료
커스텀 scope 종료
메인 스레드 종료

 

 

코루틴 컨택스트


코루틴 컨택스트는 key와 element를 갖는 map을 의미한다. 코투틴을 감싸는 변수 영역의 coroutineContext 프로퍼티를 통해 이 문맥에 접근할 수 있다. 

 

GlobalScope.launch { 
    println("작업 진행: ${coroutineContext[Job.Key]?.isActive}")
}

 

그리고 코루틴을 실행하는 중간에 withContext()에 새 문맥과 일시 중단 람다를 넘겨서 문맥을 전환시킬 수 있다.

 

 

 

 

코루틴 Dispatcher

코루틴 문맥에는 해당 코루틴의 실행에 사용하는 스레드나 스레드를 결정하는 코루틴 Dispather가 포함되어 있다.

 

 

 launch(), async() 같은 모든 코투틴 빌더는 새로운 코루틴 및 기타 문맥에 대한 Dispather를 명시적으로 지정하는데 사용할 수 있는 선택적 옵션인 CoroutineContext라는 파라미터를 허용한다.

 

import kotlinx.coroutines.*

fun main() {
    runBlocking {
       launch(Dispatchers.Default) {
           println(Thread.currentThread().name)
       }
    }
}

 

위의 코드를 실행시킨 결과는 아래와 같다.

 

DefaultDispatcher-worker-2

 

위의 코드는 전역 스레드 풀 디스패처를 이용해 코루틴을 실행하고 있다.

 

 

 


코루틴 라이브러리에는 기본적으로 몇 가지 디스패처 구현을 제공한다. 이는 다음과 같다.

 

  • Dispatchers.Default: 공유 스레드 풀로, 풀 크기는 디폴트로 사용가능한 CPU 코어 수이거나 2이다.
  • Dispatchers.IO: 스레드 풀 기반이며 디폴트 구현과 비슷하지만, 파일을 읽고 쓰는 거서럼 잠재적으로 블러킹될 수 있는 I/O를 많이 사용하는 작업에 최적화돼 있다. 
  • Dispaters.Main: 사용자 입력이 처리되는 UI 스레드에서만 배타적으로 작동하는 디스패처이다.

 

다음은 executor를 기반으로 스레드 풀을 만들고 디스패처를 사용한 예제이다.

 

코드

import kotlinx.coroutines.*

fun main() {
    newFixedThreadPoolContext(5, "스레드").use { dispatcher ->
        runBlocking {
            for (i in 1..3) {
                launch(dispatcher) {
                    println(Thread.currentThread().name)
                    delay(1000)
                }
            }
        }
    }
}

 

결과

스레드-1
스레드-2
스레드-3

 

이 디스패처는 Closeable 인스턴스도 구현한다. 스레드를 유지하기 위해 할당했던 시스템 자원을 해제하려면 close() 함수를 직접 호출하거나, 위의 코드처럼 use() 함수 블록 안에서 디스패처를 사용해야 한다.

 

 


그럼 이전처럼 디스패처를 명시적으로 지정하지않으면 어떻게 되는가? 이를 알아보도록 하자

 

 

코드

fun main() {
    runBlocking {
        println("현재 스레드 이름 - " + Thread.currentThread().name)

        launch {
            println("자동 상속 스레드 이름 - " + Thread.currentThread().name)
        }

        launch(Dispatchers.Default) {
            println("지정한 스레드 이름 - " + Thread.currentThread().name)
        }
    }
}

 

결과

현재 스레드 이름 - main
지정한 스레드 이름 - DefaultDispatcher-worker-1
자동 상속 스레드 이름 - main

 

위의 코드로 알 수 있는 것은 디스패처를 명시적으로 지정하지 않으면 부모 코루틴으로부터 물려받는다는 것이다. 만약 부모 코투틴이 없다면 암시적으로 Dispather.Default로 디스패처를 가정한다.

 

 


withContext()를 사용해 디스패처를 오버라이드할 수도 있다.

 

 

코드

fun main() {
    newSingleThreadContext("스레드").use { name ->
        runBlocking {
            println(Thread.currentThread().name)

            withContext(name) {
                println(Thread.currentThread().name)
            }
        }
        println(Thread.currentThread().name)
    }
}

 

결과

main
스레드
main

 

위의 방법은 중단 가능 루틴의 일부를 한 스레드에서만 실행하고 싶을 때 유용하다.

profile

Developing Myself Everyday

@배준형

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!