Swift는 구조화된 방식으로 비동기(asynchronous)와 병렬(parallel) 코드 작성을 지원한다.
비동기 코드(Asynchronous code)
- 일시적으로 중단되었다가 다시 실행할 수 있지만 한 번에 프로그램의 한 부분만 실행된다.
- 프로그램에서 코드를 일시 중단하고 다시 실행하면 UI 업데이트와 같은 짧은 작업을 계속 진행하면서 네트워크를 통해 데이터를 가져오거나 파일을 분석하는 것과 같은 긴 실행 작업을 계속할 수 있다.
병렬 코드(Parallel code)
- 동시에 코드의 여러 부분이 실행됨을 의미한다.
- 예를 들어 4 코어 프로세서의 컴퓨터는 각 코어가 하나의 작업을 수행하므로 코드의 4 부분을 동시에 실행할 수 있다.
병렬과 비동기 코드를 사용하는 프로그램은 한 번에 여러 연산을 수행하고, 외부 시스템을 기다리는 연산은 잠시 멈추고 이런 코드를 메모리 안전 방식으로 더 쉽게 작성할 수 있다.
병렬 또는 비동기 코드의 코드를 수행하기 위한 자원을 할당하는 이런 행동은 복잡성이 증가하는 비용이 수반된다. Swift는 자신의 의도를 표현할 수 있도록 컴파일 시간 검사가 가능하다. 예를 들어 actor(실행자)를 사용하여 안전하게 mutable state(변경 가능한 상태)에 접근할 수 있다. 그러나 느리거나 버그가 있는 코드에 동시성을 추가한다고 해서 코드가 빨라지거나 올바르게 동작한다는 보장은 없고 동시성을 추가하면 코드를 디버그 하기 더 어려워질 수 있다. 하지만 컴파일 시간에 문제를 찾는데 도움이 될 수 있다.
이전에 동시성 코드를 작성한 경험이 있다면 쓰레드 동작에 익숙할지도 모른다. Swift에서 동시성 모델은 쓰레드의 최상단에 구축되지만 직접적으로 상호작용 하지 않는다. Swift에서 비동기 함수는 실행 중인 쓰레드를 포기할 수 있다. 그러면 첫 번째 함수가 차단되는 동안 해당 쓰레드에서 다른 비동기 함수가 실행될 수 있다. 비동기 함수 실행이 재개될 때 Swift는 해당 함수가 실행될 쓰레드에 대해 어떠한 보장도 하지 않는다.
이렇게 간단한 경우에도 완료 핸들러가 연속해서 작성되어야 하므로 결국 중첩 클로저를 작성하게 된다. 중첩이 더 깊어져 코드가 복잡해지면 빠르게 다루기 힘들어질 수 있다.
listPhotos(inGallery: "Summer Vacation") { photoNames in
let sortedNames = photoNames.sorted()
let name = sortedNames[0]
downloadPhoto(named: name) { photo in
show(photo)
}
}
Defining and Calling Asynchronous Functions
비동기 함수(asynchronous function) 또는 비동기 메서드(asynchronous method)는 실행 도중에 일시적으로 중단될 수 있는 특수한 함수 또는 메서드이다. 완료될 때까지 실행되거나 오류가 발생하거나 반환되지 않는 일반적인 동기 함수 또는 메서드(synchronous functions and methods)와 대조된다. 비동기 함수도 이 세 가지 중 하나를 수행하지만 무언가를 기다리고 있을 때 중간에 일시 중지될 수 있다. 본문 내에서 실행을 일시 중지할 수 있는 부분을 표시한다.
던지는 함수(throwing function)를 나타내기 위해 throws를 사용하는 것과 유사하게 파라미터 뒤에 async 키워드를 작성해 함수 또는 메서드가 비동기임을 나타낼 수 있다. 함수 또는 메서드가 값을 반환한다면 반환 화살표(->) 전에 async를 작성한다.
func listPhotos(inGallery name: String) async -> [String] {
let result = // ... some asynchronous networking code ...
return result
}
비동기와 던지기 둘 다인 함수 또는 메서드는 throws 전에 async를 작성한다.
비동기 메서드를 호출할 때, 해당 메서드가 반환될 때까지 실행이 일시 중지된다. 중단될 가능성이 있는 지점을 표시하기 위해 호출 앞에 await를 작성한다. 에러가 있는 경우 프로그램의 흐름을 변경 가능함을 나타내기 위해 던지는 함수를 호출할 때 try를 작성하는 것과 같다. 비동기 메서드 내에서 실행 흐름은 다른 비동기 메서드를 호출할 때만 일시 중단된다. 중단은 암시적이거나 선점적이지 않다. 이것은 모든 중단 지점이 await로 표시된다는 의미이다.
갤러리에서 모든 사진 이름을 가저온 다음 첫 번째 사진을 보여주는 코드
let photoNames = await listPhotos(inGallery: "Summer Vacation")
let sortedNames = photoNames.sorted()
let name = sortedNames[0]
let photo = await downloadPhoto(named: name)
show(photo)
listPhotos(iGallery:)와 downloadPhoto(named:) 함수 모두 네트워크 요청을 필요로 하기 때문에 완료하는데 비교적 오랜 시간이 걸릴 수 있다. 따라서 async 키워드로 비동기로 만들면 그림이 준비될 때까지 기다리는 동안 앱의 나머지 코드가 계속 실행될 수 있다.
위 예제의 동시성 이해
- 첫 번째 줄에서 실행을 시작하고 첫 번째 await까지 실행된다. listPhotos(inGallery:) 함수를 호출하고 해당 함수가 반환될 때까지 실행을 일시 중지한다.
- 이 코드의 실행이 중지되는 동안 동일한 프로그램의 다른 동시 코드가 실행된다. 예를 들어 오랜 시간 실행되는 백그라운드 작업이 새 사진의 리스트를 업데이트할 수 있다. await로 표시된 다음 중단 지점 또는 완료될 때까지 실행된다.
- listPhotos(inGallery:)가 반환된 후 해당 지점에서 시작하여 계속 실행된다. 반환된 값을 photoNames에 할당한다.
- sortedNames와 name을 정의하는 부분은 일반적으로 동기 코드이다. await로 표시되지 않았으므로 가능한 중단 지점이 없다.
- downloadPhoto(named:) 함수에 대한 호출이다. 이 코드는 해당 함수가 반환될 때까지 실행을 다시 일시 중단하여 다른 동시 코드에 실행할 기회를 제공한다.
- downloadPhoto(named:)가 반환된 후에 반환 값은 photo에 할당된 다음 show(_:)를 호출할 때 인수로 전달된다.
await로 표시된 코드의 중단이 가능한 지점은 비동기 함수 또는 메서드가 반환되기를 기다리는 동안 현재 코드 부분이 실행을 일시적으로 중단할 수 있음을 나타낸다. 이를 쓰레드 양보(yielding the thread)라고 부른다. await 가 있는 코드는 실행을 일시적으로 중단할 수 있어야 하기 때문에 프로그램의 특정 위치에서만 비동기 메서드를 호출할 수 있다.
- 비동기 함수, 메서드 또는 프로퍼티의 본문에 있는 코드
- @main으로 표시된 구조체, 클래스, 또는 열거형의 정적(static) main() 메서드에 있는 코드
- 아래의 구조화되지 않은 동시성(Unstructured Concurrency)에 보이는 것처럼 구조화되지 않은 하위 작업의 코드
Task.yield() 메서드를 호출해 명시적으로 중단 지점을 추가할 수 있다.
func generateSlideshow(forGallery gallery: String) async {
let photos = await listPhotos(inGallery: gallery)
for photo in photos {
// ... render a few seconds of video for this photo ...
await Task.yield()
}
}
Task.yield(for:tolerance:clock:) 메서드는 동시성 동작이 어떻게 작동하는지 알기 위해 간단한 코드를 작성할 때 유용하다. 이 메서드는 주어진 시간만큼 현재 작업을 중단한다.
네트워크 동작을 지연시키기 위해 sleep(for:tolerance:clock:)을 사용하는 listPhotos(inGallery:) 함수
func listPhotos(inGallery name: String) async throws -> [String] {
try await Task.sleep(for: .seconds(2))
return ["IMG001", "IMG99", "IMG0404"]
}
Task.sleep(until:tolerance:clock:) 호출이 에러를 발생시킬 수 있으므로 비동기와 던지기를 모두 작성했다. 호출하려면 try와 await 모두 작성해야 한다.
let photos = try await listPhotos(inGallery: "A Rainy Weekend")
비동기 함수는 던지기 함수와 유사한 점이 있다. 비동기 함수는 던지기 함수가 다른 던지기 함수를 호출할 수 있듯이 다른 비동기 함수를 호출할 수 있다.
func getRainyWeekendPhotos() async -> Result<[String]> {
return Result {
try await listPhotos(inGallery: "A Rainy Weekend")
}
}
반면에, 동기 코드에서 호출하고 결과를 기다리기 위해 비동기 코드를 호출할 수 없다. Swift 표준 라이브러리는 의도적으로 안전하지 않은 기능을 생략한다. 이를 구현하려고 하면 미묘한 경합, 쓰레드 이슈, 그리고 데드락과 같은 문제가 발생할 수 있다. 기존 프로젝트에 비동기 코드를 추가할 때, 위에서 아래로 작업해야 한다. 특히, 비동기를 사용할 코드의 최상위 레이어부터 변환 한 다음에, 호출하는 함수와 메서드를 변환하여, 한 번에 하나의 레이어씩 작업한다. 동기 코드는 비동기 코드를 호출할 수 있는 방법이 없으므로, 상향식(bottom-up) 접근 방식을 사용할 수 없다.
Asynchronous Sequences
위에서 listPhotos(inGalleray:) 함수는 비동기적으로 배열의 모든 요소가 준비된 후에 전체 배열을 한 번에 반환한다. 또 다른 접근 방법은 비동기 시퀀스(asynchronous sequence)를 사용하여 한 번에 콜렉션의 한 요소를 기다리는 것이다.
import Foundation
let handle = FileHandle.standardInput
for try await line in handle.bytes.lines {
print(line)
}
일반적인 for-in 구문을 사용하는 대신 for 다음에 await를 작성한다. 비동기 함수와 마찬가지로 await 키워드로 중단 지점을 나타낸다. for-await-in 루프는 다음 요소를 사용할 수 있을 때까지 기다리고 각 반복이 시작될 때 잠재적으로 실행을 일시 중단한다.
Calling Asynchronous Functions in Parallel
await를 사용하여 비동기 함수를 호출하면 한 번에 코드의 한 부분만 실행된다. 비동기 코드가 실행되는 동안 호출자는 코드의 다음 라인을 실행하기 위해 이동하기 전에 해당 코드가 완료될 때까지 기다린다. 예를 들어 갤러리에서 처음 세 장의 사진을 가져오려면 다음과 같이 downloadPhoto(named:) 함수에 대한 세 번의 호출을 기다릴 수 있다.
let firstPhoto = await downloadPhoto(named: photoNames[0])
let secondPhoto = await downloadPhoto(named: photoNames[1])
let thirdPhoto = await downloadPhoto(named: photoNames[2])
let photos = [firstPhoto, secondPhoto, thirdPhoto]
show(photos)
이 방식에는 중요한 단점이 있다. 다운로드가 비동기이고 진행되는 동안 다른 작업을 수행할 수 있지만 downloadPhoto(named:)에 대한 호출은 한 번에 하나만 실행된다. 각 사진은 개별적으로 또는 동시에 다운로드할 수 있다.
비동기 함수를 호출하고 주변의 코드와 병렬로 실행하려면 상수를 정의할 때 let 앞에 async를 작성하고 상수를 사용할 때마다 await를 작성한다.
async let firstPhoto = downloadPhoto(named: photoNames[0])
async let secondPhoto = downloadPhoto(named: photoNames[1])
async let thirdPhoto = downloadPhoto(named: photoNames[2])
let photos = await [firstPhoto, secondPhoto, thirdPhoto]
show(photos)
downloadPhotos(named:)를 호출하는 세 가지는 모두 이전 호출이 완료되길 기다리지 않고 시작된다. 사용할 수 있는 자원이 충분하다면 동시에 실행할 수 있다. 코드가 함수의 결과를 기다리기 위해 일시 중단되지 않기 때문에 이러한 함수 호출 중 어느 것도 await로 표시하지 않는다. 대신 photos가 정의된 라인까지 실행이 계속된다. photos를 선언하는 시점에는 사진들이 모두 다운로드될 때까지 실행을 일시 중단하기 위해 await를 작성한다.
두 접근 방식의 차이점
- 다음 줄의 코드가 해당 함수의 결과에 따라 달라지면 await를 사용해 비동기 함수를 호출한다. 순차적으로 실행되는 작업을 생성한다.
- 나중에 코드에서 결과가 필요하지 않을 때 async-let을 사용하여 비동기 함수를 호출한다. 병렬로 수행할 수 있는 작업이 생성된다.
- awat와 async-let은 모두 일시 중단되는 동안 다른 코드를 실행할 수 있도록 한다.
- 두 경우 모두 비동기 함수가 반환될 때까지 필요한 경우 실행이 일시 중단됨을 나타내기 위해 가능한 일시 중단 지점을 await로 표시한다.
두 접근 방식을 혼합할 수도 있다.
Tasks and Task Groups
작업(task)은 프로그램의 일부로 비동기적으로 실행할 수 있는 작업 단위이다. 모든 비동기 코드는 어떠한 작업의 일부로 실행된다. 작업은 한 번에 하나의 작업만 수행하지만, 여러 작업을 생성하면, Swift는 동시에 수행하기 위해 작업을 스케쥴링할 수 있다.
async-let 구문은 암시적으로 하위 작업을 생성한다. 프로그램에서 무슨 작업을 수행해야 될지 이미 알고 있을 때 잘 동작한다.
작업 그룹(task group)(TaskGroup의 인스턴스)을 생성하고 우선순위와 취소를 더 잘 제어할 수 있으며 동적으로 작업의 수를 생성할 수 있도록 해당 그룹에 하위 작업을 명시적으로 추가할 수 있다.
작업은 계층 구조로 정렬된다. 주어진 작업 그룹의 각 작업에는 동일한 상위 작업이 있으며 각 작업에는 하위 작업이 있을 수도 있다. 작업과 작업 그룹 간의 명시적 관계 때문에 이 접근 방식을 구조적 동시성(structured concurrency)이라고 한다. 명시적 부모(parent)-자식(child) 관계는 작업 간에 여러 이점이 있다.
- 부모 작업에서 하위 작업이 완료될 때까지 기다릴 수 있다.
- 하위 작업에서 더 높은 우선순위로 설정되면, 상위 작업의 우선순위는 자동으로 높아진다.
- 상위 작업이 취소되면, 각 하위 작업들은 자동으로 취소된다.
- 작업-로컬 값(Task-local value)은 하위 작업에 효율적이고 자동으로 전파된다.
여러 사진을 다운로드하는 코드
await withTaskGroup(of: Data.self) { group in
let photoNames = await listPhotos(inGallery: "Summer Vacation")
for name in photoNames {
group.addTask {
return await downloadPhoto(named: name)
}
}
for await photo in group {
show(photo)
}
}
새로운 작업 그룹을 생성하고, 갤러리에서 사진을 다운로드하는 하위 작업을 생성한다. 하위 작업에서 사진 다운로드가 끝나자마자 해당 사진은 보인다. 하위 작업에 대한 완료 순서를 보장하지 않으므로, 갤러리에 사진은 무작위로 보일 수 있다.
사진 다운로드 코드에서 에러가 발생할 수 있다면, withThrowingTaskGroup(of:returning:body:)을 호출해야 한다.
위 코드는 각 사진은 다운로드되고 보이므로, 작업 그룹은 결과를 반환하지 않는다. 결과를 반환하는 작업 그룹의 경우, withTaskGroup(of:returning:body:)에 전달하는 클로저에 결과를 누적하는 코드를 추가한다.
let photos = await withTaskGroup(of: Data.self) { group in
let photoNames = await listPhotos(inGallery: "Summer Vacation")
for name in photoNames {
group.addTask {
return await downloadPhoto(named: name)
}
}
var results: [Data] = []
for await photo in group {
results.append(photo)
}
return results
}
이전 예제와 같이, 각 사진을 다운로드하는 하위 작업을 생성한다. 이전 예제와 다르게 for-await-in 루프는 다음 하위 작업이 완료될 때까지 기다리고, 해당 작업의 결과를 배열에 추가한 다음, 모든 하위 작업이 완료될 때까지 기다린다. 마지막으로, 작업 그룹은 다운로드한 사진의 배열을 전체 결과로 반환한다.
Task Cancellation
Swift 동시성은 협동 취소 모델(cooperative cancellation model)을 사용한다. 각 작업은 실행 중에 적절할 때 취소 여부를 확인하고, 적절하게 취소에 응답한다. 작업의 종류에 따라 취소에 대한 응답은 다음 중 하나에 해당한다.
- CancellationError와 같은 에러 발생
- nil 또는 빈 콜렉션 반환
- 부분적으로 완료된 작업 반환
작업이 취소되었는지 확인하는 방법은 두 가지가 있다.
- CancellationError를 throw 하는 Task.checkCancellation() 메서드 호출
- Task.isCancelled 프로퍼티의 값을 확인하고 핸들링
checkCnacellation()을 호출하는 것은 작업이 취소되었으면 에러를 발생시킨다. 던지는 작업은 작업 외부로 에러를 전파하여 모든 작업을 중지시킬 수 있다. 이것은 구현과 이해가 간단하다는 이점이 있다. 더 유연한 방법으로는, 네트워크 연결을 닫고 임시 파일을 지우는 것과 같은 작업 중지의 부분으로 정리 작업을 수행할 수 있는 isCancelled 프로퍼티를 사용한다.
let photos = await withTaskGroup(of: Optional<Data>.self) { group in
let photoNames = await listPhotos(inGallery: "Summer Vacation")
for name in photoNames {
group.addTaskUnlessCancelled {
guard isCancelled == false else { return nil }
return await downloadPhoto(named: name)
}
}
var results: [Data] = []
for await photo in group {
if let photo { results.append(photo) }
}
return results
}
- 취소 후에 새로운 작업이 시작되는 것을 피하기 위해 각 작업은 Task.Group.addTaskUnlessCancelled(priority:operation:) 메서드를 사용해 추가된다.
- 각 작업은 사진을 다운로드하기 전에 취소 여부를 검사한다. 작업이 취소되었으면, nil을 반환한다.
- 결국, 작업 그룹은 결과를 수집할 때 nil 값이면 건너뛴다. nil을 반환해서 취소를 처리한다는 것은 작업 그룹은 부분적인 결과를 반환할 수 있다는 의미이다. 취소했을 때 이미 다운로드된 사진은 파기하는 대신에 다운로드된 사진을 반환한다.
즉시 취소에 대한 알림이 필요한 경우 Task.withTaskCancellationHandler(operation:onCancel:) 메서드를 사용한다.
let task = await Task.withTaskCancellationHandler {
// ...
} onCancel: {
print("Canceled!")
}
// ... some time later...
task.cancel() // Prints "Canceled!"
취소 처리를 사용할 때, 작업 취소는 협조적이다. 작업은 완료될 때까지 수행하거나 취소를 확인하고 조기 중지한다. 취소 처리가 시작될 때 작업은 여전히 수행 중이므로, 경쟁 조건이 생성될 수 있는 작업과 취소 처리 간의 상태 공유를 피해야 한다.
Unstructured Concurrency
작업 그룹의 일부인 작업과 달리 구조화되지 않은 작업(unstructured task)에는 상위 작업이 없다. 프로그램이 필요로 하는 방식으로 구조화되지 않은 작업을 관리할 수 있는 완전한 유연성이 있지만 정확성에 대한 완전한 책임도 있다. 현재 액터(actor)에서 실행되는 구조화되지 않은 작업을 생성하려면 Task.init(priority:operation:) 초기화 구문을 호출해야 한다. 더 구체적으로 분리된 작업으로 알려진 현재 액터의 일부가 아닌 구조화되지 않은 작업을 생성하려면 Task.detached(priority:operation:) 클래스 메서드를 호출한다. 이 모든 동작은 서로 상호작용 할 수 있는 작업을 반환한다. 예를 들어 결과를 기다리거나 취소하는 경우가 해당된다.
let newPhoto = // ... some photo data ...
let handle = Task {
return await add(newPhoto, toGalleryNamed: "Spring Adventures")
}
let result = await handle.value
분리된 작업(detached tasks) 관리에 대한 자세한 내용은 Task를 참고해야 한다.
Actors
프로그램을 동시성 조각으로 분리하기위해 작업을 사용할 수 있다. 작업은 서로 분리되어 있어 같은 시간에 안전하게 실행될 수 있지만 작업 간에 일부 정보를 공유해야 할 수도 있다. 액터는(Actors) 동시성 코드간에 정보를 안전하게 공유할 수 있게 해준다.
클래스와 마찬가지로 액터는 참조 타입이다. 클래스와 다르게 한 번에 하나의 작업만 변경 가능한 상태에 접근할 수 있도록 허용하므로 여러 작업의 코드가 액터의 동일한 인스턴스와 상호작용 하는 것은 안전하다.
온도를 기록하는 액터
actor TemperatureLogger {
let label: String
var measurements: [Int]
private(set) var max: Int
init(label: String, measurement: Int) {
self.label = label
self.measurements = [measurement]
self.max = measurement
}
}
액터 외부의 다른 코드가 접근할 수 있는 프로퍼티가 있으며 액터 내부의 코드만 최대값을 업데이트 할 수 있게 max 프로퍼티를 제한한다.
구조체와 클래스와 동일한 생성자로 액터의 인스턴스를 생성한다. 프로퍼티 또는 메서드에 접근할 때 일시 중단 지점을 나타내기 위해 await를 사용한다.
let logger = TemperatureLogger(label: "Outdoors", measurement: 25)
print(await logger.max)
// Prints "25"
액터는 한 번에 하나의 변경 가능한 상태에 접근할 수 있도록 허용하므로 다른 작업의 코드가 이미 로거와 상호 작용하고 있는 경우 이 코드는 프로퍼티 접근을 기다리는 동안 일시 중단된다.
대조적으로 액터의 부분인 코드는 액터의 프로퍼티에 접근할 때 await를 사용하지 않는다.
extension TemperatureLogger {
func update(with measurement: Int) {
measurements.append(measurement)
if measurement > max {
max = measurement
}
}
}
update(with:) 메서드는 액터에서 이미 실행 중이므로 max와 같은 프로퍼티에 접근할 때 await를 작성하지 않는다. 이 메서드는 액터가 변경 가능한 상태와 상호 작용하기 위해 한 번에 하나의 작업만 허용하는 이유 중 하나를 보여준다. 액터의 상태에 대한 일부 업데이트는 일시적으로 불변성을 깨드린다. TemperatureLogger 액터는 온도 리스트와 최대 온도를 추적하고 새로운 측정값을 기록할 때 최대 온도를 업데이트 한다. 업데이트 도중에 새로운 측정 값을 추가한 후 max를 업데이트 하기 전에 온도 로거는 일시적으로 일치하지 않는 상태가 된다. 따라서 여러 작업이 동일한 인스턴스에 상호 작용하는 것을 방지해야 한다.
- update(with:) 메서드를 호출한다. 먼저 measurements 배열을 업데이트 한다.
- max를 업데이트 하기 전에 다른 코드에서 최대값과 온도 배열을 읽는다.
- max를 변경하여 업데이트를 완료한다.
이러한 경우 다른 곳에서 실행 중인 코드는 부정확한 정보에 접근하게 된다. Swift 액터는 한 번에 해당 상태에 대해 하나의 작업만 허용하고 해당 코드는 await가 일시 중단 지점으로 표시되는 위치에서만 중단될 수 있기 때문에 swift 액터를 사용하여 이 문제를 방지할 수 있다. update(with:)는 일시 중단 지점을 포함하지 않으므로 다른 코드는 업데이트 중간에 데이터에 접근할 수 없다.
액터 외부의 코드에서 구조체 또는 클래스의 프로퍼티에 접근하는 것과 같이 프로퍼티에 직접적으로 접근하면 컴파일 에러가 발생한다.
print(logger.max) // Error
await 작성 없이 logger.max에 접근하는 것은 액터의 프로퍼티가 해당 액터의 분리된 로컬 상태의 부분이기 때문에 실패한다. 이 프로퍼티에 접근하는 코드는 비동기 작업인 액터의 일부분으로 수행되어야 하고, await를 작성해야 한다. Swift는 액터 안의 코드만 액터의 로컬 상태에 접근할 수 있도록 보장한다. 이 보장을 액터 분리(actor isolation)이라고 한다.
- 중단 가능 지점 사이의 코드는 다른 비동기 코드의 중단 없이 순차적으로 수행된다.
- 액터의 로컬 상태와 상호작용하는 코드는 해당 액터에서만 수행된다.
- 액터는 한 번에 하나의 코드만 수행한다.
이러한 보장 때문에, await를 포함하고 액터 내부에 있는 코드는 프로그램의 다른 위치에서 일시적으로 유효하지 않은 상태를 관찰하지 않고 업데이트를 수행할 수 있다.
측정된 온도를 화씨에서 섭씨로 변환
extension TemperatureLogger {
func convertFarenheitToCelsius() {
measurements = measurements.map { measurement in
(measurement - 32) * 5 / 9
}
}
}
측정된 온도를 한 번에 하나씩 변환한다. map 작업이 진행되는 동안, 일부 온도는 화씨로 다른 온도는 섭씨로 표시된다. 그러나, await를 포함하지 않으므로, 메서드에서 중단 지점이 없다. 이 메서드가 수정하는 상태는 액터에 속하며, 해당 코드가 액터에서 수행될 때를 제외하고는 코드를 읽거나 수정하지 못하도록 한다. 온도 변환이 진행되는 동안 다른 코드가 부분적으로 변환된 온도를 읽을 수 없다는 것을 의미한다.
Sendable Types
작업 또는 액터의 인스턴스 내에서 변수와 프로퍼티와 같은 변경 가능한 상태를 포함하는 프로그램의 일부분을 동시성 도메인(concurrency domain)이라고 부른다. 어떤 데이터는 변경 가능한 상태를 포함하지만 동시 접근에 대해 보호되지 않으므로 동시성 도메인 간 공유될 수 없다.
한 동시성 도메인에서 다른 동시성 도메인으로 공유될 수 있는 타입을 전송 가능 타입(sendable type)이라고 한다. 예를 들어, 액터 메서드의 파라미터로 전달되거나 작업의 결과로 반환될 수 있다. 이 챕터의 앞부분에 있는 예제들은 동시성 도메인 간에 전달되는 데이터는 항상 안전한 간단한 값 타입을 사용하기 때문에 전송 가능성에 대해 논의하지 않았다. 반대로 일부 타입은 동시성 도메인 간에 전달하기 위해 안전하지 않다. 예를 들어, 변경 가능한 프로퍼티를 포함하고 해당 프로퍼티에 대한 접근을 직렬화하지 않는 클래스는 다른 작업간에 해당 클래스의 인스턴스를 전달할 때 예상할 수 없고 잘못된 결과를 생성할 수 있다.
Sendable 프로토콜을 선언하여 전송 가능한 타입을 표시한다. 어떠한 요구사항을 가지지 않지만 Swift가 적용하는 의미론적 요구사항이 있다. 타입을 전송 가능한 것으로 나타내기 위한 세 가지 방법이 있다.
- 타입은 값 타입이고 변경 가능한 상태는 다른 전송 가능한 데이터로 구성된다. 예를 들어, 전송 가능한 저장된 프로퍼티가 있는 구조체 또는 전송 가능한 연관된 값이 있는 열거형이 있다.
- 타입은 변경 가능한 상태가 없으며 변경 불가능한 상태는 다른 전송 가능한 데이터로 구성된다. 예를 들어, 읽기 전용 프로퍼티만 있는 구조체 또는 클래스
- 타입은 @MainActor로 표시된 클래스나 특정 쓰레드나 큐에서 프로퍼티에 순차적으로 접근하는 클래스와 같이 변경 가능한 상태의 안전성을 보장하는 코드를 가지고 있다.
의미론적 요구사항의 자세한 리스트는 전송 가능(Sendable) 프로토콜 참조
전송 가능한 프로퍼티만 가지는 구조체와 전송 가능한 연관된 값만 가지는 열거형과 같이 어떠한 타입은 항상 전송 가능하다.
struct TemperatureReading: Sendable {
var measurement: Int
}
extension TemperatureLogger {
func addReading(from reading: TemperatureReading) {
measurements.append(reading.measurement)
}
}
let logger = TemperatureLogger(label: "Tea kettle", measurement: 85)
let reading = TemperatureReading(measurement: 45)
await logger.addReading(from: reading)
TemperatureReading은 전송 가능한 프로퍼티만 가지는 구조체이며 public 또는 @usableFromInline으로 표시되지 않는 구조체이므로 암시적으로 전송 가능하다.
Sendable 프로토콜 준수가 암시되는 구조체
struct TemperatureReading {
var measurement: Int
}
명시적으로 타입을 보낼 수 없는 것으로 나태내려면 Sendable 프로토콜에 대한 암시적으로 준수를 재정의하고 확장을 사용한다.
struct FileDescriptor {
let rawValue: CInt
}
@available(*, unavailable)
extension FileDescriptor: Sendable { }
위의 코드는 POSIX 파일 디스크립터에 대한 래퍼의 일부분을 보여준다. 파일 디스크립터의 인터페이스는 정수를 사용하여 열린 파일에 대해 식별하고 상호작용 하고 정수값을 보낼 수 있지만, 비동기적 도메인을 통해 전송하는 것은 안전하지 않다.
위의 코드에서 NonsendableTemperatureReading은 암시적으로 보낼 수 있는 구조체이다. 그러나 확장에 Sendable에 대한 준수를 사용할 수 없게 만들어 타입 전송을 막을 수 있다.
참조
https://siwon-code.tistory.com/36
[Swift] Swift Concurrency와 GCD
안녕하세요. 저번 글에서 Race Condition과 Thread Safe에 대해 알아봤었는데 비동기에 대해서 공부하다 보니 자연스럽게 WWDC 2021에서 소개된 Swift Concurrency에 대해서도 관심이 생겼습니다. 그래서 이번
siwon-code.tistory.com
https://siwon-code.tistory.com/36
[Swift] Swift Concurrency와 GCD
안녕하세요. 저번 글에서 Race Condition과 Thread Safe에 대해 알아봤었는데 비동기에 대해서 공부하다 보니 자연스럽게 WWDC 2021에서 소개된 Swift Concurrency에 대해서도 관심이 생겼습니다. 그래서 이번
siwon-code.tistory.com
'Swift 공식 문서' 카테고리의 다른 글
Nested Types (0) | 2024.01.15 |
---|---|
Type Casting (0) | 2024.01.15 |
Error Handling (0) | 2024.01.08 |
Optional Chaining (1) | 2024.01.05 |
Deinitialization (1) | 2024.01.04 |