문제점
- 멀티 스레딩 환경에서 정수를 1씩 증가시키는 작업이 있다면 적절하게 동기화되지 않으면 원하는 값을 얻지 못할 것임.
동기화 블로킹
- 공유 상태에 대한 문제로 자바에서 사용되는 전통적인 도구인
synchronized
블록이나 동기화된 컬렉션
을 사용하여 해결할 수 있음.
fun main() = runBlocking {
val lock = Any()
massiveRun {
syncronized(lock) { // 스레드를 블로킹함
counter++
}
}
}
syncronized
블록 내부에선 중단 함수를 사용할 수 없음.
syncronized
블록에서 코루틴이 자기 차례를 기다릴 때 스레드를 블로킹함.
- 블로킹 없이 중단하거나, 충돌을 회피하는 방법을 사용할 것.
원자성
- 원자값을 활용한 연산은 빠르며 스레드 안전을 보장함.
- 원자성 연산은 락 없이 로우 레벨로 구현되어 효율적이고 사용하기가 쉽다.
private var counter = AtomicInteger()
fun main() = runBlocking {
massiveRun {
counter.incrementAndGet()
}
}
- 원자값은 의도대로 완벽하게 동작하지만 사용성이 제한되기 때문에 조심해서 다뤄야 함.
- 하나의 연산에서 원자성을 가지고 있다고 해서 전체 연산에서 원자성이 보장되는 것은 아님.
- read-only인 리스트를
AtomicReference
로 래핑할 수 있음.
private val users = AtomicReference(listOf<User>())
suspend fun fetchUser(id: Int) {
val newUser = api.fetchUser(id)
users.getAndUpdate { it + newUser }
}
- 충돌 없이 값을 갱신하기 위해선
getAndUpdate
라는 원자성 보장 함수를 사용할 것.
싱글스레드로 제한된 디스패처
- 단일 스레드로 사용하는 것은 대부분 공유상태 문제를 해결하는 가장 쉬운 방법임.
newSingleThreadContext
limitedParallelism(1)
- 두 가지 방법으로 디스패처를 사용할 수 있음.
- 코스 그레인드 스레드 한정
- 디스패처를 싱글스레드로 제한한
withContext
로 전체 함수를 래핑.
- 파인 그레인드 스레드 한정
- 상태를 변경하는 구문들만 래핑
- 해당 방법은 번거롭지만, 크리티컬 섹션이 아닌 부분이 블로킹되거나 CPU 집약적인 경우에 더 나은 성능을 제공함.
- 위 두 가지 방법은 일시 중단 함수에 적용하는 경우에는 성능에 큰 차이가 없음.