참조 타입의 문제점은 여러 쓰레드에서 Heap 데이터에 동시에 접근할 수 있기 때문에 Race Condition문제에 취약할 수 밖에 없었습니다.
동시성 문제가 발생하지 않도록 NSLock, 직렬큐, Semaphore와 같이 Swift에서 제공해주는 여러 도구를 사용할 수 있지만, 여전히 단점은 존재하고 실수하기 쉽습니다.
이런 문제를 해결하기 위해서 나온 것이 Actor 입니다. Actor 는 Serial Executor를 통해서 쓰레드로부터 오는 요청에 대해서 직렬로 처리하여 동시 접근을 막는 참조 타입입니다.
Actor는 Serial Executor를 통해서 작업이 처리되는 구조로, 작업이 종료될 때까지 기다려야 하기 때문에 무조건 비동기 컨텍스트에서 접근해야만 하는 특징이 있습니다.
그러면 Serial Executor는 어떤 순서로 동작할까요?
FIFO 구조가 아닌 우선순위에 따라서 동작합니다. 작업의 우선순위를 보면 GCD에 존재하는 QoS와 비슷하게 존재하는 걸 확인할 수 있습니다.
TaskPriority.userInitiated // 우선 순위 높음
TaskPriority.high
TaskPriority.medium
TaskPriority.low
TaskPriority.utility
TaskPriority.background // 우선 순위 낮음
Actor는 어떻게 우선순위를 고려해서 작업을 할 수 있을까요?
WWDC 2021 - Swift concurrency: Behind the scenes 에서는 다음과 같이 설명하고 있습니다.
Since actors are designed for reentrancy, the runtime may choose to move the higher-priority item to the front of the queue, ahead of the lower-priority items.
Actor가 재진입성(reentrancy)을 갖기 때문에, 런타임이 높은 우선순위 작업을 낮은 우선순위 작업들 앞에 배치할 수 있습니다.
WWDC에서는 아래와 같이 설명하고 있습니다.
Serial Executor에는 다음과 같이 작업이 배정되었습니다.
우선순위가 높은 작업: A, B
우선순위가 낮은 작업: 1 ~ 7

가장 앞에 있는 A 작업이 가장 먼저 실행되고 종료되었습니다.

여기서 작업 순서가 변경될 수 있는 2가지 시나리오가 있습니다.
- 이제 다시 작업을 진행해야 하는데, 런타임 스케쥴러는
1번 작업보다 우선순위가 더 높은B작업을 먼저 실행합니다.
1번 작업을 진행하는데,1번 작업에 있는 await 메소드로 인해서 suspension point가 발생했습니다. 이제 런타임 스케쥴러는 우선순위가 높은 작업인B를 맨 앞으로 가져옵니다.
액터의 재진입성(reentrancy) 특징에 좀 더 집중해보기 위해서 2번째 경우로 생각해보겠습니다.

1 번 작업을 뒤로 보내고 작업 우선순위가 높은 B 작업을 먼저 진행하는 것을 볼 수 있습니다.
위의 예시같이 suspension point가 발생했을 때 런타임은 작업의 우선 순위를 판단하여 작업 순서를 변경할 수 있습니다.
이 suspention point는 재진입성(reentrancy)과 큰 연관이 있습니다.
Actor 내부에서 작업이 await 메소드로 중단된 사이에 큐에 대기하던 다른 작업이 들어와 먼저 실행될 수 있게 허용하는 것.
이것이 Actor의 재진입성 특성입니다. 이 특성을 통해서 직렬큐임에도 불구하고 작업 우선순위에 따라서 작업의 순서는 변경될 수 있습니다.
아래는 참고할 예시 코드입니다.
actor MyActor {
func task1() async throws {
print("task1 start")
try await Task.sleep(for: .seconds(0.5)) // (suspension point)
print("task1 end")
}
func task2() {
print("task2 run")
}
}
let actor = MyActor()
Task(priority: .background) {
try await actor.task1()
}
Task(priority: .userInitiated) {
try? await Task.sleep(for: .seconds(0.1))
await actor.task2()
}

GCD의 경우도 작업의 우선순위가 존재하지만, 사실상 Concurrent 상황일 때만 적용됩니다. GCD에는 재진입성(reentrancy)라는 개념이 없기 때문에 FIFO 로만 동작한다는 단점이 있었습니다.
그로 인해서 작업순위가 높더라도 작업순위가 낮은 작업을 기다려야 하는 우선순위 역전 현상이 발생합니다.
Actor는 직렬큐에서도 새로운 개념인 재진입성(reentrancy)로 해당 문제를 해결했다는 것을 기억하고 있으면 좋을 거 같습니다.
참고자료
https://developer.apple.com/videos/play/wwdc2021/10254/?utm_source=chatgpt.com&time=2074
https://developer.apple.com/documentation/swift/taskpriority