코루틴 공식 가이드 자세히 읽기 — Part 8

Myungpyo Shim
7 min readJan 25, 2019

공식 가이드 읽기 (8 / 9)

Shared mutable state and concurrency

Korean [English]

(목차로 돌아가기)

코루틴은 Dispatcher.Default 같은 멀티 스레드 디스패쳐를 통해서 동시에 수행 되도록 만들 수 있습니다. 그래서 이러한 상황에서 발생 할 수 있는 모든 일반적인 동시성 문제들 또한 가지고 있습니다. 주로 발생하는 문제는 변경 가능한 어떤 상태 객체에 여러 스레드에서 접근할 경우에 대한 접근 동기화 입니다. 이 문제에 대한 코루틴 프레임워크의 몇 가지 해결책들은 멀티 스레드 세상에서의 해결책들과 비슷하지만 몇몇 해결책들은 코루틴 월드 고유의 것들도 존재합니다.

문제점 (The problem)

우선 100개의 코루틴을 생성하고 각각의 코루틴은 동일한 동작을 1000번 수행하도록 하는 다음과 같은 함수를 만들어 봅시다. 또한 비교를 위해서 이 코루틴들의 완료 시간도 측정해 볼 것입니다. 우선 간단히 메인함수에서 GlobalScope 의 Dispatchers.Default 를 이용하여 코루틴 간에 공유되는 변수를 증가시키는 동작을 해보겠습니다.

위 예제를 실행해 봅시다. 무엇이 출력 되었나요? “Counter = 100000” 이 출력될 확률은 매우 낮습니다. 왜냐하면 천 개의 코루틴이 어떠한 동기화도 없이 다중 스레드에서 공유 중인 counter 변수를 증가시키고 있기 때문입니다.

만약 여러분이 2개 혹은 더 적은 CPU 를 갖는 오래된 시스템을 가지고 있다면 계속해서 100000 의 결과를 볼 수도 있습니다. 왜냐하면 이 경우 스레드 풀이 하나의 스레드로 운용될 것이기 때문입니다.

Volatile 은 도움이 되지 않는다 (Volatiles are of no help)

간혹 단순히 변수를 volatile 로 만드는 것만으로 이러한 동시성 문제를 해결할 수 있다는 오해가 있습니다. 이를 확인 해 보기 위해서 앞서 살펴본 예제에서 var counter = 0 변수에 @Volatile 어노테이션을 붙여 다시 시도해 봅시다.

이 코드는 더 느리게 동작 함에도 여전히 “Counter = 100000” 이라는 기대하는 결과를 받을 수 없습니다. 왜냐하면 volatile 변수는 선형적인(linearizable — atomic 의 기술 용어) 읽기 와 쓰기를 보장하지만 방대하게 발생하는 액션에 대한 원자성을 제공하진 않기 때문입니다. (우리의 경우 값을 증가 시키는 것)

스레드에 안전한 데이터 구조 (Thread-safe data structures)

스레드나 코루틴 모두에 적용 가능한 일반적인 해결책은 공유되는 상태 객체에 적용되는 모든 액션들에 대해서 동기화 기능이 제공되는 스레드에 안전한 데이터 구조(synchronized, linearizable, atomic 같은)를 사용하는 것입니다. 이번 예제 같이 단순한 카운터 동작을 위해서는 동기화를 지원하는AtomicInteger 클래스의 incrementAndGet() 함수를 사용할 수 있습니다.

이러한 방식이 지금과 같은 단순한 케이스를 위한 가장 빠른 해결책이며 동시에 단순한 카운터나, 컬렉션, 큐 그리고 다른 표준 데이터 구조나 그것들에 대한 기본 연산에도 모두 적용 가능한 해결책입니다.

하지만 이 해결책은 이용 가능한 스레드에 안전한 구현이 없는 복잡한 상태 혹은 연산으로의 확장이 쉽지 않습니다.

작은 단위 스레드 한정 (Thread confinement fine-grained)

스레드 한정은 공유되는 상태에 대한 접근을 단일 스레드로 제한하는 해결 방식입니다. 이런 방식은 보통 UI 애플리케이션에서 전형적으로 사용되는 방식인데 모든 UI 상태는 단일한 event-dispatch/application 스레드로 제한됩니다. 코루틴에서는 단일 스레드 컨텍스트를 사용하는 코루틴을 작성함으로써 손쉽게 이러한 구현을 할 수 있습니다.

이 코드는 매우 느리게 동작하는데, 그 이유는 아주 작은 범위의 코드 블록을 세밀하게 스레드 한정하고 있기 때문입니다. 각각의 증가 연산은 withContext { } 블록을 이용하여 멀티 스레드(Dispatchers.Default) 컨텍스트에서 싱글 스레드 컨텍스트로 전환이 발생하며 이루어집니다.

넓은 단위 스레드 한정 (Thread confinement coarse-grained)

실제 상황에서 스레드 한정은 더 큰 코드 블록 단위로 이루어 집니다. 예를들어 상태를 업데이트 하기 위한 보다 큰 단위의 비지니스 로직들이 단일 스레드로 한정됩니다. 다음 예제는 이런 방식으로 동작하며 각각의 코루틴을 시작하기 위해서 단일 스레드 컨텍스트를 사용합니다. 여기서 우리는 CoroutineScope() 함수를 이용하는데 이는 코루틴 컨텍스트 참조를 코루틴 스코프로 변환하기 위해서 입니다.

위 예제는 앞서 세밀한 한정을 한 경우에 비해서 빠르게 동작합니다.

상호 배제 (Mutual exclusion)

앞서 살펴본 동기화 문제를 상호 배제를 통해 해결할 수 있으며, 이것은 모든 공유되는 상태의 변경들이 절대 동시 실행되지 않도록 크리티컬 섹션으로 보호하는 것입니다. 블록킹 세계에서 여러분은 이러한 구현을 하기 위해서 synchronized 블록이나 ReentrantLock 을 사용했을 것입니다. 코루틴에서는 뮤텍스(Mutex)가 그것을 대체할 수 있으며 여기엔 크리티컬 섹션을 정의하기 위한 lock() 과 unlock() 함수가 존재합니다. 일반 락킹 매커니즘과의 주요한 차이점은 Mutex.lock() 은 서스펜드 함수를 통해 컨트롤 되기 때문에 스레드를 블록하지 않는다는 점입니다.

또한 Mutex.withLock { } 확장함수를 이용하면 다음과 같은 일반 적인 Mutex 사용 패턴을

mutex.lock()
try {
....
} finally {
mutex.unlock()
}

아래 예제와 같이 간략하게 작성 가능합니다.

이러한 Lock 을 사용하는 예제는 작은 단위(fine-grained)의 스레드 한정이므로 성능면에서 손실이 있습니다. 하지만 특정 상황에서는 이 선택이 오히려 괜찮은 선택인 경우도 있습니다. 일례로 어떠한 공유되는 상태를 어쩔 수 없이 주기적으로 변경해야 하고 이를 위한 스레드 한정은 적용하기 어려울 경우 를 들 수 있습니다.

액터 (Actors)

액터는 코루틴과 이 코루틴 내부로 캡슐화된 상태 값, 그리고 다른 코루틴과 통신할 수 있는 채널의 조합으로 구성되어 있습니다.
간단한 액터는 함수로 작성될 수 있지만 복잡한 상태를 갖는 경우에는 클래스로 작성하는 것이 더 적합합니다.

actor { } 코루틴 빌더는 액터의 메일 박스 채널을 스코프로 결합하는 것을 용이하게 해줌으로써 메시지를 수신할 수 있도록 해주고, 송신 채널을 결과 작업 객체로 결합함으로써 액터에 대한 단일 참조를 핸들 타입으로 전달할 수 있습니다.

액터를 사용하는 첫 단계는 액터가 처리할 메시지들의 클래스를 정의하는 것이며 코틀린의 Sealed 클래스가 이러한 목적에 적합한 클래스입니다. 우리는 CounterMsg Sealed 클래스와 함께 카운터를 증가시키기 위한 IncCounter 메시지와 그 값을 얻기 위한 GetCounter 메시지를 정의합니다. 그리고 나서는 응답을 보내야 합니다. 여기서는 이러한 목적으로 미래에 알 수 있는 단일 값을 나타내는 CompletableDeferred communication primitive 를 사용합니다.

액터가 실행되는 컨텍스트가 무엇인지는 중요하지 않습니다. 액터는 코루틴이며 코루틴은 순차적으로 실행됩니다. 그러므로 상태를 특정 코루틴으로 한정하는 것 만으로 공유된 변경 가능한 상태에서 발생하는 문제에 대한 해결책이 됩니다. 사실, Actor 는 그 자신의 내부 상태를 변경할 수는 있지만 서로 간에는 메시지를 이용해서만 영향을 줄 수 있습니다. (Lock 의 사용 없이)

액터는 부하 상태에서 Locking 보다 효율적인데 그 이유는 이 경우에 액터는 항상 할일이 있으며 다른 컨텍스트로 전환할 이유가 전혀 없기 때문이다.

actor 코루틴 빌더는 produce 코루틴빌더의 형제 입니다. 액터는 메시지를 수신할 채널과 관련이 있는 반면 프로듀서는 데이터를 전송할 채널과 관련이 있습니다.

--

--