개요
•
스레드 간에 상태 공유를 회피하는데 사용할 수 있는 몇가지 도구들에 대해 소개
•
이 챕터에서 다루는 방법을 사용한다면 레이스 컨디션을 방지할 수 있다
원자성 위반; 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…