ReentrantLock 사용해 락 걸기
코틀린에서는 기본적으로 자바의 ReentrantLock 클래스를 사용해 락을 걸고 해제할 수 있다. ReentrantLock을 사용해 락을 걸고 해제하는 방법은 간단하다. 임계영역(Critical Area)의 시작지점에서 ReentrantLock 객체의 lock 함수를 호출하고 임계영역의 종료 지점에서 ReentrantLock의 unlock 함수를 호출하면 된다.
ReentrantLock 객체를 사용해 보기 위해 다음과 같은 SafeAdder 클래스를 만들어보자. 이 SafeAdder 클래스는 add 함수를 여러 스레드가 동시에 호출하더라도 손실되는 연산 없이 더하기 연산이 수행되도록 만든 클래스이다.
*물론 add 함수에 @Synchronized를 붙이면 간단하게 처리할 수 있지만 여기서는 ReentrantLock을 사용하기 위해 다음과 같이 처리한다.
class SafeAdder() {
private val lock = ReentrantLock()
var value: Int = 0
fun add() {
lock.lock() // 임계 영역 시작
value += 1
lock.unlock() // 임계 영역 종료
}
}
SafeAdder 클래스를 만들었으면, 백그라운드 스레드에서 실행되는 1000개의 코루틴에서 SafeAdder 객체의 add 함수에 동시 접근하도록 만든 후, SafeAdder 클래스의 value 값을 살펴보자. 그러면
fun main() = runBlocking<Unit> {
val safeAdder = SafeAdder()
withContext(Dispatchers.Default) {
repeat(1000) {
launch(Dispatchers.Default) {
safeAdder.add()
}
}
}
println("safeAdder.value >> ${safeAdder.value}") // safeAdder.value >> 1000
}
그러면 1000개의 연산 중 단 하나도 손실되지 않아 'safeAdder.value >> 1000' 이 출력되는 것을 나오는 것을 볼 수 있다.
ReentrantLock의 withLock 함수로 락을 안전하게 사용하기
위와 같이 lock과 unlock 함수를 직접 호출하는 방법으로도 임계 영역을 만들 수 있지만, 이렇게 두 함수를 매번 호출하다 보면 하나를 빼먹는 실수를 할 수 있고, 이 실수는 심각한 버그를 만들 수 있다. 예를 들어 누군가 실수로 위의 SafeAdder 객체를 다음과 같이 만들 수 있다.
class SafeAdder() {
private val lock = ReentrantLock()
var value: Int = 0
fun add() {
lock.lock() // 임계 영역 시작
value += 1
}
}
만약 위와 같이 바꾼 후 다시 위에서 만든 main 함수를 실행해보면, 데드락 때문에 프로그램의 실행이 끝나지 않는 것을 볼 수 있다.
이런 실수를 줄이기 위해 lock, unlock 쌍을 사용하기보다는 withLock 함수를 사용하는 것이 권장된다. withLock 함수는 람다식을 인자로 받으며, 람다식 진입 전에 lock을 호출하고, 람다식을 모두 실행하면 unlock을 호출해 이 두 쌍을 안전하게 사용할 수 있게 한다.
class SafeAdder() {
private val lock = ReentrantLock()
var value: Int = 0
fun add() {
lock.withLock {
value += 1
}
}
}
정리
- ReentrantLock을 사용해 임계영역을 만들 수 있다.
- ReentrantLock을 사용할 때는 lock과 unlock을 직접 사용하기보다는 withLock 함수를 사용하는 것이 안전하다.