ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 코루틴에서 java 패키지의 ReentrantLock 의 문제점
    개발 흉내 2024. 7. 6. 14:31
    @Service
    class ProductService {
        companion object {
            val lock = ReentrantLock() // are you sure?
        }
    
        suspend fun task() {
            lock.lock()
    
            try {
                // ... do task
                // ... do task
                // ... do task
            } finally {
                lock.unlock()
            }
        }
    }

    동료의 코드를 리뷰하던 중 ReentrantLock 을 사용하는 코드를 보았다. ReentrantLock 는 코루틴 작업들의 동시 접근을 제어하고 있었는데, ReentrantLock 의 동작이 코루틴과 잘 맞는 지 의문이 들었고 이에 대해 더 조사해보기로했다.

     

    ReentrantLock 는 다른 스레드의 접근을 막지만, 같은 스레드가 여러번 접근하는 것은 허용한다는 Lock 구현체이다. Lock 의 진입점을 여러 곳에 두고 접근한다는 것 자체가 Clean 하지 못하다는 느낌은 들지만 분명 쓰임새가 있는 Lock 이다.

     

    하지만 같은 스레드는 허용한다는 점에서 코루틴에서 사용한다면 문제가 될 수 있다.

     

    코틀린의 코루틴은 보통 여러 Thread 를 생성하지않고, Dispatcher 라는 Thread Pool 위에서 동작한다. Dispatcher 는 코루틴을 적절한 스레드에 할당하여 효율적인 동시성이 가능하도록 도와준다. 구체적으로는 스레드가 하나의 작업이 끝내면 스레드가 종료되는 게 아니라 그대로 다음 작업을 시작한다. 즉, 하나의 스레드가 여러 작업을 수행하는 것이다.

    그렇다면 코루틴 내에서 ReentrantLock 을 사용할 경우 어떤 일이 일어날까? 서로 다른 사용자 요청에 대해 코루틴이 동작했지만 Dispatcher 가 "우연히" 같은 스레드에게 코루틴을 전달하면 ReentrantLock 는 동시에 접근을 허용해버린다.

     

    예제 코드를 보자.

    val lock = ReentrantLock()
    
    suspend fun task(name: String) {
        lock.lock()
    
        try {
            (1..3).forEach {
                println("$name is running... $it")
                delay(1000)
            }
        } finally {
            lock.unlock()
        }
    }
    
    fun main() {
        runBlocking {
            val singleThreadDispatcher = newFixedThreadPoolContext(1, "a-single-thread")
    
            launch(singleThreadDispatcher) {
                task("firstTask")
            }
    
            launch(singleThreadDispatcher) {
                task("SecondTask")
            }
        }
    }

    task 함수는 두 번의 launch 에 의해 동시에 실행되었지만, task 는 ReentrantLock 에 의해 한번에 하나씩 순차적으로 실행될 것을 기대했을 것이다.

     

    하지만 결과는 다르다.

    firstTask is running... 1
    SecondTask is running... 1
    firstTask is running... 2
    SecondTask is running... 2
    firstTask is running... 3
    SecondTask is running... 3

    두 코루틴 함수는 동시에 실행되었다. ReentrantLock 가 같은 스레드라고 판단했기 때문이다. 이렇듯 코루틴에서 ReentrantLock 과 같이 스레드에 민감하게 동작하는 코드들은 주의가 필요하다.

     

    대신에 Mutex 와 같이 kotlinx.coroutines 패키지에서 지원하는 lock 을 쓰면 올바르게 동작한다.

    val lock = Mutex()
    firstTask is running... 1
    firstTask is running... 2
    firstTask is running... 3
    SecondTask is running... 1
    SecondTask is running... 2
    SecondTask is running... 3

     

    전체 코드는 아래에서 실행해볼 수 있다.

    https://pl.kotl.in/JmL0Vnr_C

    댓글

Designed by Tistory.