13장. 동시성

Bimmer·2023년 9월 14일
0
post-thumbnail

코루틴

코틀린 프로그램에서도 자바 동시성 기본 요소를 쉽게 사용해 스레드 안전성을 달성할 수 있다.
코루틴이라는 강력한 메커니즘 덕분에 우리에게 익숙한 명령형 스타일로 코드를 작성하면 컴파일러가 코드를 효율적인 비동기 계산으로 자동 변환해준다. 이런 메커니즘은 실행을 잠시 중단했다가 나중에 중단한 지점부터 실행을 다시 재개할 수 있는 일시 중단 가능한 함수라는 개념을 중심으로 이뤄진다.

대부분의 코루틴 기능이 별도 라이브러리로 제공되기 때문에 명시적으로 프로젝트 설정에 이를 추가해야 한다.

일시 중단 함수

전체 코루틴 라이브러리를 뒷받침하는 기본 요소는 일시 중단 함수다. 이 함수는 일반적인 함수를 더 일반화해 함수 본문의 원하는 지점에서 함수에 필요한 모든 런타임 문맥을 저장하고 함수 실행을 중단한 다음, 나중에 필요할 때 다시 실행을 계쏙 진행할 수 있게 한 것이다.

코틀린에서는 이런 함수에 suspend라는 변경자를 붙인다.

suspend fun foo() {
	println("started")
    delay(100)
    println("finished")
}

delay() 함수는 코루틴 라이브러리에 정의된 일시 중단 함수다. 이 함수는 Thread.sleep()과 비슷하다. 하지만 delay()는 현재 스레드를 블럭시키지 않고 자신을 호출한 함수를 일시 중단시키며 스레드를 다른 작업을 수행할 수 있게 풀어준다.

일시 중단 함수는 일시 중단 함수와 일반 함수를 원하는 대로 호출할 수 있다. 일시 중단 함수를 호출하면 해당 호출 지점이 일시 중단 짖머이 된다. 일시 중단 지점은 임시로 실행을 중단했다가 나중에 재개할 수 있는 지점을 말한다. 반면 일반 함수 호출은 일반 함수처럼 작동해서 함수 실행이 다 끝난 뒤 호출한 함수로 제어가 돌아온다.

반면에 코틀린은 일반 함수가 일시 중단 함수를 호출하는 것을 금지한다.

fun foo() {
	println("started")
    delay(100)	// error: delay is a suspend function
    println("finished")
}

 

코루틴 빌더

일반적으로 동시성 코드의 동작을 제어하고 싶기 때문에 공통적인 생명 주기와 문맥이 정해진 몇몇 작업이 정의된 구체적인 영역 안에서만 동시성 함수를 호출한다. 이런 구체적 영역을 제공하기 위해 코루틴을 실행할 때 사용하는 여러 가지 함수를 코루틴 빌더(coroutine builder)라고 부른다. 코루틴 빌더는 CoroutineScope 인스턴스의 확장 함수로 쓰인다.

launch

launch() 함수는 코루틴을 시작하고, 코루틴을 실행 중인 작업의 상태를 추적하고 변경할 수 있는 Job 객체를 돌려준다.

import kotlinx.coroutines.*
import java.lang.System.*

fun main() {
	val time = currentTimeMillis()
    
    GlobalScope.launch {
        delay(100)    
	    println("Task 1 finished in ${currentTimeMillis() - time} ms")
    }
    
    GlobalScope.launch {
        delay(100)    
	    println("Task 2 finished in ${currentTimeMillis() - time} ms")
    }
    
    Thread.sleep(200)
}

=>
Task 2 finished in 217 ms
Task 1 finished in 220 ms

두 작업이 프로그램을 시작한 시점을 기준으로 거의 동시에 끝났다는 점에서 알 수 있는 것처럼 두 작업이 시렞로 병렬적으로 실행됐다는 점에 주목하자. 다만 실행 순서가 항상 일정하게 보장되지는 않으므로 상황에 따라 둘 중 어느 한쪽이 먼저 표시될 수 있다.

main() 함수 자체는 Thread.sleep()을 통해 메인 스레드 실행을 잠시 중단한다. 이를 통해 코루틴 스레드가 완료될 수 있도록 충분한 시간을 제공한다. 코루틴을 처리하는 스레드는 데몬 모드로 실행되기 때문에 main() 스레드가 이 스레드보다 빨리 끝나버리면 자동으로 실행이 종료된다.

일시 중단 함수의 내부에서 sleep()과 같은 스레드를 블럭시키는 함수를 실행할 수도 있지만, 그런 식의 코드는 코루틴을 사용하는 목적에 위배된다. 그래서 도잇성 작업의 내부에서는 delay()를 사용해야 한다.

코루틴은 스레드보다 훨씬 가볍다. 특히 코루틴은 유지해야 하는 상태가 더 간단하며 일시 중단되고 재대될 때 완전한 문맥 전환을 사용하지 않아도 되므로 엄청난 수의 코루틴을 충분히 동시에 실행할 수 있다.

launch() 빌더는 동시성 작업이 결과를 만들어내지 않는 경우 적합하다.

 

async

동시성 작업의 결과가 필요한 경우 async() 빌더를 사용해야 한다. 이 함수는 Deferred의 인스턴스를 돌려주고, 이 인스턴스는 Job의 하위 타입으로 await() 메서드를 통해 계산 결과에 접근할 수 있게 해준다. await() 메서드를 호출하면 계산이 완료되거나 계산 작업이 취소될 때까지 현재 코루틴을 일시 중단시킨다. 작업이 취소되는 경우 await()는 예외를 발생시키면서 실패한다.

import kotlinx.coroutines.*
import java.lang.System.*

suspend fun main() {
	val message = GlobalScope.async {
        delay(100)
        "abc"
    }
    
    val count = GlobalScope.async {
        delay(100)
        1 + 2
    }
    
    delay(100)
   
    val result = message.await().repeat(count.await())
    println(result)
}

=>
abcabcabc

lauch()와 async() 빌더의 경우 스레드 호출을 블럭시키지는 않지만, 백그라운드 스레드를 공유하는 풀을 통해 작업을 실행한다.

 

runBlocking

runBlocking() 빌더는 디폴트로 현재 스레드에서 실행되는 코루틴을 만들고 코루틴이 완료될 때까지 현재 스레드의 실행을 블럭시킨다. 코루틴이 성공적으로 끝나면 일시 중단 람다의 결과가 runBlocking()의 호출 결괏값이 된다. 코루틴이 취소되면 runBlocking()은 예외를 던진다. 반면에 블럭된 스레드가 인터럽트되면 runBlocking()에 의해 시작된 코루틴도 취소된다.

import kotlinx.coroutines.*
import java.lang.System.*

fun main() {
    
    GlobalScope.launch {
        delay(100)    
	    println("Background task: ${Thread.currentThread().name}")
    }
    
	runBlocking {
	    println("Primary task: ${Thread.currentThread().name}")
        delay(200)
    }
    
}

=>
Primary task: main @coroutine#2
Background task: DefaultDispatcher-worker-1 @coroutine#1

runBlocking() 내부 코루틴은 메인 스레드에서 실행된 반면, launch()로 시작한 코루틴은 공유 풀에서 백그라운드 스레드를 할당 받았다.

이런 동작 때문에 runBlocking()을 다른 코루틴 안에서 사용하면 안된다. runBlocking()은 블러킹 호출과 넌블러킹 호출 사이의 다리 역할을 하기 위해 고안된 코루틴 빌더이므로, 테스트나 메인 함수에서 최상위 빌더로 사용하는 등의 경우에만 사용해야 한다.

profile
hello

0개의 댓글