Coroutine은 Dispatchers.Default와 같이 멀티 스레드를 관리하는 Dispatcher에 의해 병렬적으로 실행될 수 있다. 이는 병렬 실행 시 일어날 수 있는 일반적인 문제들을 모두 만들어낸다. 가장 주요한 문제는 변경 가능한 공유 상태의 동기화이다. Coroutine에서 이 문제에 대한 일부 해결 방식은 멀티 스레드 세계에서의 해결방식과 유사하지만, 다른 해결 방식들은 Coroutine에만 있다.
Coroutine을 여러개 실행했을 때의 문제점
같은 동작을 수천번 하는 수백개의 Coroutine을 실행한다고 하자. 이후의 추가 비교를 위해 완료 시간을 측정한다 :
suspend fun massiveRun(action: suspend () -> Unit) {
val n = 100 // number of coroutines to launch
val k = 1000 // times an action is repeated by each coroutine
val time = measureTimeMillis {
coroutineScope { // scope for coroutines
repeat(n) {
launch {
repeat(k) { action() }
}
}
}
}
println("Completed ${n * k} actions in $time ms")
}
복수의 스레드를 관리하는 Dispatchers.Default 를 사용해 공유 가변 변수를 증가시키는 간단한 동작으로 시작하자.
var counter = 0
fun main() = runBlocking {
withContext(Dispatchers.Default) {
massiveRun {
counter++
}
}
println("Counter = $counter")
}
📌 전체 코드는 이곳에서 확인할 수 있습니다.
마지막에 무엇이 출력될까? 이것이 “Counter = 100000”을 출력할 가능성은 거의 없다. 100개의 Coroutine이 동기화 없이 여러 스레드에서 counter을 동시에 증가시키기 때문이다.*1
📖 아래 내용은 독자의 이해를 위해 번역자가 추가한 글입니다.
*1. 여러 스레드에서 변수에 동시 접근할 뿐만 아니라 값을 갱신 시키는 동작까지 하기 때문이다.
이 글은 Coroutines 공식 문서를 번역한 글입니다.
원문 : Shared mutable state and concurrency - The problem
원문 최종 수정 : 2022년 6월 27일
Volatile은 동시성 문제를 해결하지 못한다
변수를 volatile로 만드는 것이 동시성 문제를 해결한다는 잘못된 인식이 있다. 시도해보자 :
@Volatile // in Kotlin `volatile` is an annotation
var counter = 0
fun main() = runBlocking {
withContext(Dispatchers.Default) {
massiveRun {
counter++
}
}
println("Counter = $counter")
}
📌 전체 코드는 이곳에서 확인할 수 있습니다.
이 코드는 느리게 동작하지만, 여전히 마지막에 “Counter = 100000”을 얻기 못한다. volatile 변수가 선형화(기술적인 용어로 “원자성”이라 한다)된 읽기와 쓰기를 제공하지만, 값을 증가시키는 것과 같은 더 큰 동작*1에 대해서는 원자성을 제공하지 않는다.
📖 아래 내용은 독자의 이해를 위해 번역자가 추가한 글입니다.
*1. 읽기+쓰기를 한 번에 하는 동작을 뜻한다. 읽기와 쓰기 각각에 원자성이 보장되더라도 읽기+쓰기 동작은 그 중간에 원자성이 보장되지 않는 시간이 있어서 그렇다.
이 글은 Coroutines 공식 문서를 번역한 글입니다.
원문 : Shared mutable state and concurrency - Volatiles are of no help
원문 최종 수정 : 2022년 6월 27일