로컬 비동기 계산을 제출 순서대로 완료해야 하는 Mac 앱에서는 동기 메인 액터 제출 메서드를 사용해 각 작업을 이전 작업 뒤에 연결하세요. 액터 격리만으로는 이러한 순서가 보장되지 않습니다.
이 안내서에서는 합성 정수 계산을 위한 작은 대기열을 만듭니다. 제출이 이루어지는 시점을 정확히 정의하고, 중단 지점이 있어도 순서를 유지하며, 각 계산 결과를 반환하고, 앞선 작업이 실패해도 이후 작업을 계속 실행합니다.
엄격한 제출 순서는 무엇을 의미하나요?
데스크톱 계산기가 작업 A, B, C를 제출한다고 가정해 보세요. 요구되는 계약은 A의 계산이 끝난 뒤 B의 계산이 시작되고, B가 끝난 뒤 C가 시작되는 것입니다. 각각 별도로 시작한 세 작업을 알파벳순으로 기다리면 결과를 관찰하는 순서만 정해집니다.
제출 경계가 중요합니다. 이 구현에서는 메인 액터 격리 상태의 submit 메서드가 실행되어 체인에 다음 연결을 설정할 때 제출이 이루어집니다. 하나의 메인 액터 컨텍스트에서 연속으로 호출하면 그 사이에 중단되지 않으므로 A, B, C 순서가 정해집니다.
독립 작업들이 동시에 제출을 요청하면 도착 순서는 지정되지 않습니다. 대기열은 제출 메서드가 요청을 받아들이는 순서를 보존합니다. 애플리케이션에서 이러한 생산자 간에 특정 순서가 필요하다면 제출 전에 순서를 정하세요. 예를 들어 유한한 배치를 명시적으로 정렬된 배열에 모을 수 있습니다.
이 설계에서는 짧은 장부 기록 단계에 메인 액터를 사용합니다. Apple은 MainActor를 실행자가 메인 디스패치 큐와 동일한 싱글턴 액터로 정의합니다. 아래의 실제 계산은 별도의 작업자 액터에서 수행합니다. MainActor 문서
액터나 직렬 작업 대기열만으로는 왜 충분하지 않나요?
액터는 액터 격리 작업을 한 번에 하나씩 실행하지만, 비동기 메서드는 중단되어 다른 호출이 진행될 수 있습니다. Swift의 액터 설계는 액터를 기다리는 작업이 원래 기다리기 시작한 순서대로 실행된다는 보장도 명시적으로 하지 않습니다. 따라서 비동기 계산 전체를 액터 안에 넣는 것만으로는 앞선 계산이 끝난 뒤 다음 계산을 시작하는 동작이 보장되지 않습니다. SE-0306: 액터
별도의 Task 인스턴스를 만들더라도 서로 간의 의존 관계는 생기지 않습니다. 작업은 생성 후 실행을 시작할 수 있습니다. 핸들을 보관하면 기다리거나 취소할 수 있지만, 실행 순서가 정해지는 것은 아닙니다. Task 문서
Foundation은 다른 순서 제어 수단을 제공합니다. OperationQueue는 준비 상태, 우선순위, 의존 관계에 따라 작업을 예약합니다. 특정 실행 순서가 중요하다면 의존 관계를 사용하라고 Apple은 안내합니다. maxConcurrentOperationCount을 1으로 설정하면 동시 실행 수는 제한되지만, 순서를 보장하려면 여전히 명시적인 의존 관계가 필요합니다. OperationQueue 문서
애플리케이션에 이미 필요한 기능에 따라 수단을 선택하세요.
| 상황 | 적합한 방법 | 순서를 정하는 요소 |
|---|---|---|
| 하나의 비동기 함수에서 처리하는 정해진 배치 | 각 계산을 기다린 뒤 진행하는 루프 | 순차적인 제어 흐름 |
| 데스크톱 동작으로 시간이 지나며 들어오는 작업 | 작업 체인이 있는 동기 제출 메서드 | 각 계산이 이전 작업의 완료 게이트를 기다림 |
| 기존 Foundation 작업 | 대기열에 넣기 전에 설정한 의존 관계 | 각 작업이 이전 작업의 완료 상태에 의존함 |
이 경우 작업 체인이 유용한 이유는 계산이 나중에 실행되는 동안 제출 메서드가 즉시 핸들을 반환하기 때문입니다. 새 작업이 들어오지 않는 고정된 배치라면 순차 루프가 보통 코드가 더 간단합니다.
비동기 계산 전체가 끝날 때까지 기다리는 대기열 만들기
대기열은 tail이라는 비공개 비던지기 완료 작업을 저장합니다. 제출할 때마다 현재 꼬리를 캡처하고, 이를 기다린 뒤 계산 작업을 만들며, 계산 완료를 관찰하는 새 작업으로 꼬리를 바꿉니다.
완료 작업은 result을 기다립니다. 이는 작업 완료 후 성공 또는 실패를 나타냅니다. 따라서 계산이 실패해도 다음 작업은 진행할 수 있으며, 계산 자체의 핸들에는 오류가 보존됩니다. 호출자는 핸들을 통해 여전히 그 오류를 가져올 수 있습니다. Task 결과 문서
다음은 Swift 6 툴체인이 설치된 Mac에서 사용할 수 있는 완전한 명령줄 예제입니다. OrderedDemo.swift로 저장하세요. 소스 코드를 살펴보았지만, 이 조사 환경에는 Swift 컴파일러가 없어 컴파일할 수 없었습니다.
import Foundation
@MainActor
final class OrderedIntQueue {
private var tail: Task<Void, Never>?
func submit(
_ operation: @escaping @Sendable () async throws -> Int
) -> Task<Int, Error> {
let predecessor = tail
let job = Task<Int, Error> {
if let predecessor {
await predecessor.value
}
try Task.checkCancellation()
return try await operation()
}
tail = Task<Void, Never> {
_ = await job.result
}
return job
}
}
enum CalculationError: Error {
case inputOutOfRange
}
actor CalculationWorker {
private var events: [String] = []
func square(_ input: Int, label: String) async throws -> Int {
events.append("start \(label)")
guard (-1000...1000).contains(input) else {
throw CalculationError.inputOutOfRange
}
await Task.yield()
try Task.checkCancellation()
let answer = input * input
events.append("finish \(label)")
return answer
}
func trace() -> [String] {
events
}
}
@main
struct OrderedDemo {
@MainActor
static func main() async {
let queue = OrderedIntQueue()
let worker = CalculationWorker()
let a = queue.submit {
try await worker.square(3, label: "A")
}
let b = queue.submit {
try await worker.square(4, label: "B")
}
let c = queue.submit {
try await worker.square(5, label: "C")
}
var answers: [Int] = []
for job in [a, b, c] {
switch await job.result {
case .success(let answer):
answers.append(answer)
case .failure(let error):
print("Calculation failed: \(error)")
}
}
let events = await worker.trace()
precondition(answers == [9, 16, 25])
precondition(events == [
"start A", "finish A",
"start B", "finish B",
"start C", "finish C"
])
print(events.joined(separator: "\n"))
print("Answers: \(answers)")
}
}
저장한 프로그램이 있는 디렉터리에서 터미널을 열고 다음 명령으로 컴파일하고 실행하세요.
swiftc -swift-version 6 -parse-as-library OrderedDemo.swift -o OrderedDemo
./OrderedDemo
예상 출력은 예제의 입력값과 의존성 체인으로부터 다음과 같이 도출됩니다.
start A
finish A
start B
finish B
start C
finish C
Answers: [9, 16, 25]
어설션은 반환된 값과 계산 경계를 모두 확인합니다. 제출 순서대로 핸들을 기다리면서 결과를 출력하기만 해서는 계산이 그 순서대로 실행됐음을 보장할 수 없습니다.
작업자는 계산이 중단될 기회를 포함하도록 의도적으로 Task.yield()을 호출합니다. 양보하면 다른 작업이 실행될 수 있지만, 실행자가 같은 작업을 즉시 재개할 수도 있습니다. 따라서 대기열의 순서는 작업 양보 시 작업이 전환될 것이라는 가정이 아니라 의존성에서 비롯됩니다. Task 양보 문서
중단 지점이 있어도 체인이 순서를 보존하는 이유는 무엇인가요?
세 가지 요소가 이 보장을 만듭니다. 첫째, submit에는 중단 지점이 없으므로, 현재 tail을 캡처하고 바꾸는 동안 다른 메인 액터 제출이 끼어들 수 없습니다. Swift 문서에 따르면 중단 지점이 없는 액터 격리 코드는 다른 액터 작업이 그 순서를 끊지 않은 채 실행됩니다. 동시성: 액터
둘째, 각 작업은 계산 클로저를 호출하기 전에 캡처한 이전 작업을 기다립니다. 비던지기 작업의 value에 접근하면 해당 작업이 완료될 때까지 기다립니다. 뒤의 작업은 기다리는 장부 기록을 일찍 실행할 수 있지만, 계산을 일찍 시작할 수는 없습니다. 비던지기 Task의 value 문서
셋째, 이전 작업의 게이트는 앞선 작업이 결과나 오류를 낸 뒤에만 완료됩니다. 연결을 거슬러 따라가면 접수된 모든 제출의 순서가 보장됩니다. C는 B의 완료 게이트를 기다리고, 그 게이트는 B를 기다리며, B는 A의 완료 게이트를 기다립니다.
이 순서 보장은 제출된 각 클로저 안에서 기다리는 작업에 적용됩니다. 클로저가 구조화되지 않은 다른 작업을 시작하고 기다리지 않은 채 반환하면, 그 추가 작업은 클로저가 끝난 뒤에도 계속될 수 있습니다. 반환하기 전에 필요한 모든 계산 단계를 기다리는 작업에 포함하세요.
데스크톱 앱에서는 관련 동작을 처리하는 메인 액터 컨트롤러나 모델에 대기열 인스턴스 하나를 두세요. 해당 컨텍스트에서 submit을 직접 호출한 다음, 반환된 핸들로 완료를 확인하세요. 먼저 독립 작업을 만들고 그 작업이 시작된 뒤에만 제출하면 제출 경계가 해당 작업의 실제 실행 시점으로 옮겨집니다.
오류와 취소는 어떻게 처리해야 하나요?
이 구현은 실패 후 계속 진행하는 정책을 따릅니다. 실패한 작업은 오류를 보존하고 완료 게이트를 마치며, 접수된 다음 작업은 계산을 시작할 수 있게 됩니다. 예제에서는 첫 오류가 발생한 뒤 나머지 결과를 포기하지 않고 각 핸들의 result을 확인합니다.
간단한 실패 확인을 하려면 B의 입력값을 4에서 1001으로 바꾸세요. 작업자는 곱셈 전에 해당 값을 거부합니다. 예상 답을 [9, 25]로 바꾸고, 예상 추적을 start A, finish A, start B, start C, finish C로 바꾸세요. 프로그램은 실패를 출력하면서도 C를 계산해야 합니다. 입력 범위는 산술 연산을 단순하게 유지하기 위해 정한 예시용 계약입니다.
취소는 협력적으로 이루어집니다. 대기열은 이전 작업을 기다린 뒤 취소 여부를 확인하고, 작업자는 양보한 뒤 다시 확인합니다. 취소가 요청되면 Task.checkCancellation()이 CancellationError를 던집니다. Task checkCancellation 문서
실행 전에 취소를 확인하려면 C를 제출한 직후, 결과 루프에 들어가기 전에 b.cancel()을 호출하세요. 중단 없이 이어지는 메인 액터 제출에서는 B가 아직 계산을 시작할 수 없습니다. 예상 답을 [9, 25]로 바꾸고, 예상 추적에서 B의 두 항목을 모두 제거하세요. B는 취소를 보고하기 전에 A의 완료 게이트를 기다리고, 이후 C가 진행합니다.
완료 게이트는 비공개로 유지하세요. 게이트는 체인의 장부 기록을 나타내고, 반환되는 핸들은 개별 계산을 나타냅니다. 작업을 취소한다고 해서 앞선 계산을 중단하거나, 앞선 계산이 실행 중일 때 이후 계산을 시작해야 하는 것은 아닙니다.
애플리케이션에서 첫 실패 후 전체 배치를 중단해야 한다면 명시적인 배치 정책이 필요합니다. 게이트가 오류를 전파하도록 바꾸면 이후 작업의 실행 여부와 결과 보고 방식이 달라집니다.
언제 Foundation 의존 관계를 사용해야 하나요?
데스크톱 애플리케이션이 이미 작업을 Foundation Operation 객체로 표현한다면, 대기열에 넣기 전에 각 작업이 이전 작업에 의존하도록 설정하세요. Foundation은 이전 작업이 성공했든 실패했든 완료되면 의존 관계가 충족된 것으로 봅니다. 성공 여부에 따라 동작해야 한다면 결과를 별도로 처리해야 합니다. Operation 문서
Swift 비동기 함수를 이 모델과 연결할 때는 주의하세요. Task를 시작하고 즉시 반환하는 동기 작업은 해당 작업의 전체 수명을 나타내지 않습니다. 사용자 정의 비동기 작업은 필요한 알림과 함께 실행 및 완료 상태를 정확히 관리해야 종속 작업이 의도한 작업이 끝날 때까지 기다립니다. Apple은 비동기 작업과 완료 상태 보고 항목에서 이러한 책임을 설명합니다. Operation 수명 주기 문서
여기서 만든 로컬 비동기 계산기에는 작업 체인을 사용하면 계약이 명확해집니다. 제출을 동기적으로 접수하고, 이전 작업의 완료를 기다리며, 계산 전체를 실행하고, 결과를 공개합니다. 대기열을 데스크톱 동작에 연결할 때도 이러한 경계를 지키고, 반환값뿐 아니라 시작 및 완료 추적으로 실행 순서를 검증하세요.
출처
- MainActor — Apple
- SE-0306: Actors — Swift
- Task — Apple
- OperationQueue — Apple
- result — Apple
- yield() — Apple
- Concurrency — Swift
- value: Nonthrowing task — Apple
- checkCancellation() — Apple
- Operation — Apple