coroutine 에서의 multi-thread conflict avoid

참치돌고래·2022년 12월 12일
0

java에서는 클래스의 필드변수로 있는 경우, synchronized나 atomicReference를 통하여 thread-safe하게 작동하게 만들고는 한다. 반면, kotlin에서 coroutine 을 사용하여 java와는 다른 방법으로 thread-safe하게 작동하도록 만들 수 있다.

dispatcher limited to a single thread

coarse-grained thread confinement

val dispatcher = Disptchers.IO.limitedParallelism(1)

fun main() = runBlocking{
	massiveRun {
    	withContext(disptacher) {
        	counter++
        }
    }
    println(counter)
}

이렇게 single-thread 를 통해서만 작동하게 하는 방법을 coarse-grained thread confinement라고 한다. 보통 변수를 lock을 걸 수 없을 때 사용한다. 하지만, 쓰레드를 하나 사용하는 만큼, 멀티쓰레드에 비해 성능이 떨어진다. 또한 해당 자원이 빈번하게 요청이 들어올 경우 병목현상이 발생할 수 있다.

fine-grained thread confinement

위에서 경우와 반대로 fine-grained thread confinement 접근방식으로 구현해보자. 오직 race-condition 이나 동시성 문제가 발생할 수 있는 부분만 해당 single-thread 로 작동하게 만드는 것이다.
즉, 동시성 문제가 발생할 수 있는 자원 접근에 대해서만 withContext(...)를 통해서 작동하게 만드는 것이다.

val dispatcher = Disptchers.IO.limitedParallelism(1)

suspend fun fetchuser(id : Int) {
	val newUser = api.fetchUser(id)
    withContext(dispatcher) { //해당 부분만 withContext로 wrapping
    	users += newUser
    }
}
}
 

Mutex

앞서 구현한 dispatcher의 옵션을 건드는 방식보다 훨씬 가볍고, 성능 역시 우위이다. 하지만, mutex 가 반환되지 못하는 케이스가 발생하면 영원히 접근을 하지못하게 된다. 이러한 문제는 finally 구문을 통해 해결하곤 한다.
dispatcher와 비교해서 가장 부각되는 단점은 coroutine 역시 block 된다는 것이다.


suspend fun add(message: String) = mutex.withLock { delay(1000) // we simulate network call messages.add(message)
}

suspend fun main() {
val repo = MessagesRepository()
val timeMillis = measureTimeMillis { coroutineScope {
            repeat(5) {
                launch {
} }
}
repo.add("Message$it") }
    println(timeMillis) // ~5120
}

add 라는 함수가 delay를 통해 지연되는 동안 mutex의 경우는 쓰레드가 다음 coroutine으로 넘어가는 것이 아니라 기다리게 된다. 반면에, 앞서 구현한 dipatcher 의 케이스는 다음 coroutine으로 넘어가는 것이 가능하다.

profile
안녕하세요

0개의 댓글