반응형
- 코루틴은 Dispatcher.Default와 같은 멀티 스레드 디스패쳐를 통해서 동시에 수행되도록 만들수 있다.
- 이러한 상황에서 주로 발생하는 문제는 동기화 이슈가 있으며 일반적인 멀티 스레드에서의 동기화 해결책들과 비슷하지만 코루틴 고유의 것들도 존재 (Actor)
- Volatile [공유객체]
- 하나의 객체를 여러 개의 쓰레드가 함께 사용하거나 공유한다는 것을 의미
- ex) 놀이터에 그네가 하나 있고, 어린 아이들은 셋 있다. 이런 상황에서 그네를 [공유 객체], 아이들을 [Thread]라고 말할 수 있다.
- 클래스의 멤버변수는 heap 메모리에 존재하므로 스레드가 공유하여 접근할 수 있으며 이때 각 스레드는 속도향상을 위해 메인 메모리에 접근해서 값을 가져가는게 아닌, 캐시에서 변수값을 읽어가며 쓰기 또한 캐시값을 이용, 어떤 시점에 변경된 캐시 값에 메인 메모리에 업데이트 된다.
- 따라서 멀티 스레드가 해당 변수의 값을 읽거나 쓰면 각각 자신의 캐시에서 값을 읽고 쓰고, 어떤 시점에서 변경된 값을 메인 메모리에 업데이트 하기 때문에 실제 원하는 갑솨 달라지게 된다.
- 이러한 문제를 막기위한 키워드가 volatile이며 해당 키워드는 접근 가능한 변수의 값을 캐시를 통해 사용하지 않고 스레드가 직접 메인 메모리에 접근하여 읽고 쓴다.
- 하지만 volatile로도 완벽한 동기화를 할 수 없으며 문제점도 있다.
- 캐시를 이용하지 않고 메인 메모리에 직접 액세스 하기 때문에 더 비싼 코스트를 지불
- volatile 변수는 읽기 쓰기가 JVM에 의해 리오더링 되지 않는다.
- volatile 변수는 리드시 항상 최신값을 반환하지만 여러 쓰레드가 동시 읽기, 쓰기를 하면 쓰기 시점을 알수 없기 때문에 여전히 동기화 문제가 일어난다.
class Worker : Thread() { @Volatile var stop = false override fun run() { super.run() while(!stop){ } } }
- Mute
- 여러 스레드를 실행하는 환경에서 자원에 대한 접근 제한을 강제하기 위한 동기화 매커니즘
- ex) 화장실에 있는 유일한 변기칸(공유된 자원)
- 대변이 마려운 한명이 변기칸에 들어감
- 들어간 사람은 이제 변기칸에 다른 사람이 들어오지 못하도록 문을 걸어 잠굼(Lock)
- 대변이 마려운 새로운 사람이 화장실 도착, 유일한 변기칸에 진입을 시도
- 기존에 들어간 사람이 나올 때까지 기다려야 함
- 기존에 들어간 사람이 나올 때까지 기다리는 사람이 더 증가
- 자바에서는 Synchronized나 ReentrantLock을 이용하여 critical section을 블락시킨다.
- 코루틴에서는 lock과 unlock 메소드를 가지는 Mutex를 이용하며 Mutex.lock()은 suspend function이다.
suspend fun CoroutineScope.massiveRun(action: suspend () -> Unit) { ... // do something } val mutex = Mutex() var counter = 0 fun main() = runBlocking { GlobalScope.massiveRun { mutex.withLock { // 상호ㅓ배제를 사용한 보호 counter++ } } println("Counter = $counter") }
mutex.lock() // 보호하고자 하는 임계 구역 코드 try { ... } finally { mutex.unlock() }
- 이러한 예제는 작은 단위의 스레드 한정이므로 성능면에서는 손실이 있지만 특정 상황에서는 괜찮은 선택이 될 수 있다.
- ex) 공유되는 상태를 어쩔 수 없이 주기적으로 변경해야 하고 이를 위한 스레드 한정은 적용하기 어려운 경우
- Actor
- 코루틴에서 쓰레드간 동기화를 지원하기 위한 도구
- 코루틴과 코루틴 내부로 캡슐화 된 상태값, 그리고 다른 코루틴과 통신할 수 있는 채널의 조합으로 구성 되어 있다.
- 상태 엑세스를 단일 스레드로 한정한다.
- 다른 쓰레드는 채널을 통해서 상태 수정을 요청한다.
- 다른 언어의 actor 개념은 들어오고 나가는 메일 박스 기능과 비슷하지만 코틀린에서는 들어오는 메일 박스 기능만 한다고 볼 수 있다.
- 액터는 한 번에 1개의 메시지만 처리하는 것을 보장하며 어떤 특정 상태를 관리하기 위한 백그라운드 테스크에 유용함
- 채널과 같기 때문에 메시지를 보낼 때 send()를 사용한다.
val actorCounter = actor<Void?> { for (msg in channel) { counter++ } } val workA = async (context){ repeat(2000) { actorCounter.send(null) } } val workB = async (context){ repeat(300) { actorCounter.send(null) } } workA.await() workB.await() println(counter)
- Flow
- 코루틴의 플로우는 데이터 스트림이며, 코루틴 상에서 리액티브 프로그래밍을 지원 하기 위한 구성요소이다.
- 기존 명령형 프로그래밍에서는 데이터의 소비자는 데이터를 요청한 후 받은 결과값을 일회성으로 수신하지만 이러한 방식은 데이터가 필요할 때마다 결과값을 매번 요청해야 한다는 점에서 매우 비효율적이다.
- 리액티브 프로그래밍에서는 데이터를 발행하는 발행자가 있고 데이터의 소비자는 데이터의 발행자에 구독 요청을 하고 데이터의 발행자는 새로운 데이터가 들어오면 데이터의 소비자에게 지속적으로 발행한다.
- 즉, 리액티브 프로그래밍에는 하나의 데이터를 발행하는 발행자가 있고 해당 발행자는 데이터의 소비자에게 지속적으로 데이터를 전달하는 역할을 한다. 이것을 데이터 스트림이라고 한다.
- 데이터 스트림은 아래 세가지로 구성되며 flow의 핵심 구성 요소이다.
- Producer(생산자)
- Intermediary(중간 연산자)
- Consumer(소비자)
fun getBroadCastInfo(id: String): Flow<BroadCastInfo) {
return flow {
val data = sauceLiveApi.getBroadCastInfo(id)
emit(data)
}
viewModelScope.launch {
playerRepository.getBroadCastInfo(id)
.flowOn(Dispatchers.IO)
.catch {}
.collect {}
}
- Flow의 함수들
https://velog.io/@wijoonwu/Thread-와-공유객체
https://drcode-devblog.tistory.com/297
https://www.charlezz.com/?p=45959
https://myungpyo.medium.com/코루틴-공식-가이드-자세히-읽기-part-8-1b434772a100
https://origogi.github.io/coroutine/액터/
https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/
반응형