코루틴
todo
- 코틀린 코루틴의 일시 중단 가능한 함수를 바탕으로 동시성 흐름 제어, 코루틴의 생명 주기에 따른 상태 변화
- 코루틴 취소, 코루틴 예외 처리 동시상 작업이 쓰레드를 할당 받는 것
- kotlin에서 java의 동시성 연산을 사용 하더라도 대부분의 동시성 연산이 blocking이다.
Thread.sleep,Thread.join,Object.wait가 그 예시다.- contextSwitch가 수반된다. 이는 성능에 부정적 영향을 미친다. 더 큰 문제는 리소스가 막대하게 든다는 것이다.
- asyncrhonous가 더 효율적인 대안이 될 수 있다. 그러나 명령형 제어 흐름을 사용할 수 없어서 코드 복잡도가 급격하게 높아진다는 것이 문제가 될 수 있다.
일시 중단 함수
- 가장 기본 요소는
일시 중단 함수다. - 함수 본문이 원하는 지점에서 함수에 필요한 모든 런타임 문맥을 저장하고 함수 실행을 중단한 다음 나중에 필요할 때 꺼내서 다시 진행하는 식으로 작동한다.
suspend fun speak() { print("WAIT 100") delay(100) //Thread.wait과 달리 블럭시키지 않고 일시 중단시킨다. print("FINISH") } - 일시 중단 함수는 일시 중단 함수와 일반 함수를 원하는 대로 호출할 수 있다. 중단 함수를 호출하면 호출 지점이 일시 중단 지점이 된다.
- 일시 중단 지점은 임시로 실행을 중단했다가 나중에 재개할 수 있는 지점을 의미한다.
- 일시 중단 함수를 호출할 수 있는 건 일시 중단 함수다. 상위 함수가 일반 함수인 경우, 일시 중단 함수를 내부에서 호출할 수 없다. 그래서 main에 suspend를 붙이기도 한다.
- 현실적으로 동시성 코드의 동작을 제어하기 위해서 공통적인 생명 주기와 문맥이 정해진 몇몇 작업이 정해진 구체적인 영역 안에서만 동시성 함수를 호출한다.
- 이런 영역 제공을 위해서 coroutine builder를 사용해서 만든다.
- 빌더는
CouroutineScope인스턴스의 확장 함수로 쓰인다.
코루틴 빌더
- launch()
- async()
- runBlocking()
- lauch로 시작하고 코루틴을 실행 중인 작업의 상태를 추적하고 변경할 수 있는 Job을 리턴한다.
- Job은
Coroutine -> Unit의 일시 중단 람다를 받는다.
class CoroutineBuilderExample {
@Test
fun coroutine(): Unit {
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 128ms
* Task 1 finished in 128ms
*/
}
}
- 두 작업이 병렬로 실행한다.
- 순서 보장은 되지 않는다.
Thread.sleep으로 메인 쓰레드를 블로킹하며 중단한다.- 코루틴은 daemon mode로 실행 되기에 main보다 먼저 끝나면 실행이 안 될 수도 있다.
- 코루틴은 유지해야 하는 상태가 간단하며, 일시 중단 된고 재개될 때 완전한 문맥 전환을 사용하지 않으므로 더 가볍고 더 많은 수를 동시에 사용할 수 있다.
launch()는 동시성 작업이 결과를 만들어내지 않는 경우 적합하다.- 결과가 필요하면
async()를 쓰면 된다.class CoroutineBuilderExample { @Test suspend fun coroutineAsync() { val message = GlobalScope.async { delay(100) "abc" } val count = GlobalScope.async { delay(100) 1 + 2 } delay(200) val result = message.await().repeat(count.await()) println(result) } } - Deferred의 인스턴스를 돌려주고, 이 인스턴스는 Job의 하위 타입으로 await() 메소드로 계산 결과에 접근할 수 있다.
- java의 future와 유사하다.
- launch, async는 쓰레드 호출을 블럭시키지는 않지만 백그라운드 쓰레드를 공유하는 풀을 통해서 작업을 실행한다.
- 그래서 launch, async는 따로 메인쓰레드를 sleep으로 기다려야 했다.
- 반대로
runBlocking()는 디폴트로 현재 쓰레드에서 실행되는 코루틴을 만들고 코루틴이 완료될 때까지 현재 쓰레드의 실행을 블럭시킨다. - 코루틴이 끝나면 일시 중단 람다의 결과가
runBlocking()의 결과가 된다. - 만약 block 된 쓰레드가 interrupt되면 해당 코루틴도 취소 된다.
class CoroutineTest {
@Test
fun coroutineRunBlocking() {
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은 다른 코루틴 안에서 실행하면 안된다.
코루틴의 영역, 구조적 동시성
- 전역 영역이란 코루틴 생명 주기가 전체 애플리케이션 생명 주기를 따르는 영역이다.
- 만일 어떤 연산 도중에만 실행하길 바란다면 부모-자식 관계를 맺고 실행 시간 제한이 가능하다.
- 어떤 코루틴을 다른 코루틴의 문맥에서 실행하면 전자가 자식, 후자가 부모가 된다. 자식이 종료되어야 부모가 종료될 수 있다.
- 이런 기능을
structured concurrency, 구조적 동시성이라고 부른다. 비교 대상은 지역 변수 영역 안에서 블럭이나 서브 루틴을 사용하는 경우가 있겠다.
@Test
fun scope () {
runBlocking {
println("Parent task started")
launch {
println("Task A started")
delay(200)
println("Task A finished")
}
launch {
println("Task B started")
delay(200)
println("Task B finished")
}
delay(100)
println("Parent task finished")
}
println("Shutting down")
//Parent task started
//Task A started
//Task B started
//Parent task finished
//Task A finished
//Task B finished
//Shutting down
}
- runBlocking은 블로킹 동작이고 따라서 자식으로 사용하는 것보다는 부모로 사용하는 것에 적합하다.
- 살펴보면 100밀리초만 기다리게 시켰기에 부모가 더 빨리 끝나지만 일시 중단 상태로 두 자식을 기다린다.
runBlocking이 메인 쓰레드를 블럭하기 때문이다.coroutineScope호출로 블럭을 감싸면 커스텀 영역을 도입할 수도 있다.
- runBlocking 같이 람다 결과를 반환한다.
- 자식들이 완료될 때까지 기다린다.
- 그러나
runBlocking과 달리 현재 쓰레드를 블럭시키지 않는다. ```kotlin @Test fun scopeCoroutineScope () { runBlocking { println(“Parent task started”)
coroutineScope {
launch {
println("Task A started")
delay(200)
println("Task A finished")
}
launch {
println("Task B started")
delay(200)
println("Task B finished")
}
}
println("Parent task finished")
}
println("Shutting down")
//Parent task started
//Task A started
//Task B started
//Task A finished
//Task B finished
//Parent task finished
//Shutting down
} ``` - coroutineScope의 호출이 일시 중단 되므로 순서대로 실행된다.
코루틴 문맥
- 코루틴 마다 CoroutineContext 인터페이스로 표현되는 문맥에 연관되어 있다.
- 코루틴을 감싸는 영역의 coroutineContext 프로퍼티로 이 문맥에 접근할 수 있다.
- 문맥에는 코루틴에서 사용할 수 있는 여러 데이터가 들어있다.
- 코루틴이 실행 중인 취소 가능한 작업을 표현하는 Job
- 코루틴의 쓰레드의 연관을 제어하는 dispatcher
@Test fun coroutineContext() { GlobalScope.launch { println("Task is active: ${coroutineContext[Job.Key]!!.isActive}") } Thread.sleep(100) //Task is active: true }
- 코루틴 실행 중간에
withContext()로 일시 중단 람다를 넘겨서 문맥을 전환시킬 수도 있다.