- 코루틴은
Dispatchers.Default
와 같이 멀티 스레드를 관리하는 Dispatcher
에 의해 병렬적으로 실행될 수 있음.
- 병렬 실행 시 가장 중요한 문제는
변경 가능한 공유 상태
의 동기화임.
Coroutine을 여러개 실행했을 때의 문제점
import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis
var counter = 0
fun main() = runBlocking {
withContext(Dispatchers.Default) {
massiveRun { counter++ }
}
}
suspend fun massiveRun(action: suspend () -> Unit) {
val n = 100
val k = 1000
val time = measureTimeMillis {
coroutineScope {
repeat(n) {
launch {
repeat(k) { action() }
}
}
}
}
println("Counter = $counter")
}
실행결과
Counter = 40101
- 복수의 스레드를 관리하는
Dispatchers.Default
를 사용해 공유 변수의 값을 증가시키는 동작임.
- 출력의 결과는 “Counter = 100000”을 출력할 가능성은 거의 없음.
- 100개의 코루틴이 동기화 없이 여러 스레드에서 counter를 동시에 증가시킴.
Volatile은 동시성 문제를 해결하지 못한다.
Volatile이란?
@Volatile을 붙이면 변수의 값이 메인 메모리에만 저장되며, 멀티 쓰레드 환경에서 메인 메모리의 값을 참조하므로 변수 값 불일치 문제를 해결할 수 있게된다.
다만 CPU캐시를 참조하는 것보다 메인메모리를 참조하는 것이 더 느리므로, 성능은 떨어질 수 밖에 없다.
<https://www.charlezz.com/?p=45959>
@Volatile
var counter = 0
- 변수를
volatile
로 만드는 것이 동시성 문제를 해결한다는 잘못된 인식이 있음.
volatile
은 값을 증가시키는 것과 같은 동작에는 원자성을 제공하지 않음.
Thread-safe한 데이터 구조
- 스레드와 코루틴에 모두 작동하는 일반적인 해결 방법은 공유 상태에 수행되어야하는 동작에 필수적인 동기화를 제공하는 스레드 안전한 데이터 구조를 사용하는 것임.
- 스레드 안전한은 동기화된, 선형선, 원자성 이라고도 부름.
val counter = AtomicInteger()
fun main() = runBlocking {
withContext(Dispatchers.Default) {
massiveRun {
counter.incrementAndGet()
}
}
}
실행결과
Counter = 100000
AtomicInteger
는 원자적인 동작을 제공함.
세밀하게 Thread 제한하기
- 스레드 제한
- 단일 스레드로 제한된 공유 상태 문제에 대해 접근하기.
- 세밀하게 스레드를 제한하기 때문에 아주 느리게 동작함.
val counterContext = newSingleThreadContext("CounterContext")
var counter = 0
fun main() = runBlocking {
withContext(Dispatchers.Default) {
massiveRun {
withContext(counterContext) {
counter++
}
}
}
}
굵게 Thread 제한하기