Shared mutable state and concurrency
코루틴은 멀티스레드 디스패처에 의하여 동시에 실행되는 것 처럼 Dispatchers.Default 에 의해 비동기적으로 실행 될 수 있습니다. 그것은 동시성으로 인해 발생될 수 있는 모든 일반적인 문제점을 가지고 있습니다. 주요 이슈는 변경 가능한 공유 자원에 대해 접근함으로써 발생되는 동시성 문제입니다. 코루틴 관점에서 이 문제에 대한 몇 가지 해결방법은 멀티스레드 관점에서의 해결책들과 비슷하지만, 몇 가지 해결 방법은 코루틴 고유의 방식으로 해결이 가능합니다.
The problem
먼저 동일한 동작을 천 번씩 실행하는 코루틴을 100번 실행시켜 봅시다. 또한, 추후 비교를 위하여 완료 시간을 측정해 봅시다.
Dispatchers.Default 를 이용하여 멀티 스레드 환경에서 코루틴간 공유되는 변수를 증가시키는 동작을 시작합니다.
최종적으로 어떤 값을 출력하나요? 100개의 코루틴이 서로 동기화 없이 여러개의 스레드에서 동시에 카운터를 증가시키기 때문에, "Counter = 100000" 이 출력될 가능성은 거의 없습니다.
Volatiles are of no help
단순히 변수에 Volatile 을 지정하는 것 만으로도 동시성 문제를 해결할 수 있다는 오해가 있습니다. 앞 예제의 counter 변수에서 Volatile 을 적용하고 다시 실행해봅시다.
이 예제는 속도는 더 느려지며, 여전히 종료 후 "Counter = 100000" 을 출력하지 않습니다. 왜냐면 volatile 변수는 선형적인('원자성'의 기술적 용어) 읽기 및 쓰기를 보장하지만, 더 큰 액션에 대한 원자성을 제공하지는 않기 때문입니다. (이 예제에서는 값을 증가시키는 것)
Thread-safe data structures
스레드와 코루틴 모두에서 모든 동기화 처리에 있어, 일반적인 해결책은 공유 상태에서 수행될 수 있는 연산에 대하여 스레드 안전한 데이터 구조(synchronized, linearizable, 혹은 atomic) 를 사용하는 것입니다.
counter 예제에서는 AtomicInteger 클래스의 incrementAndGet() 함수를 사용할 수 있습니다.
예제 코드는 앞에서 소개한 특별한 문제를 해결하는 가장 빠른 해결책입니다.
단순 카운터나, 컬랙션, 큐 그리고 다른 표준 데이터 구조나 다른 기본 연산자들에도 적용 가능한 해결방법 입니다. 하지만, 이 해결방법은 복잡한 상태로 확장시키거나, 스레드 안전하게 오퍼레이션을 확장하는 것이 쉽지 않습니다.
Thread confinement fine-grained
스레드 한정(Thread confinemnet) 은 변경 가능한 공유 상태에 대한 모든 접근을 단일 스레드에서만 처리하도록 제한하여 공유 상태 문제에 대한 해결 접근 방식입니다. UI 상태가 단일 이벤트 디스패치 / 어플리케이션 스레드로 제한되는 UI 응용 프로그램에서 보통 사용됩니다. 코루틴에서는 단일 스레드 컨텍스트를 사용하는 방법으로 쉽게 적용할 수 있습니다.
이 예제코드는 스레드 한정 코드블록을 세밀하게 분류하기 때문에 매우 느리게 동작합니다. 각각의 증가 처리는 다중 스레드 컨텍스트 (Dispatchers.DEFAULT) 에서 싱글 스레드 스레드 컨텍스트로 전환이 발생하며 이루어집니다.
Thread confinement coarse-grained
실제로는 스레드 한정은 매우 큰 작업 단위로 이루어닙니다.
예를들어, 큰 작업의 비즈니스 로직으로 상태를 업데이트하는 수행하는 것은 단일 스레드에 국한됩니다. 다음 예제는 단일 스레드 컨텍스트에서 각 코루틴을 실행합니다.
이제 훨씬 더 빨리 동작되며, 정확한 결과가 출력됩니다.
Mutual exclusion
상호 배제 방법은 공유 상태의 모든 수정을 임계영역(=critical section)으로 절때 비동기적으로 실행되지 않도록 보호하는 해결책입니다. 블로킹 환경에서는 일반적으로 synchronized 또는 ReentrantLock 을 사용합니다. 코루틴에서는 이러한 해결책의 대안으로 Mutex 가 있습니다. lock / unlock 함수로
임계영역(=critical section) 범위를 정할 수 있습니다. 중요한 차이점은, Mutex.lock() 은 중단함수로, 스레드를 블로킹 하지 않습니다.
또한 Mutex 는 withLock 확장 함수를 제공하여 아래 패턴을 편리하게 작성 가능합니다.
mutex.lock()
try {
...
} finally {
mutex.unlock()
}
이 예제에서는 상호배재를 세밀하게 제어하고있어 그만큼 대가가 필요합니다. 그러나 몇몇 공유 상태를 주기적으로 변경해야 하고, 이를 위한 스레드 한정을 적용하기 어려운 경우에는 좋은 해결책이 될 수 있습니다.
Actors
actor 는 코루틴과 이 코루틴에 제한되고 캡슐화된 상태, 다른 코루틴과 통신하기위한 채널의 조합으로 구성되어 있습니다. 단순한 actor 의 경우 함수로 작성할 수 있지만, 좀 더 복잡한 상태의 actor 라면 클래스로 작성하는 것이 더 적절합니다.
actor 의 코루틴 빌더는 actor 의 메일박스 채널을 해당 스코프에서 메시지를 수신하고, 송신 채널을 결과 작업(Job) 객체로 결합하여 actor 에 단일 참조 핸들 타입으로 전달할 수 있습니다.
첫번째 단계는 actor 가 처리할 메시지의 클래스를 정의하는 것입니다.
코틀린의 sealed class 는 이 목적에 가장 적절합니다. CounterMsg 라는 sealed class 로 카운터를 증가시키기 위하여 IncCounter 클래스와, 카운터 값을 가져오기 위하여 GetCounter 클래스를 정의합니다. 그리고 나서 응답을 보내야 합니다. CompletableDeferred 는 단일값을 나타내며 추후 알려지기(=통신되기 위한) 목적으로 사용됩니다. 그런 다음, 우리는 actor 코루틴 빌더를 사용하여 actor 를 시작하는 함수를 정의합니다.
예제코드에서 actor 는 어떤 context 에서 실행되는지 중요하지 않습니다.
actor 는 코루틴이며 순차적으로 실행되기 때문에, 특정 코루틴에서 변경 가능한 공유 상태에 대한 문제 해결책으로 실행됩니다. 실제로 actor 는 자신의 상태를 수정할 수 있지만, 오직 메시지를 통해서만 영향을 받습니다. (lock 처리가 필요한 것을 회피할 수 있습니다.)
이 경우 항상 해야 할 일이 있고 다른 context 로 전환할 필요가 전혀 없기 때문에 Actor 는 부하가 걸린 상태에서 locking 하는 것 보다 효율적입니다.
actor 코루틴빌더는 produce 코루틴빌더의 이중체입니다.
actor 는 메시지를 수신하는 채널과 관련있는 반면,
producer 는 요소를 송신하는 채널과 관련이 있습니다.