Concurrency - swift docs 를 읽고 정리한 글 입니다.
Swift는 구조화된 방식으로 비동기 및 병렬 코드를 작성할 수 있도록 bulit-in 지원합니다. 한 번에 한 개의 프로그램만 실행 될지라도 비동기 코드는 일시 중단되었다가 나중에 재개될 수 있습니다.
프로그램에서 코드를 일시 중단했다가 재개하면 장기 실행 작업(네트워크를 통해 데이터를 가져오거나 파일을 구문 분석하는 등)을 계속하면서 단기 작업(UI 업데이트 등)에서 계속 진전을 이룰 수 있습니다.
병렬 코드는 여러 코드가 동시에 실행되는 것을 의미합니다. 예를 들어 4코어 프로세서가 있는 컴퓨터는 4개의 코드를 동시에 실행할 수 있으며 각 코어가 하나의 작업을 수행합니다.
병렬 및 비동기 코드를 사용하는 프로그램은 한 번에 여러 작업을 수행합니다. 이는 외부 시스템을 기다리는 작업을 중단하고 메모리 안전한 방식으로 코드를 작성하는 것을 쉽게 해줍니다.
병렬 또는 비동기 코드에서 추가적인 스케줄링은 복잡성 비용의 증가를 가져옵니다. Swift에서는 몇 가지 컴파일 타임 체크를 통해 개발자의 의도를 표현할 수 있습니다. 예를들어 actor를 사용하여 가변 상태(mutable state)값에 안전하게 접근할 수 있습니다.
그러나 느리거나 버그가 많은 코드에 동시성 코드를 추가하는 것이 코드가 더 빨라지거나 정확해진다는 보장은 없습니다. 사실 동시성을 추가하면 코드 디버깅이 더 어려워질 수 있습니다. 그러나 동시성이 필요한 코드에 swift에서 제공하는 Concurrency를 사용한다면 컴파일 타임에 문제를 파악하는 데 도움이 됩니다.
이 장의 나머지 부분에서는 비동기(asynchronous) 코드와 병렬(parallel) 코드의 일반적인 조합을 지칭할 때 동시성(concurrency)이라는 용어를 사용합니다.
Note
동시성 코드를 작성한 적이 있다면 쓰레드 작업에 익숙할 수도 있습니다. Swift의 동시성 모델은 쓰레드 위에 구축되어 있지만 직접 쓰레드와 상호 작용하지 않습니다. Swift에서 비동기 함수는 실행 중인 쓰레드를 포기하고 블락킹 상태가 되어 다른 비동기 함수를 해당 쓰레드에서 실행시킬 수 있습니다. 다만 함수가 어떤 쓰레드에서 실행될지 보장하지 않습니다.
Swift의 언어 지원 없이 동시성 코드를 작성할 수 있지만, 해당 코드는 읽기 더 어렵곤 합니다. 아래는 사진 이름 리스트를 다운로드하고 해당 리스트의 첫 번째 사진을 다운로드한 후 해당 사진을 사용자에게 보여주는 코드입니다.
listPhotos(inGallery: "Summer Vacation") { photoNames in
let sortedNames = photoNames.sorted()
let name = sortedNames[0]
downloadPhoto(named: name) { photo in
show(photo)
}
}
이렇게 단순한 경우에도 연속된 completion handler 코드를 작성해야 하므로 중첩 클로저가 생기게 됩니다. 많은 중첩을 갖는 더욱 복잡한 코드는 빠르게 다루기 힘들어질 것 입니다.
Defining and Calling Asynchronous Functions
비동기 함수 및 메서드는 실행을 통해 일정 부분 진행되는 동안 중단될 수 있는 함수 및 메서드의 특별한 종류입니다.
여기부터 함수 및 메서드를 함수로 통일하겠습니다.
이는 일반적인 동기 함수와는 대조됩니다. 1) 끝까지 실행하여 완료 2) 오류 발생 3) return 되지 않음
비동기 함수는 여전히 이 세 가지 중 하나를 수행하지만, 무언가를 기다리고 있을 때 완료 전 중간 지점에서 일시 중지할수 있습니다. 비동기 함수의 본문 안에서 실행이 중단될 수 있는 장소를 표시합니다.
함수가 비동기임을 나타내기 위해 매개 변수 뒤에 async 키워드를 씁니다. 이는 에러 처리를 호출 시점으로 넘기기 위해 throws로 표시하는 것과 유사합니다.
함수가 반환 값을 가지면 반환 화살표 앞에 async를 표기합니다. 예를 들어 갤러리에서 사진의 이름을 가져오는 방법은 다음과 같습니다:
func listPhotos(inGallery name: String) async -> [String] { // ✅ async
let result = // ... some asynchronous networking code ...
return result
}
비동기이면서 에러를 던지는 함수는 throws 키워드 이전에 async를 표기합니다.
async 함수를 호출하면 해당 함수가 return 할 때 까지 실행이 중단됩니다. 함수 호출 앞에 await를 적어 중단점을 표기합니다. 이는 에러를 던지는 함수 앞에 try를 적어 에러가 발생할 수 있는 지점을 표기하는 것과 같습니다.
async 함수 내부에서는 다른 async 함수가 호출되는 경우에만 실행 흐름이 중단됩니다. 즉 중단(suspension)은 암시적(implicit)이거나 선점형(preemptive)이 아닙니다.
- 모든 중단 지점은 await으로 명시되어야 하며 비선점 방식으로 동작한다. (하나의 작업이 끝나지 않으면 다른 작업이 해당 쓰레드를 차지할 수 없음)
아래 코드는 갤러리 모든 사진들의 이름을 가져오고 첫 번째 사진을 보여주는 예시 코드 입니다.
let photoNames = await listPhotos(inGallery: "Summer Vacation") // ✅ await으로 중단점 설정
let sortedNames = photoNames.sorted()
let name = sortedNames[0]
let photo = await downloadPhoto(named: name) // ✅ await으로 중단점 설정
show(photo)
listPhotos(inGallery:)와 downloadPhoto(name:) 함수는 모두 네트워크 요청을 해야 하기 때문에 완료까지 비교적 긴 시간이 걸릴 수 있습니다. 이 함수들 선언부의 return 화살표 앞에 async를 표시하면 사진이 준비되기를 기다리는 동안 앱의 나머지 코드들은 계속 실행 가능하게 됩니다.
아래는 하나의 가능한 실행 순서입니다 :
1. 코드는 첫 번째 줄부터 실행되기 시작하여 첫 번째 await까지 실행됩니다. listPhotos(inGallery:) 함수를 호출하고 해당 함수가 돌아오기를 기다리는 동안 실행을 중지합니다.
2. 이 코드의 실행이 중단되는 동안 동일한 프로그램의 다른 동시성 코드가 실행됩니다. 예를 들어, 긴 시간 수행되는 background task는 새로운 사진 갤러리 리스트를 업데이트합니다. 이 코드는 await이 표시된 다음 중단점을 만나거나 완료될 때 까지 실행됩니다.
3. listPhotos(inGallery:)가 반환된 후 이 코드는 해당 라인부터 실행을 이어갑니다. 반환된 값은 photoNames에 할당합니다.
4. sortedNames과 name가 정의된 라인들은 일반적인 동기 코드입니다. 이 라인들에는 await 표시가 없기 때문에 어떠한 중단점도 생기지 않습니다.
4. 다음 await 키워드는 downloadPhoto(name:) 함수 호출에 표시되어 있습니다. 해당 함수가 돌아올 때까지 실행을 다시 중단하여 다른 동시성 코드가 실행될 수 있는 기회를 줍니다.
6. downloadPhoto(name:)가 반환된 후 반환 값이 photo에 할당되고 다음 show(_:)의 매개변수로 전달됩니다.
await으로 표시된 중단점은 비동기 함수가 끝나기를 기다리는 동안 실행을 중지할 수 있음을 나타냅니다. Swift가 내부적으로 현재 쓰레드에서의 코드 실행을 중단하고 다른 코드를 실행하기 때문에쓰레드를 양보한다(yeilding the thread)고도 합니다.
await가 있는 코드는 실행을 중지시킬 수 있어야 하므로, 특정 위치에서만 비동기 함수를 호출할 수 있습니다.
- 비동기 함수, 메서드 또는 프로퍼티의 본문에 있는 코드
- @main으로 표시된 구조체, 클래스, 열거형의 static main() 메서드의 코드
- 이후 Unstructured Concurrency 파트에서 소개될 비구조화된 자식 Task.
중단점 사이의 코드는 다른 동시성 코드에 의해 방해받을 일이 없이 순차적으로 실행됩니다. 예를들어 아래의 코드는 한 갤러리에서 다른 갤러리로 사진을 이동합니다.
let firstPhoto = await listPhotos(inGallery: "Summer Vacation")[0]
add(firstPhoto, toGallery: "Road Trip")
// 이 지점에서 firstPhoto는 두 갤러리에 모두 존재한다.
remove(firstPhoto, fromGallery: "Summer Vacation")
add(_:toGallery:) 와 remove(_:fromGallery:) 사이에 다른 코드들이 실행될 수는 없습니다. 이 사이 지점에서 두 갤러리 모두 firstPhoto가 나타나 일시적으로 앱의 불변성(invariants)이 깨집니다.
미래에도 이 지점에 await 함수가 추가되지 않아야 함을 더 분명히 하기 위해 이 코드들을 동기 함수로 변경할 수 있습니다.
func move(_ photoName: String, from source: String, to destination: String) {
add(photoName, toGallery: destination)
remove(photoName, fromGallery: source)
}
// ...
let firstPhoto = await listPhotos(inGallery: "Summer Vacation")[0]
move(firstPhoto, from: "Summer Vacation", to: "Road Trip")
위의 예제에서 move(_:from:to:) 함수는 동기 함수이므로 중단점을 절대 포함할 수 없음을 보장합니다. 만약 동시성 코드를 이 함수에 추가하게 된다면, 컴파일 타임 에러를 통해 버그를 막을 수 있습니다.
Note
Task.sleep(until:tolerance:clock:) 메서드는 동시성 작업이 어떻게 동작하는지 배울 때 유용한코드입니다.
이 메서드는 다른 작업 없이 그저 지정된 시간(nanaoseconds)을 기다리고 반환합니다.
ex) listPhotos(inGallery:)에서 네트워크 요청을 기다리는 상황을 Task.sleep으로 시뮬레이션
func listPhotos(inGallery name: String) async throws -> [String] {
try await Task.sleep(until: .now + .seconds(2), clock: .continuous)
return ["IMG001", "IMG99", "IMG0404"]
}
Asynchronous Sequences
앞서 나온 listPhotos(inGallery:) 함수는 배열의 모든 요소가 준비된 후에 전체 배열을 한 번에 비동기로 반환합니다.
또 다른 방법은 비동기 시퀀스를 사용하여 컬렉션의 요소를 한 번에 하나씩 기다리는 것입니다. 비동기 시퀀스에 대한 반복은 다음과 같습니다.
import Foundation
let handle = FileHandle.standardInput
for try await line in handle.bytes.lines {
print(line)
}
위의 예제는 일반적인 for-in 루프를 사용하는 대신 그 뒤에 await를 씁니다. 비동기 함수를 호출할 때와 같이 await은 가능한 중단 지점을 나타냅니다. for-await-in 루프는 각 반복이 시작될 때, 실행을 일시 중단합니다. (다음 요소 사용 가능할 때까지 대기)
Sequqnece 프로토콜을 채택한 커스텀 타입을 for-in 루프에서 사용할 수 있는 것 과 동일하게 AsyncSequence 프로토콜을 채택한 타입으로 for-await-in 루프를 사용할 수 있습니다.
Calling Asynchronous Functions in Parallel
await를 사용하여 비동기 함수를 호출하면 한 번에 하나의 코드만 실행됩니다. 비동기 코드가 실행되는 동안, 호출자는 해당 코드가 끝날 때 까지 다음 라인을 실행하지 않고 기다립니다.
예를들어, 갤러리에서 처음 세 장의 사진을 가져오기 위해 아래와 같이 세 번의 await 키워드를 붙여 downloadPhoto(name:) 함수 호출할 수 있습니다.
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가 비동기이고 이 작업 중에 다른 작업이 가능하지만, downloadPhoto(named:)는 한 번에 하나만 실행됩니다. 각 사진의 다운로드는 다음 다운로드가 시작되기 이전에 완전히 끝나게 됩니다. 그러나 이 작업은 대기할 필요가 없습니다. 각 사진은 독립적으로, 또는 동시에 다운로드할 수 있습니다.
비동기 함수를 다른 코드들과 병렬(parallel)적으로 실행하기 위한 방법이 있습니다. 상수를 정의하기 전 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)
이 예제에서 downloadPhoto(named:)을 다운로드하기 위한 세 개의 호출 모두 이전 호출이 완료될 때까지 기다리지 않고 시작합니다. 사용 가능한 시스템 리소스가 충분하다면 동시에 실행할 수 있습니다. 코드가 함수의 결과를 기다리도록 중단되지 않기 때문에 함수들의 호출 중 어느 것도 await을 표기하지 않습니다.
대신, photos가 정의된 행까지 실행이 계속되고 이 지점에서 프로그램은 비동기 호출의 결과를 필요로 하므로, 세 개의 사진 모두 다운로드가 끝날 때까지 실행을 중지하도록 await를 씁니다.
이 두 접근법의 차이는 다음과 같습니다 :
- 비동기 함수를 await 키워드로 호출
- 다음 라인들이 함수의 결과 값에 영향을 받는 경우 사용
- 순차적으로 작업을 수행
- 비동기 함수를 async let 키워드로 호출
- 함수의 결과 값이 당장 필요 없는 경우 사용
- 병렬로 작업을 수행
두 방식 모두 이들이 중단되었을 때 다른 코드들을 실행할 수 있습니다. 두 방식 모두 가능한 중단점을 await로 표시하여 필요한 경우 비동기 함수가 반환될 때까지 실행이 중지됨을 나타냅니다. 또한 두 가지 방법을 동일 코드 안에서 혼합하여 사용할 수 있습니다.
Tasks and Task Groups
Task는 프로그램의 일부로 비동기로 실행될 수 있는 작업 단위입니다. 모든 비동기 코드는 Task의 일부로 실행됩니다. 앞에서 설명한 async-let 구문은 자식 Task를 생성해줍니다.
또한 Task 그룹을 생성하고 자식 Task를 해당 그룹에 넣어 우선순위(priority), 취소(cancellation)을 더 잘 제어하고 동적으로 Task들을 생성할 수 있습니다.
Task들은 계층 구조로 정렬됩니다. Task 그룹 안의 모든 Task는 동일한 부모 task를 가지며 각각의 자식 Task들을 가질 수 있습니다. Task와 Task 그룹간의 관계가 명시적이기 때문에 이러한 방식을 구조화된 동시성(Structured Concurrency)이라고 합니다.
정확성에 대한 책임의 일부가 개발자에게 있기는 하지만, Task 간의 명시적인 부모 자식 관계는 취소 전파 같은 동작을 Swift가 처리하게 하고 컴파일 타임에 일부 에러들을 감지할 수 있게 합니다.
await withTaskGroup(of: Data.self) { taskGroup in
let photoNames = await listPhotos(inGallery: "Summer Vacation")
for name in photoNames {
taskGroup.addTask { await downloadPhoto(named: name) }
}
}
more info : TaskGroup
Unstructured Concurrency
Swift는 앞서 설명한 동시성에 대한 구조화된 접근 방식 외에도 비구조화된 동시성을 지원합니다.
Task 그룹의 일부인 Task와 다르게 비구조화된 Task는 부모 Task가 없습니다. 프로그램에 필요한 방식으로 비구조화된 Task를 관리할 수 있는 유연성을 가지고 있지만 정확성에 대한 책임도 전적으로 있습니다.
현재 액터에서 실행되는 비구조화된 Task를 만들려면 Task.init(priority:operation:) 생성자를 호출합니다.
현재 액터의 일부가 아닌 비구조화된 Task(특히 분리된 Task)를 만들려면 Task.detached(priority:operation:) 클래스 메서드를 호출합니다.
위 두 케이스 모두 결과를 기다리거나 취소하는 등 상호 작용할 수 있는 Task를 반환합니다.
let newPhoto = // ... some photo data ...
let handle = Task {
return await add(newPhoto, toGalleryNamed: "Spring Adventures")
}
let result = await handle.value
more info : Task
Task Cancellation
Swift 동시성은 협력 취소 모델(cooperative cancellation model)을 사용합니다. 각 Task는 실행 중 알맞은 시점에 취소되었는지 여부를 확인하고, 적절한 방법으로 취소에 대응합니다.
수행 작업에 따라 보통 다음 중 하나를 의미하게 됩니다.
- CancellationError 같은 에러 던짐
- nil 또는 빈 컬렉션 반환
- 부분적으로 성공한 작업 반환
취소 여부를 확인하려면 Task가 cancel 되었을 때 CancellationError을 던지는 Task.checkCancelation()를 호출하거나 Task.isCancelled 값을 확인하고 직접 핸들링할 수 있습니다. 에를 들어 갤러리에서 사진을 다운로드하는 작업에서 부분 다운로드들을 삭제하고 네트워크 연결을 닫아야할 수 있습니다.
취소를 수동으로 전파하려면 Task.cancel()을 호출합니다.
Actors
Task를 사용하여 프로그램을 격리된(isolated) 동시성 조각으로 분리할 수 있습니다. Task는 서로에게 격리되어 있어 동시에 실행되는 것이 안전하지만 때로는 Task 간에 일부 정보를 공유해야 합니다. Actors를 사용하여 동시성 코드 간에 정보를 안전하게 공유할 수 있습니다.
클래스와 마찬가지로 actors는 참조타입입니다. 따라서 Classes Are Reference Type의 값 타입과 참조 타입의 비교는 클래스 뿐만이 아니라 액터에게도 적용됩니다.
클래스와 다른 점은, 액터는 한 번에 하나의 Task만 가변 상태(mutable state)에 접근할 수 있어서 여러 Task들의 코드가 하나의 액터 인스턴스와 상호작용하는 것이 안전합니다.
예를들어 온도를 기록하는 액터는 다음과 같습니다 :
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
}
}
actor 키워드를 사용하여 액터를 나타내고 대괄호 쌍으로 정의합니다.
TemperatureLogger 액터는 액터 외부의 다른 코드가 접근할 수 있는 프로퍼티를 가지며, 액터 내부의 코드만이 최댓 값을 업데이트할 수 있도록 max 프로퍼티를 제한합니다.
구조체 및 클래스와 동일한 이니셜라이저 구문을 사용하여 액터 인스턴스를 만듭니다. 액터의 프로퍼티 또는 메서드에 접글할 때 await를 사용하여 잠재적 중단점을 표시합니다. 예를 들어 다음과 같습니다:
let logger = TemperatureLogger(label: "Outdoors", measurement: 25)
print(await logger.max)
// Prints "25"
이 예시에서, logger.max에 접근하는 것이 가능한 중단점입니다.
액터는 한 번에 하나의 작업만 가변 상태에 접근할 수 있도록 허용하므로 다른 Task의 코드가 이미 logger와 상호 작용 중이라면 이 코드는 프로퍼티에 접근을 기다리는 동안 일시 중단됩니다.
반대로 액터 내부에서 프로퍼티에 접근할 때는 await 를 표기하지 않습니다.
다음은 TemperatureLogger를 새 온도로 업데이트하는 메서드 입니다 :
extension TemperatureLogger {
func update(with measurement: Int) {
measurements.append(measurement)
if measurement > max {
max = measurement
}
}
}
update(with:) 메서드는 이미 액터에서 실행 중이므로 max와 같은 프로퍼티에 대한 접근에 await를 표시하지 않습니다.
이 메서드는 액터가 한 번에 하나의 작업만 가변 상태와 상호 작용하도록 허용하는 이유 중 하나도 보여 줍니다 :
액터 상태의 일부 업데이트는 일시적으로 불변성(invariants)를 깨트립니다. TemperatureLogger 액터는 온도 리스트와 최대 온도를 추적하고, 새 측정을 추가할 때 최대 온도를 갱신합니다. 새 측정을 추가한 직후인 업데이트 도중, TemperatureLogger는 일시적으로 일관되지 않은(inconsistent) 상태가 됩니다.
여러 Task들이 동일한 인스턴스에 동시에 접근하지 못하도록 하면 아래 문제들이 방지됩니다.
- 당신의 코드가 update(with:) 메서드를 호출합니다. 먼저 measurments 배열을 갱신합니다.
- 당신의 코드가 max 를 갱신하기 전에 다른 곳의 코드가 최댓 값과 배열의 온도들을 읽습니다.
- 당신의 코드가 최댓 값을 변경하여 업데이트를 마칩니다.
이 경우 다른 곳에서 실행 중인 코드가 잘못된 정보를 읽게 되는데, 이는 데이터가 일시적으로 유효하지 않은 상태에서 액터에 대한 접근이 update(with:) 호출 도중 끼워졌기 때문입니다.
Swift 액터를 사용할 때 이 문제를 방지할 수 있는데, 그 이유는 상태에 대한 연산을 한 번에 하나만 허용하고 await가 표시된 중단점에서만 그 코드들이 중단(interrupt)될 수 있기 때문입니다.
update(with:)에는 중단점이 없기 때문에, 다른 코드들이 update 도중 데이터에 접근할 수 없게 됩니다.
만약 클래스의 인스턴스처럼 액터 외부에서 이 프로퍼티들에 접근하려 한다면, 컴파일 타임 에러가 발생할 것 입니다.
print(logger.max) // Error
await를 쓰지 않고 logger.max에 접근은 실패하게 됩니다. 이는 해당 액터의 프로퍼티들이 액터의 격리된 지역 상태(isolated local state)의 일부이기 때문입니다. Swift는 액터 내부의 코드만이 액터의 지역 상태에 접근할 수 있다고 보장합니다. 이를 actor isolation이라고 합니다.
Sendable Types
Task와 Actor는 안전하게 동시에 실행할 수 있도록 프로그램을 조각으로 나눌 수 있습니다.
Task나 Actor 인스턴스 안에서 변수나 프로퍼티들 같은 가변 상태를 포함하는 프로그램의 부분을 동시성 영역(Concurreny Domain)이라고 합니다.
동시성 영역 간에는 일부 종류의 데이터는 공유할 수 없습니다. 이는 데이터가 가변 상태를 포함하지만 중복되는 접근을 방지하지 못하기 때문입니다.
한 동시성 영역에서 다른 동시성 영역으로 공유할 수 있는 유형을 sendable type이라고 합니다.
예를들어 actor의 메서드를 호출할 때 인수로 전달하거나 task의 결과 값으로 반환할 수 있습니다. 지금까지는 동시성 영역 간 데이터 공유에 항상 안전한 값 타입(value type)만 사용했기에 전달 가능(sendability)에 대해 이야기 하지 않았습니다.
동시성 영역간의 전달이 안전하지 않은 타입이 존재합니다. 예를 들어 가변 프로퍼티를 포함하고 해당 프로퍼티에 대한 순차적 접근(serialize access)을 하지 않는 클래스는 서로 다른 Task 간에 인스턴스를 전달할 때 에측할 수 없는 잘못된 결과를 생성할 수 있습니다.
Sendable 프로토콜을 채택하도록 선언하여 전송 가능한 것으로 타입을 표시합니다.
해당 프로토콜에는 코드 요구 사항이 없지만 Swift가 적용하는 의미적 요구 사항(semantic requirements)이 있습니다. 다음은 전송할 수 있는 타입이 되기 위한 세 가지 일반적인 방법입니다 :
- 값 타입(value type)이며 가변 상태들이 다른 sendable 데이터로 구성 (예: sendable 프로퍼티로 이루어진 구조체 또는 sendable로 이루어진 관련 값이 있는 열거형)
- 가변 상태가 없고, 불변 상태들이 모두 sendable 데이터로 구성 (예: 읽기 전용 프로퍼티만 존재하는 구조체, 클래스)
- 가변 상태의 안전을 보장하는 코드를 가짐 (예: @MainActor로 표시된 클래스 또는 특정 쓰레드 또는 큐에서 프로퍼티들에 순차적 접근 하는 클래스)
의미적 요구사항에 대한 자세한 목록은 Sendable 를 참조하십시오.
sendable 프로퍼티만 있는 구조체나 sendable 연관 값만 있는 열거형과 같이 항상 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이 sendable 프로퍼티로만 갖는 구조체이며, public이나 @usableFromInline으로 표시되지 않았기 때문에 이는 암시적으로 sendable입니다. 아래와 같이 Sendable 프로토콜을 명시하지 않아도 됩니다.
struct TemperatureReading {
var measurement: Int
}
타입에 명시적으로 Sendable하지 않음을 표시하려면 extension을 사용하여 Sendable 프로토콜에 대한 암시적인 채택을 오버라이딩 합니다.
struct FileDescriptor {
let rawValue: CInt
}
@available(*, unavailable)
extension FileDescriptor: Sendable { }
위의 코드는 POSIX 파일 디스크립터 주변의 wrapper 일부를 보여줍니다.
파일 디스크립터용 인터페이스가 정수 값을 사용하여 열린 파일을 식별 및 상호 작용할 수 있고 Int는 sendable 하지만, 동시성 영역 간에 파일 디스크립터를 전송하는 것은 안전하지 않습니다.
NonsendableTemperatureReading는 암시적으로 Sendable을 충족하는 구조체입니다. 하지만 extension을 사용해 Sendable의 채택을 사용할 수 없게 만들어 sendable을 막을 수 있습니다.
'iOS > Concurrency' 카테고리의 다른 글
Concurrency (5) Intermediate async/await & Checked Continuation (0) | 2024.09.01 |
---|---|
Concurrency (4) Custom Asynchronous Sequences With AsyncStream (0) | 2024.08.24 |
Concurrency (3) AsyncSequence & Intermediate Task (0) | 2024.08.15 |
Concurrency (2) Getting started With async/await (0) | 2024.08.15 |
Concurrency (1) Why Modern Swift Concurrency? (0) | 2024.08.14 |