코루틴만의 동시성 제어 방식에 대하여 살펴봅니다.
스레드에서의 일반적인 임계구역 설정
runBlocking {
@Synchronized
fun criticalSection() {
println("Start")
Thread.sleep(10)
println("End")
}
repeat(2) {
thread {
criticalSection()
}
}
}
아래와 같이 우리는 2개의 스레드를 실행하여 Start, End 가 순서대로 표현되길 기대합니다.
Start
End
Start
End
코루틴에서는?
스레드를 코루틴으로 변경한다고 가정해보겠습니다.
runBlocking {
@Synchronized // 오류
suspend fun criticalSection() {
println("Start")
delay(10)
println("End")
}
repeat(2) {
CoroutineScope(Dispaters.Default).launch {
criticalSection()
}
}
}
@Synchronized 어노테이션을 추가한 함수는 suspend 함수로 사용이 금지되어 있어, 코루틴과 호환되지 않음을 직감적으로 알 수 있습니다.
사실 이렇게 금지해놓은 것은, 일반적인 스레드와는 달리 CPS 작동 방식과 스레드를 잘개 쪼개서 사용하는 코루틴의 특징적인 이유도 있을 것 입니다.
Race Condition 상황과 synchronized 블록
runBlocking {
var count = 0
val increaseJob = CoroutineScope(Dispatchers.Default).launch { // A
repeat(10000) {
synchronized(this) { // 효과 없음
count++
}
}
}
val decreaseJob = CoroutineScope(Dispatchers.Default).launch { // B
repeat(10000) {
synchronized(this) { // 효과 없음
count--
}
}
}
increaseJob.join()
decreaseJob.join()
println("Completed: $count")
}
하나의 자원(count)을 접근하는 두 개의 코루틴이 있다고 가정해봅시다.
A 코루틴에서는 count 를 10,000 회 증가, B 코루틴에서는 count 를 10,000 회 감소 시킵니다.
우리는 최종 count 결과 값으로 0이 나올 것을 기대하지만 결과는 실행할 때마다 달라집니다.
Completed: 570 // :(
synchronized 블록으로 처리했음에도 불구하고 임계영역 설정은 효과가 없습니다.
Mutex
코루틴에서의 임계영역 설정은 Mutex 를 사용하면 목적을 달성할 수 있습니다.
Mutex 가 코루틴 패키지 내에 존재하는 것도 의도된 부분입니다.
runBlocking {
val mutex = Mutex()
suspend fun criticalSection() = mutex.withLock {
println("Start")
delay(10)
println("End")
}
repeat(2) {
CoroutineScope(Dispatchers.Default).launch {
criticalSection()
}
}
}
우리가 기대했던 결과가 예상대로 잘 나왔고, 코루틴의 Mutex 를 사용해서 동기화도 성공하였습니다.
Start
End
Start
End
다른 방법은 없을까?
newSingleThreadContext()를 사용, 코루틴에서 사용하는 스레드를 하나로 제한하여 공유 자원에 대한 접근을 단순화 시키는 방법AtomicXXX와 같은 클래스를 사용하여 스레드로 부터 안전하게 자원의 원자성을 유지하는 방법(제대로 동작하지 않음)@Volatile변수를 사용하는 방법