Kotlin in Action - 15장 구조화된 동시성 June 15, 2025
📖 15.1 코루틴 스코프가 코루틴 간의 구조를 확립한다
구조화된 동시성을 통해 각 코루틴은 코루틴 스코프에 속하게 된다.
다른 코루틴 빌더의 본문에서 launch나 async를 사용해 새로운 코루틴을 만들면 이 새로운 코루틴은 자동으로 해당 코루틴의 자식이 된다.
fun main() {
runBlocking {
launch {
delay(1.seconds)
launch {
delay(250.milliseconds)
log("Grandchild done")
}
log("Child 1 done!")
}
launch {
delay(500.milliseconds)
log("Child 2 done!")
}
log("Parent done!")
}
}
0 [main @coroutine#1] Parent done!
524 [main @coroutine#3] Child 2 done!
1020 [main @coroutine#2] Child 1 done!
1275 [main @coroutine#4] Grandchild done
모든 자식 코루틴이 완료될 때까지 프로그램이 종료되지 않는다.
🔖 15.1.1 코루틴 스코프 생성: coroutineScope 함수
코루틴 빌더를 사용해 새로운 코루틴을 만들면 이 코루틴은 자체적인 CoroutineScope를 생성한다.
CoroutineScope 함수의 전형적인 사용 사례는 동시적 작업 분해(여러 코루틴을 활용해 계산 수행)
suspend fun generateValue(): Int {
delay(500.milliseconds)
return Random.nextInt(0, 10)
}
suspend fun computeSum() {
log("Computing a sum...")
val sum = coroutineScope {
val a = async { generateValue() }
val b = async { generateValue() }
a.await() + b.await()
}
log("Sum is $sum")
}
🔖 15.1.2 코루틴 스코프를 컴포넌트와 연관시키기: CoroutineScope
coroutineScope 함수가 작업을 분해하는 데 사용되는 반면 구체적 생명주기를 정의하고, 동시 처리나 코루틴의 시작과 종료를 관리하는 클래스를 만들고 싶을 대도 잇다.
class ComponentWithScope(dispatcher: CoroutineDispatcher = Dispatchers.Default) {
private val scope = CoroutineScope(dispatcher + SupervisorJob())
fun start() {
log("Starting")
scope.launch {
while (true) {
delay(500.milliseconds)
log("Component working!")
}
}
scope.launch {
log("Doing a one-off task...")
delay(500.milliseconds)
log("Task done!")
}
}
fun stop() {
log("Stopping!")
scope.cancel()
}
}
이 Component 클래스의 인스턴스를 생성하고 start를 호출하면 컴포넌트 내부에서 코루틴이 시작된다.
🔖 15.1.3 GlobalScope의 위험성
fun main() = runBlocking {
GlobalScope.launch {
delay(1000)
launch {
delay(250)
log("Grandchild done")
}
log("Child 1 done!")
}
GlobalScope.launch {
delay(500)
log("Child 2 done!")
}
log("Parent done!")
}
0 [main @coroutine#1] Parent done!
GlobalScope는 전역 수준에 존재하는 스코프
GlobalScope를 사용하면 구조화된 동시성이 제공하는 모든 이점을 포기
자동취소 불가, 생명주기 개념 없음
🔖 15.1.4 코루틴 콘텍스트와 구조화된 동시성
fun main() {
runBlocking(Dispatchers.Default) {
log(coroutineContext)
launch {
log(coroutineContext)
launch(Dispatchers.IO + CoroutineName("mine")) {
log(coroutineContext)
}
}
}
}
0 [DefaultDispatcher-worker-1 @coroutine#1] [CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@3a617b0f, Dispatchers.Default]
13 [DefaultDispatcher-worker-2 @coroutine#2] [CoroutineId(2), "coroutine#2":StandaloneCoroutine{Active}@5e2dba97, Dispatchers.Default]
14 [DefaultDispatcher-worker-3 @mine#3] [CoroutineName(mine), CoroutineId(3), "mine#3":StandaloneCoroutine{Active}@12764f6a, Dispatchers.IO]
자식 코루틴은 부모의 콘텍스트 상속
새로운 코루틴은 부모-자식 관계 설정하는 역할을 하는 새 Job 객체 생성
디스패처를 지정하지 않고 새로운 코루틴을 시작하면 부모 코루틴의 디스패처에서 실행
fun main() = runBlocking(CoroutineName("A")) {
log("A's job: ${coroutineContext.job}")
launch(CoroutineName("B")) {
log("B's job: ${coroutineContext.job}")
log("B's parent: ${coroutineContext.job.parent}")
}
log("A's children: ${coroutineContext.job.children.toList()}")
}
코루틴 간의 부모-자식 관계를 확인할 수 있음
구조화된 동시성에 의해 설정된 이 부모-자식 관계는 취소와도 연관이 있다.
📖 15.2 취소
취소는 코드가 완료되기 전에 실행을 중단하는 것을 의미
취소는 불필요한 작업을 막아준다.
취소는 메모리나 리소스 누수 방지에 도움을 준다.
취소는 오류 처리에서도 중요한 역할을 한다.
🔖 15.2.1 취소 촉발
fun main() = runBlocking {
val launchedJob = launch {
log("I'm launched!")
delay(1000.milliseconds)
log("I'm done!")
}
val asyncDeferred = async {
log("I'm async")
delay(1000.milliseconds)
log("I'm done!")
}
delay(200.milliseconds)
launchedJob.cancel()
asyncDeferred.cancel()
}
0 [main @coroutine#2] I'm launched!
11 [main @coroutine#3] I'm async
cancel을 호출해 해당 코루틴의 취소를 촉발할 수 있다.
🔖 15.2.2 시간제한이 초과된 후 자동으로 취소 호출
suspend fun calculateSomething(): Int {
delay(3.seconds)
return 2 + 2
}
fun main() = runBlocking {
val quickResult = withTimeoutOrNull(500.milliseconds) {
calculateSomething()
}
println(quickResult) // null
val slowResult = withTimeoutOrNull(5.seconds) {
calculateSomething()
}
println(slowResult) // 4
}
withTimeout, withTimeoutOrNull 함수는 계산에 쓸 최대 시간을 제한하면서 값을 계산할 수 있게 해준다.
🔖 15.2.3 취소는 모든 자식 코루틴에게 전파된다
fun main() = runBlocking {
val job = launch {
launch {
launch {
launch {
log("I'm started")
delay(500.milliseconds)
log("I'm done!")
}
}
}
}
delay(200.milliseconds)
job.cancel()
}
코루틴을 취소하면 해당 코루틴의 모든 자식 코루틴도 자동으로 취소된다.
여러 계층에 걸쳐 코루틴이 중첩돼 있는 경우에도 가장 바깥쪽 코루틴을 취소하면 고손자 코루틴까지 모두 적절히 취소된다.
🔖 15.2.4 취소된 코루틴은 특별한 지점에서 CancellationException을 던진다
취소 메커니즘은 CancellationException이라는 특수한 예외를 특별한 지점에서 던지는 방식으로 작동
취소된 코루틴은 일시 중단 지점에서 CancellationException을 던진다.
코루틴 계층에서 취소를 전파하기 때문에 이 예외를 직접 처리하지 않아야 한다.
🔖 15.2.5 취소는 협력적이다
suspend fun doCpuHeavyWork(): Int {
log("I'm doing work!")
var counter = 0
val startTime = System.currentTimeMillis()
while (System.currentTimeMillis() < startTime + 500) {
counter++
}
return counter
}
fun main() = runBlocking {
val myJob = launch {
repeat(5) {
doCpuHeavyWork()
}
}
delay(600.milliseconds)
myJob.cancel()
}
프로그램이 종료되기 전에 doCpuHeavyWork가 5번 완료된다.
doCpuHeavyWork 함수는 일시 중단 지점을 포함하지 않는다.
코틀린 코루틴의 취소가 협력적인 이유는 결국 스스로 취소 가능하게 로직을 제공해야 하기 때문
🔖 15.2.6 코루틴이 취소됐는지 확인
val myJob = launch {
repeat(5) {
doCpuHeavyWork()
if (!isActive) return@launch
}
}
코루틴이 취소됐는지 확인할 때는 isActive 속성을 확인
ensureActive 함수는 비활성화일 때, CancellationException을 던진다.
🔖 15.2.7 다른 코루틴에게 기회를 주기: yield 함수
코루틴 라이브러리는 yield 함수 제공
취소 가능 지점 제공
점유된 디스패처에서 다른 코루틴이 작업할 수 있게 해줌
fun doCpuHeavyWork(): Int {
var counter = 0
val startTime = System.currentTimeMillis()
while (System.currentTimeMillis() < startTime + 500) {
counter++
}
return counter
}
fun main() {
runBlocking {
launch {
repeat(3) {
doCpuHeavyWork()
}
}
launch {
repeat(3) {
doCpuHeavyWork()
}
}
}
}
첫 번째 코루틴이 완료될 때까지 두 번째 코루틴은 실행되지 않음.
일시 중단 지점이 없기 때문
suspend fun doCpuHeavyWork(): Int {
var counter = 0
val startTime = System.currentTimeMillis()
while (System.currentTimeMillis() < startTime + 500) {
counter++
yield()
}
return counter
}
fun main() {
runBlocking {
launch {
repeat(3) {
doCpuHeavyWork()
}
}
launch {
repeat(3) {
doCpuHeavyWork()
}
}
}
}
yield 함수를 사용하면 코루틴이 교차 실행됨
🔖 15.2.8 리소스를 얻을 때 취소를 염두에 두기
취소 후, close 함수가 호출되지 않고 리소스가 누수될 수 있음.
finally 블록을 사용해 명시적으로 닫자.
리소스가 AutoClosable 인터페이스를 구현하는 경우 .use 함수를 사용해 같은 동작을 더 간결하게 처리할 수 있다.
🔖 15.2.9 프레임워크가 여러분 대신 취소를 할 수 있다
많은 실제 어플리케이션에서는 프레임워크가 코루틴 스코프를 제공하고 자동취소한다.
사용자는 적절한 코루틴 스코프를 선택