-
Actor in Swift concurrencyiOS 2022. 12. 31. 07:39
Actor는 Swift concurrency의 핵심으로, async/await 및 structured concurrency와 함께 작동하여 정확하고 효율적인 동시성 프로그래밍 구축을 돕습니다.
데이터 경합(Data Race)은 두 개의 별도의 스레드가 동일한 데이터에 동시에 접근하고 이러한 접근 중 적어도 하나가 '쓰기' 작업일 때 발생합니다. 데이터 경합이 발생할 수 있다고 판단되면, actor를 사용해서 low-level에서 데이터 경합을 방지할 수 있습니다.
데이터 경합은 공유되는 가변 상태로 인해 발생합니다. struct는 동시에 접근할 수 있는 코드가 있을 때 컴파일하는 것을 허용하지 않으며, 값 타입이므로 따로 변수에 복사해서 할당한 후 작업을 수행하면 데이터 경합은 발생하지 않지만, 그것은 별개의 값이므로 공유의 의미가 없습니다. 따라서 변수의 값 증가에 더불어 추가적인 작업을 하는 참조 타입(이 글에서는 class, actor)을 예로 들어보겠습니다.
Class
class CounterClass { var value = 0 // 증가 후 반환 func increment() -> Int { value += 1 return value } }
class 에서 increment()만 중복으로 실행한다면?
// run let counter = CounterClass() Task.detached { print(counter.increment()) } Task.detached { print(counter.increment()) }
실행 결과
// 예상 가능 1 2 // 예상 가능 2 1 // 두 Task 모두 각각 0을 읽고(read) 1 증가(write) 1 1 // 두 Task 모두 증가 작업 후에 반환 2 2
실제로 위처럼 단순하게 구현하고 실행해봤을 때는 1, 2 또는 2, 1을 반환했습니다. 하지만 데이터 경합이 발생할 수 있으므로 실행 시점에 따라 어떻게 나올지 모릅니다. 이는 경쟁을 유발하는 데이터 접근이 곳곳에 있을 수 있으며 운영 체제의 스케줄러가 프로그램을 실행할 때마다 다른 방식으로 동시 작업을 수행할 수 있으므로 확실한 결과를 보장받지 못하기 때문입니다. 하지만 이를 방치한 상태로 규모를 키우면서 작업하면, 문제가 발생했을 때 디버깅에 어려움을 겪겠죠?
데이터 경합을 눈으로 확인하기 위해 추가적인 작업을 수행
단순하고 명확한 결과를 보기 위해 조금 불필요한 작업을 추가해봤습니다.
class CounterClass { var value = 0 // "wait"를 세 차례 출력하고 반환 func incrementWithWait() -> Int { value += 1 for _ in 0...2 { print("wait") } return value } // 증가 후 반환 func increment() -> Int { value += 1 return value } }
실행
// run let counter = CounterClass() Task.detached { print(counter.incrementWithWait()) } Task.detached { print(counter.increment()) }
실행 결과
// 여러가지 경우 중 하나 wait wait wait 1 2 // incrementWithWait()에서 증가시키고 출력하는 동안, increment()에서 1 증가시키고 반환 // 이후, 출력을 마치고 반환 2 wait wait wait 2 ...
실행 결과는 위와 같이 다양하게 나옵니다. 결과를 예상할 수 없습니다. 상황에 따라 문제가 발생할 수도, 아닐 수도 있습니다. 이를 actor를 사용하여 데이터 경합을 제거함으로써 결과를 보장받을 수 있습니다.
Actor
// class -> actor actor CounterActor { var value = 0 func increment() -> Int { value += 1 return value } func incrementWithWait() -> Int { value += 1 for _ in 0...2 { print("wait") } return value } } let counter = CounterActor() Task.detached { // 일시 중단할 수 있음을 알림. // 다른 작업을 먼저 수행하게 된다면, actor가 자유로워질 때까지 기다려야 함. print(await counter.incrementWithWait()) } Task.detached { // 일시 중단할 수 있음을 알림. print(await counter.increment()) }
실행 결과
// actor의 동기 코드는 항상 중단없이 완료될 때까지 실행됨. // incrementWithWait()를 수행하는 Task 이후, increment()를 수행하는 Task가 발생한 경우. wait wait wait 1 2 // 상황에 따라서 increment()가 먼저 실행되면서 이렇게도 출력될 수 있을 것 같습니다. 1 wait wait wait 2
1, 1 또는 2, 2 의 결과는 나올 수 없습니다. 어떻게 결과를 보장받을 수 있는 걸까요? actor는 자체의 상태를 가지며 해당 상태는 프로그램의 나머지 부분과 격리(isolate)됩니다. serial dispatch queue를 사용함으로써 얻는 것과 동일한 상호 배제를 제공합니다. 이를 위한 메커니즘이 있습니다.
- 외부에서 actor와 상호작용할 때, 다른 코드가 actor의 상태에 동시에 접근하지 않도록 보장.
- actor가 다시 자유로워지면 기다리고 있던 실행(접근)을 재개.
- await 키워드는 액터에 대한 비동기 호출이 위와 같은 일시 중단을 포함할 수 있음을 나타내는 것.
Actor isolation
데이터 경합은 동시에 동시에 접근하고, 접근 중 적어도 하나의 작업이 쓰기일 때 발생한다고 했죠? 그렇다면, 해당하지 않는 작업을 하는 동안에는 actor로의 접근을 막을 필요가 없습니다. 그렇게 한다면 처리만 늦어질 테니까요. 이럴 때, nonisolated 키워드를 사용해서 Actor-isolation에서 분리된 작업으로 처리해야 합니다. actor에 격리되지 않았다는 의미로 해석할 수 있을 것 같습니다. 다음과 같이 또다른 작업을 추가해보겠습니다.
actor CounterActor { var value = 0 func increment() -> Int { // 공유 가변 데이터에 접근해서 증가시킴. value += 1 // 이후에는 접근할 필요가 없다면.. let convetedValue = doSomething(value) return convertedValue } // nonisolated nonisolated func convert(_ value: Int) -> String { // 증가시킨 value를 가지고 가공을 하는 등 작업을 수행 ... } }
nonisolated는 구문적으로 actor에 설명되어 있지만 actor 외부에 있는 것으로 취급됨을 의미합니다. 예를 들어, nonisolated 함수는 변경할 수 없는 let 프로퍼티를 참조하여 처리하는 작업이 가능하며, 그런 작업을 하고 있을 때는 nonisolated 함수 측면에서는 다시 actor에 의해 보호받는 상태에 접근하기까지 스레드 풀의 모든 스레드에서 자유롭게 실행될 수 있고, 대기 중인 다른 코드 측면에서는 actor에 접근하여 동시에 작업을 진행할 수 있습니다.
nonisolated 함수 내부에서 공유 가변 상태에 대한 접근이 필요하다면, 다음과 같이 함수를 비동기 함수로 만든 다음, 호출(접근)에 대해서 await 키워드를 표시하여 일시 중단할 수 있음을 알리면 됩니다.
actor CounterActor { var value = 0 func increment() -> Int { value += 1 let convetedValue = doSomething(value) return convertedValue } // Do something with value func doSometing() { ... } nonisolated func convert(_ value: Int) async -> String { // 증가시킨 value를 가지고 가공을 하는 등 작업을 수행 ... // 도중에 value(공유 가변 상태)에 접근해야 한다면(이 예는 조금 이상하지만) await doSomething() ... } }
참고
WWDC 2021 Protect mutable state with actors
(https://developer.apple.com/videos/play/wwdc2021/10133/)
WWDC 2022 Visualize and optimize Swift concurrency
'iOS' 카테고리의 다른 글
UIButton 커스텀(Image & Title) (0) 2023.02.01 UIKit에서 SwiftUI View 활용하기 (0) 2023.01.02 Diffable DataSource (2) 2022.12.29 Localization in iOS (0) 2022.12.14 Operator, Combine Framework in Swift (0) 2022.12.11