소개

7장_스레드 한정, 액터 그리고 뮤텍스

개요

스레드 간에 상태 공유를 회피하는데 사용할 수 있는 몇가지 도구들에 대해 소개
이 챕터에서 다루는 방법을 사용한다면 레이스 컨디션을 방지할 수 있다

원자성 위반; Atomicity Violation

공유 상태에 관해 언급할 때 흔히 많은 스레드에서 하나의 변수를 읽거나 쓰는 것에 비유
→ 서로 다른 스레드에서 하나의 변수에 대한 읽기와 쓰기의 동시수행
공유 상태를 가지는 경우 동시성 코드 블록에서 문제가 발생할 수 있다. → 스레드의 캐시, 메모리 액세스의 원자성 으로 인해 다른 스레드에서 수행한 수정사항의 유실 → 상태의 일관성을 해치는 원인이 됨
결국 동시성 코드 블록에 대한 동기화가 필요. → 책에선 두가지를 제시

원자성 위반 문제를 해결하는 방법#1 - 스레드 한정

개요

공유 상태에 접근하는 모든 코루틴을 단일 스레드에서 실행되도록 한정하는 것
상태가 더 이상 스레드 간에 공유되지 않으며, 하나의 스레드만 상태를 수정
즉, 하나의 스레드만 상태와 상호 작용하도록 보장하여 쓰기가 아닌 읽기 전용으로만 공유할 수 있게 하는 것

스레드 한정 솔루션#1 - 코루틴 자체를 단일 스레드로 한정

private val context = newSingleThreadContext("somethingContext")
Kotlin
복사
상태의 변경이 한 곳에서만 발생할 때 사용 가능

스레드 한정 솔루션#2 - 액터

private var counter = 0 private val context = newSingleThreadContext("counterActor") // read-only fun getCounter() = counter val actorCounter = actor<Void?>(context) { for (msg in channel) { counter++ } } // use fun main() = runBlocking { val workerA = asyncIncrement(2000) val workerB = asyncIncrement(100) workerA.await() workerB.await() print("counter [${getCounter()}]") } fun asyncIncrement(by: Int) = GlobalScope.async { for (i in 0 until by) { actorCounter.send(null) } }
Kotlin
복사
여러 다른 부분에서 공유 상태를 수정해야 하거나, 원자성을 지녀야하는 블록에 대한 더 높은 유연성을 원하는 경우 솔루션#1 대신 사용
더 높은 유연성? → 외부에서는 그냥 메시지를 보내고, 액터에서는 메 시지를 받아서 처리만 하면 되기 때문에 역할을 분리하여 더 높은 유연성을 도모 + 여러 부가 기능을 액터에 응집시킬 수 있음
액터가 차지하는 메모리 공간은 어느 다른 스레드 혹은 액터도 접근할 수 없다. 즉, 액터 내부에서 일어나는 일은 어느 누구와도 공유되지 않는다.

원자성 위반 문제를 해결하는 방법#2 - 상호 배제

개요

동기화 방법#1 - 스레드 한정의 경우 코드 블록 전체가 단일 스레드에서만 발생하도록 보장함으로써 원자성 위반을 회피
상호 배제는 코드 블록의 실행이 여러 스레드에서 발생할 수 있지만, 동시에는 하나의 스레드만 실행할 수 있도록하는 매커니즘

상호배제 솔루션 - 뮤텍스

val mutex = Mutex() fun asyncIncrement(by: Int) = GlobalScope.async { for (i in 0 until by) { mutex.withLock { counter++ } } } java.concurrent.Lock ㄴ tryLock ㄴ lock ㄴ unlock
Kotlin
복사
한 번에 하나의 코루틴만 Lock을 보유하고, Lock을 시도하는 다른 코루틴은 일시중단
+) 추가적인 Mutex API
val mutex = Mutex() mutex.lock() // 이미 Lock 상태라면 일시 중단 // something mutex.unlock() // not suspending
Kotlin
복사
val mutex = Mutex() mutex.tryLock() // return Boolean
Kotlin
복사

원자성 위반 문제를 해결하는 방법#3 - 휘발성 변수

앞선 예시에서 사용된 원자성 위반을 회피/상호배제하는 예시와는 케이스가 다르다.

스레드 캐시

JVM의 각 스레드는 비휘발성 변수의 캐시된 사본을 가질 수 있다.
이 캐시는 항상 변수의 실제 값과 동기화되지 않는다.
한 스레드에서 공유 상태를 변경하면 캐시가 업데이트 될 때까지 다른 스레드에서는 볼 수 없다.
@Volatile var shutdownRequested = false
Kotlin
복사
@Volatile : 변수의 변경사항을 다른 스레드에 즉시 표시하는 방법(즉시라고는 하지만, 그에 준하는 수준)
값 갱신 - 조회에 대해 thread-safe 하지 않기 때문에 공유 상태에서 발생하는 이슈를 해결할 수 없다
그럼 왜 쓰냐? → 100% 동기화되지 않아도 문제가 되지 않는 경우 유용하게 사용 가능
단, 이를 사용하기 위해선 두 가지 조건을 만족하여야 함
1.
변수 값의 변경은 현재 상태에 의존하지 않는다.
2.
휘발성 변수는 다른 변수에 의존하지 않으며, 다른 변수도 휘발성 변수에 의존하지 않는다.
+) usecase
@Volatile private var shutdownRequested = false fun shutdown() { shutdownRequested = true } fun process() { while (!shutdownRequested) { // game render // process something } // game end }
Kotlin
복사
shutdown()에서 작성된 shutdownRequested의 수정은 변수 자체의 현재 상태에 의존하지 않는다. 항상 true로 설정된다.
다른 변수는 shutdownRequested에 의존하지 않으며, 다른 변수에도 의존하지 않는다.
이 방법의 경우 모든 스레드가 shutdown 요청을 할 수 있으며, 모든 스레드가 즉시 값의 변경을 볼 수 있다는 것.

원자성 위반 문제를 해결하는 방법#4 - 원자적 데이터 구조

val counter = AtomicInteger() // ConcurrentHashMap << fun asyncIncrement(by: Int) = GlobalScope.async { for (i in 0 until by) { counter.incrementAndGet() } }
Kotlin
복사

Conclusion

공유 상태를 가지는 경우 동시성 코드에서 문제가 될 수 있다.
→ 다른 스레드에서 수행한 수정 사항이 유실될 수 있다.
잘 고민하고 잘 작성하자. → 노하우는 To be continue…