java에서는 클래스의 필드변수로 있는 경우, synchronized나 atomicReference를 통하여 thread-safe하게 작동하게 만들고는 한다. 반면, kotlin에서 coroutine 을 사용하여 java와는 다른 방법으로 thread-safe하게 작동하도록 만들 수 있다.
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
접근방식으로 구현해보자. 오직 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
}
}
}
앞서 구현한 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으로 넘어가는 것이 가능하다.