코루틴에서의 락
우리는 이전 글에서 코루틴에서 ReentrantLock 사용해 락을 걸게 될 경우, 락이 해제 되기 전에 코루틴의 스레드 양보가 일어나면 데드락이 발생할 수 있다는 것을 알아보았다.
그렇다면, 코루틴에서 안전한 임계 영역(Critical Area)을 만들기 위해서는 어떻게 해야 할까? 바로 젯브레인의 코루틴 라이브러리(kotlinx.coroutines)에서 제공하는 Mutex 객체를 사용해야 한다.
Mutex 사용해 코루틴에 락 걸기
Mutex 객체는 코루틴에서 안전한 임계영역을 만들기 위해 사용하는 객체이다. Mutex 객체는 ReentrantLock과 사용법이 매우 비슷하다. lock 함수를 사용해 락을 걸 수 있으며, unlock 함수를 통해 락이 해제될 때까지 다른 코루틴이 임계영역에 접근할 수 없다.
이전 글에서 다뤘던 SafeAdder 클래스에서 Mutex로 사용하도록 바꾸면 다음과 같아진다.
class SafeAdder() {
private val lock = Mutex()
var value: Int = 0
suspend fun add() {
lock.lock()
value += 1
lock.unlock()
}
}
이를 사용해 다음 main 함수를 실행해보자. 이 main 함수는 백그라운드 스레드에서 실행되는 1000개의 코루틴을 생성하며 각 함수는 SafeAdder 객체의 add 함수를 호출한다.
fun main() = runBlocking<Unit> {
val safeAdder = SafeAdder()
withContext(Dispatchers.Default) { // 백그라운드 스레드에서 실행
repeat(1000) { // 1000번 반복
launch { // 코루틴 생성
safeAdder.add()
}
}
}
println("safeAdder.value >> ${safeAdder.value}")
}
그러면 1000개의 연산 중 단 하나의 연산도 손실되지 않아 다음과 같은 결과가 나오는 것을 볼 수 있다.
Mutex를 사용해야 하는 이유
1. 안전한 임계 영역 만들기
앞서 ReentrantLock에서 코루틴의 스레드 양보로 인해 lock 함수를 호출한 스레드와 재개되는 스레드가 동일하지 않을 때 unlock이 호출되지 않아 문제가 생겼던 것을 떠올려보자. 같은 상황을 재현하기 위해 위의 SafeAdder의 add 함수에 delay(1L)을 통해 스레드를 양보할 수 있는 부분을 추가해보자.
class SafeAdder() {
private val lock = Mutex()
var value: Int = 0
suspend fun add() {
lock.lock()
delay(1L) // 스레드 양보 부분 추가
value += 1
lock.unlock()
}
}
그런 후 다시 main 함수를 실행해보면 결과 값은 이전과 같이 1000이 나오게 되며, 락에 문제가 생기지 않는다.
fun main() = runBlocking<Unit> {
val safeAdder = SafeAdder()
withContext(Dispatchers.Default) {
repeat(1000) {
launch {
safeAdder.add()
}
}
}
println("safeAdder.value >> ${safeAdder.value}")
}
// 결과 출력: 데드락이 발생하지 않는다.
// safeAdder.value >> 1000
그 이유는 Mutex 객체의 특성에 있다. Mutex 객체는 코루틴이 lock을 호출하면, 같은 코루틴이 unlock을 호출할 때까지 다른 코루틴이 임계영역에 접근하지 못하도록 한다. 따라서 락을 건 코루틴이 락을 해제할 때까지 기다리며, 이를 통해 락을 안전하게 걸 수 있다.
* ReentrantLock 은 '다른 스레드'가 임계영역에 접근하지 못하도록 한다.
2. 스레드 양보
Mutex를 써야하는 두 번째 이유는 바로 스레드 양보 기능이다. Thread1에서 실행되는 A코루틴에서 Mutex의 lock이 호출되면, Thread2에서 실행되는 B코루틴이 Mutex의 lock을 호출 했을 때, 기존의 락이 제거되기 전까지 스레드를 양보한다. 이를 통해 스레드를 효율적으로 사용할 수 있다.
withLock 함수를 통해 lock과 unlock 안전하게 사용하기
Mutex에도 ReentrantLock과 마찬가지로 withLock함수가 있어 이를 사용하면, lock과 unlock쌍을 더욱 안전하게 사용할 수 있다. lock과 unlock 함수를 직접 호출하기 보다 withLock 함수를 사용하는 습관을 들이자.
class SafeAdder() {
private val lock = Mutex()
var value: Int = 0
suspend fun add() {
lock.withLock { // 람다식을 실행하기 전 lock, 실행한 후 unlock 호출
value += 1
}
}
}