- 시작하기 앞서 코틀린 채널에 대해서 다시 한번 떠올려 보자
- 채널이란 : 2개의 코루틴 사이를 연결한 파이프라인 정도라고 보면 된다.
- 스레드간 커뮤니케이션
- 코루틴 말고도 우리는 보통 코딩을 할 때 리소스를 블로킹 하는 작업 (네트워킹, DB사용) 들은 스레드로 떼어낸다.
- 이 스레드간 공유할 자원이 필요할때 우리는 두개의 스레드가 동시에 그걸 쓰거나 읽게 하지 못하도록 자원을 lock 하거나 메모리에 의존했다.
- 메모리 공유 → 이것이 우리가 주로 스레드간 커뮤니케이션을 할때 사용한 방법이다.
- 하지만 이렇게 하면 레드록, 레이스 컨디션 같은 이슈가 발생할 수 있다.
- 채널의 커뮤니케이션은?
- 메모리를 공유함으로써 ⇒ 커뮤니케이션 하는게 아닌
- 커뮤니케이션을 통해서 ⇒ 메모리를 공유한다.
https://proandroiddev.com/kotlin-coroutines-channels-csp-android-db441400965f
- 공유 상태를 가질때 동시성 코드 블록에서 문제가 될 수 있다.
- 스레드의 캐시, 메모리의 액세스의 원자성으로 인해 다른 스레드에서 수행한 수정 사항이 유실될 수 있다.
- 상태의 일관성을 해치는 원인이 된다.
- 그래서 스레드 동기화를 할 수 있는 두 가지 방법이 있다.
- 스레드 한정
- 공유 상태에 접근하는 모든 코루틴을 단일 스레드에서 실행되도록 한정하는 것
- 상태가 더 이상 스레드 간에 공유되지 않으며 하나의 스레드만 상태를 수정함
- 즉, 하나의 스레드만 상태와 상호 작용하도록 보장해서 쓰기가 아닌 읽기 전용으로만 공유할 수 있게 하는것
- 상호 배제
- 한 번에 하나의 코루틴만 코드 블록을 실행 할 수 있도록 하는 동기화 매커니즘
- 코드 블록을 원자적으로 만들기 위해서 잠금을 사용해 코드 블록을 실행하려는 모든 스레드의 동기화를 강제하는 것
- 스레드 한정
스레드 한정
코루틴자체를 단일 스레드로 한정
1
private val context = newSingleThreadContext("counterActor")
- 앱의 여러 다른 부분에서 공유 상태를 수정해야 하거나
- 원자 블록에 더 높은 유연성을 원하는 시나리오의 경우 이를 확장하는 방법이 필요하다.
- 그래서 필요한게 ⇒ 액터
액터
- 동작원리
- 상태에 대한 접근은 단일 스레드로 한정!
- 다른 스레드가 액터에게 message로 상태에 대한 변경 요청!
- 액터는 channel을 사용하기 때문에 message 요청은 순차적 실행을 보장
- ⇒ 동시성으로 인한 오류 방지
- 액터가 위 방식보다 좋은점?
- 상태 변경에 대한 요청을 좀 더 유연하게 할 수 있다.
- 외부에게 특정 message로 요청을 받아 처리할 수 있기 때문에 원자 블록(액터 내부 코드블럭)이 조금 더 유연해짐
- 상태 변경에 대한 요청을 좀 더 유연하게 할 수 있다.
- 액터의 내부적 동작 원리
- 액터란 스레드 혹은 객체와는 구별되는 추상적 존재
- 액터가 차지하는 메모리 공간은 어느 다른 스레드 혹은 액터도 접근할 수 없다.
- 즉, 액터 내부에서 일어나는 일은 어느 누구와도 공유되지 않는다.
- 액터는 서로간에 공유하는 자원이 절대 없고, 서로간 상태에 접근할 수 없다.
- 오직 Message만을 이용해서 액터에게 정보를 전달해 요청하는 방식
- 액터는 channel을 이용해 순차적 처리
- 액터 심화
- 액터는 코루틴의 복합체다
- 코루틴 + 코루틴 내부로 캡슐화 된 속성값 + 통신가능한 채널
- 속성값은 사용된 코루틴 안에서만 유효하고
- 다른 코루틴과는 채널로 통신하는 방식
- 액터는 코루틴의 복합체다
- 동작원리
상호 배제
- 상호 배제란 한 번에 하나의 코루틴만 코드 블록을 실행할 수 있도록 하는 동기화 매커니즘을 말한다.
- 즉, 모든 공유되는 상태의 변경들이 절대 동시 실행되지 않도록 함
- 코루틴 뮤텍스를 사용하면 됨
- 코틀린 뮤텍스의 가장 중요한 특징은 블록되지 않는다는 점이다.
- 동시에 실행되면 안되는 부분을 lock() / unlock() 으로 보호한다.
- 자바의 넌 블로킹 synchronized 정도로 생각할 수 있겠다.
- 다만 가장 큰 차이는 Mutex.lock() 은 일시중단 함수를 통해 컨트롤 되기 때문에 → 스레드를 블록하지 않는다는 점!
- 일시 중단 과 차단은 다르다
- 일시중단 - 하나의 스레드에서 여러가지 작업들을 처리한다.
- 차단 - 해당 스레드는 블록의 작업이 다 처리될 때 까지 다른 작업을 수행할 수 없다.
- 일시 중단 과 차단은 다르다
그 외 원자성 위반 문제를 해결할 수 있는 다른 방법들?
- 코루틴의 방법론으로 해결하는 건 아니지만 2가지의 방법이 있다
- 휘발성 변수 최신화
- 원자적 데이터 구조 사용
휘발성 변수
- 휘발성 변수는 구현하려는 스레드 안전(thread-safe) 카운터와 같은 문제를 해결하지 못한다.
- 일단 스레드 안전 문제가 발생하는 이유 2가지를 다시한번 살펴보자
- 다른 스레드가 읽거나 수정하는 동안 스레드의 읽기가 발생할 때
- 다른 스레드가 수정한 후 스레드의 읽기가 발생하지만, 스레드의 로컬 캐시가 업데이트되지 않았을 때
- 2번은 휘발성 변수 최신화로 해결이 가능하다.
- 1번은 … 휘발성 변수 최신화로 해결 불가
- 휘발성 변수 최신화 하는 방법
@Volatile 주석을 사용할 수 있다.
1 2
@Volatile var shutdownRequested = false
- @Volatile은 Kotlin/JVM에서만 사용할 수 있다.
- 휘발성(volatility)을 보장하는 기능을 JVM의 기능을 사용하기 때문에, 다른 플랫폼에서는 사용할 수 없다.
- 그렇다면 @Volatile를 사용할 수 있는 경우?
- 두 가지 시나리오 전제가 참이어야 한다.
- 변수 값의 변경은 현재 상태에 의존하지 않는다.
- 휘발성 변수는 다른 변수에 의존하지 않으며, 다른 변수도 휘발성 변수에 의존하지 않는다.
- 이렇게 되면 다른 스레드가 읽거나 수정하는 동안 다른 스레드의 읽기가 발생하는 것 과 관련없어지므로 괜찮다.
- ex) 변수값의 변경은 항상 a → b로만 변경하며 / 휘발성 변수의 값이 다른 변수의 값에 의존하지 않음
- 두 가지 시나리오 전제가 참이어야 한다.
원자적 데이터 구조
- JVM에서 제공하는 원자적 데이터 구조 사용
ex) AtomicInteger()…
1 2
val counter = AtomicInteger() counter.incrementAndGet()
결론
- 코루틴에서 동시성 코드 작성시 생기는 문제 중 → 원자성 위반의 문제를 해결하기 위해 도대체 어떤 방법을 사용해야할까?
- 상호배제를 사용해야하는 경우
- 어떠한 공유되는 상태를 어쩔 수 없이 주기적으로 변경해야하며 → 이를 위해 스레드 한정을 사용할 수 없을 때
- 스레드 한정을 사용해야하는 경우
- 다른 컨텍스트로 전환해야 하는 비용이 없기 때문에 부하 측면에서는 locking을 하는 경우보다 효율적이다.
Comments powered by Disqus.