스레드 양보란?
코루틴이 스레드를 양보한다는 것은 어떤 뜻일까?
코루틴에서 대표적으로 아래의 함수들이 스레드를 양보하는 중단함수이다.
- delay
- yield
- join
- await
아래 코드를 통해 살펴보자.
suspend fun main() = runBlocking {
val start = System.currentTimeMillis()
repeat(3) {
launch {
delay(1000)
println("elapsed time: ${System.currentTimeMillis() - start}")
}
}
}
>> elapsed time: 1012
>> elapsed time: 1021
>> elapsed time: 1021
위 코드는 코루틴 3개를 각각 1초씩 delay 를 주었다.
하지만, 마지막에 실행이 완료된 코루틴의 경과시간은 3초가 아니라 1초대이다.
즉, 코루틴 3개가 동시에 실행되었음을 알 수 있다.
이를 도식화하면 아래와 같다.
반면, Thread.sleep 을 사용하면 마지막 경과시간이 3초정도로 나온다.
suspend fun main() = runBlocking {
val start = System.currentTimeMillis()
repeat(3) {
launch {
Thread.sleep(1000)
println("elapsed time: ${System.currentTimeMillis() - start}")
}
}
}
>> elapsed time: 1009
>> elapsed time: 2022
>> elapsed time: 3024
중단함수가 스레드를 양보한다는 것은 중단함수를 일시 중단하고, 스레드가 다른 코루틴을 실행할 수 있도록 하는 것이다.
스레드를 양보하는 행위는 스레드의 state 와는 다르다.
Thead.sleep() 과 같은 메서드는 스레드의 상태롤 WAITING, TIMED_WAITING 과 같은 상태로 변경하는 것으로, 스레드 자체가 블로킹된다. 따라서, CPU 의 스케줄링 자체에서 제거되버린다.
참고로, 코루틴이 중단되는 것을 나타내는 별도의 상태는 존재하지 않는다.
코루틴은 하나의 스레드에서만 실행되지 않는다
코루틴은 하나의 스레드에서만 실행되는 것이 아니다.
하나의 코루틴은 여러 스레드에 의해서 실행될 수 있다.
하나의 코루틴을 여러번 반복해서 실행해보자.
suspend fun main() = coroutineScope {
val dispatcher = newFixedThreadPoolContext(nThreads = 2, name = "custom")
launch(dispatcher) {
repeat(50) {
println(Thread.currentThread().name)
delay(50)
println(Thread.currentThread().name)
}
}
println()
}
>> custom-1 @coroutine#1
>> custom-2 @coroutine#1
>> custom-2 @coroutine#1
>> custom-2 @coroutine#1
>> custom-2 @coroutine#1
>> custom-1 @coroutine#1
... 반복
위 코드를 실행해보면, 하나의 코루틴을 반복실행할 때 custom-1, custom-2 스레드에 번갈아가면서 할당되는 것을 볼 수 있다.
이번에는 delay 가 아닌 Thread.sleep 을 통해 실행을 멈춰보자.
suspend fun main() = coroutineScope {
val dispatcher = newFixedThreadPoolContext(nThreads = 2, name = "custom")
launch(dispatcher) {
repeat(50) {
println(Thread.currentThread().name)
Thread.sleep(50)
println(Thread.currentThread().name)
}
}
println()
}
>> custom-1 @coroutine#1
>> custom-1 @coroutine#1
>> custom-1 @coroutine#1
>> custom-1 @coroutine#1
...생략...
이번에는 하나의 스레드에서만 실행결과가 나온다. 왜 이런일이 발생하는 걸까?
실행 스레드는 코루틴이 재개될 때 바뀔 수 있다.
코루틴의 실행 스레드는 코루틴이 재개될 때 바뀔 수 있다.
재개는 코루틴이 중단되었다가 다시 시작되는 것을 말한다.
Thread.sleep() 은 코루틴이 중단되었다가 재개되는 것이 아니라,
스레드가 TIMED_WAITING 상태로 전이되었다가 RUNNABLE 상태로 돌아오는 것이다.
이때는 스레드를 양보하여, 코루틴이 중단되는 것이 아니라
스레드 자체가 블록킹되어버리는 것이다.
이것을 코드로 확인해보자.
참고로, launch 내부가 delay 외에 실행할 작업이 없는 경우, TIMED_WAITING 상태가 되므로, 무한 루프를 도는 코루틴을 추가하여 넣었다.
suspend fun main() = coroutineScope {
var thread: Thread? = null
val dispatcher = newSingleThreadContext("custom")
val job = launch(dispatcher) {
println(Thread.currentThread().name)
thread = Thread.currentThread()
launch {
while (true) {
}
}
delay(5000)
println(Thread.currentThread().name)
}
delay(1000)
println("${thread?.name} ${thread?.state}")
println(job.isActive)
println(job.isCancelled)
println(job.isCompleted)
}
>> custom @coroutine#1
>> custom @coroutine#2 RUNNABLE
>> true
>> false
>> false
위 delay 를 통해 코루틴을 중단시킨 경우, 바깥쪽 launch 는 스레드를 안쪽 launch 에게 양보하였다.
즉, 코루틴이 중단되는 것은 다른 코루틴에게 스레드를 양보하여 사용이 가능한 상태를 말한다.
따라서, launch 에서 사용되는 스레드는 RUNNABLE 상태이고, 코루틴은 active 상태이다.
반면, Thread.sleep 을 사용한 코드를 살펴보자.
suspend fun main() = coroutineScope {
var thread: Thread? = null
val dispatcher = newSingleThreadContext("custom")
val job = launch(dispatcher) {
println(Thread.currentThread().name)
thread = Thread.currentThread()
launch {
while (true) {
}
}
Thread.sleep(5000)
println(Thread.currentThread().name)
}
delay(1000)
println("${thread?.name} ${thread?.state}")
println(job.isActive)
println(job.isCancelled)
println(job.isCompleted)
}
>> custom @coroutine#1
>> custom @coroutine#1 TIMED_WAITING
>> true
>> false
>> false
Thread.sleep 을 사용한 코드는 바깥쪽 launch 의 스레드 자체가 TIMED_WAITING 으로 전이되었다.
이 경우, launch 를 실행하는 스레드 자체가 블록킹되었기 때문에, 그 동안 코루틴 자체가 실행이 될 수 없다.
스레드가 블록킹되어 코루틴 자체를 실행할 수 없는 것과
코루틴이 중단되어 다른 코루틴에게 스레드를 양보하는 것은 다르다.
'자바와 코틀린' 카테고리의 다른 글
JWT (0) | 2024.11.17 |
---|---|
코루틴 - 중단함수 (0) | 2024.09.17 |
코루틴 - 구조화된 동시성 (2) | 2024.09.17 |
코루틴 - 코루틴 빌더와 Job (0) | 2024.09.17 |
코루틴 - CoroutineDispatcher (0) | 2024.09.17 |