Swift/Swift 가이드

[Swift] 공식 문서 - 동시성(Concurrency)

유정주 2022. 7. 29. 12:43
반응형

새로 배운 점

  • 동시성이 필요한 코드에서 Swift 언어 수준에서 제공하는 동시성을 사용하면 Swift가 컴파일 타임에 문제를 잡는데 도움을 줄 수 있습니다.
  • Swift의 동시성 모델은 Thread 위에서 구축되었지만, 직접 상호작용 하지는 않습니다.
  • 비동기 메서드 내부에서 실행 흐름은 오직 다른 비동기 메서드를 호출할 때만 일시 정지되는데, 일시중지는 암시적이거나 우선적이지 않기 때문에 가능한 모든 일시중지 지점은 await로 표시됩니다.
  • await 키워드가 사용된 코드는 실행을 일시 중지할 수 있어야 하기 때문에, 프로그램의 특정 장소에서만 비동기 함수나 메서드를 호출할 수 있습니다.
    • 비동기 함수, 메서드, 프로퍼티의 본문에 있는 코드
    • @main으로 표시된 구조체, 클래스, 열거형의 static main() 메서드에 있는 코드
    • Unstructured Concurrency에서 볼 수 있듯이 detached child task에 있는 코드
  • 비동기 함수를 호출해 주변 코드와 병렬적으로 실행되도록 하려면, 상수를 정의할 때 let 앞에 async를 작성한 다음 상수를 사용할 때마다 await를 작성하면 됩니다.
  • 두 접근법의 차이점에 대해 고민하고, 적절하게 사용할 수 있는 방법은 다음과 같습니다.
    • 다음 줄의 코드가 해당 함수의 결과에 따라 달라지는 경우, await를 사용해 비동기 함수를 호출하세요. => 순차 수행
    • 다음 코드에서 결과가 필요하지 않을 때, async-let을 사용해 비동기 함수를 호출하세요. => 병렬 수행
    • await와 async-let 모두 일시 중지된 동안 다른 코드를 실행할 수 있게 합니다.
    • 두 경우 await를 사용해 모든 일시중지 가능 지점을 표시하여 필요한 경우 비동기 함수가 반환될 때까지 실행이 일시 중지됨을 의미합니다.
  • Task는 프로그램의 일부로 비동기로 실행될 수 있는 작업의 단위입니다.
  • 모든 비동기 코드는 여러 task의 일부로 실행됩니다.
  • 이전 단원에서 설명한 async-let 구문은 child task를 생성합니다.
  • 또한 task group을 생성하고 해당 그룹에 child tasks를 추가하여 우선순위 및 해제를 보다 세부적으로 제어할 수 있으며, 동적으로 여러 task를 생성할 수 있습니다.
  • 현재 actor에서 실행되는 구조화되지 않은 task를 생성하려면 Task.init(priority:operation:) 초기화 구문을 호출해야 합니다.
  • 현재 actor의 일부가 아닌 구조화되지 않은 task를 생성하려면 Task.detached(priority:operation:) 클래스 메서드를 호출합니다.
  • Actors와 Sendable에 관한 내용

 

Concurrency

Swift는 비동기와 병렬 코드를 작성하기 위해 구조화된 빌트인 방식을 지원합니다.

비동기적 코드는 한 번에 하나의 프로그램만 실행되지만 일시 중단된 후 나중에 재개될 수 있습니다.

프로그램을 일시 중단하고 다시 시작하면 UI 업데이트 같은 short-term 작업을 계속하면서

네트워크를 통해 데이터를 가져오거나 파일을 파싱 하는 것과 같은 long-running 작업을 진행할 수 있습니다.

병렬 코드는 여러 코드 조각이 동시에 실행됨을 의미합니다.

예를 들어, 쿼드코어 프로세서가 장착된 컴퓨터는 각 코어가 작업 중 하나를 수행하면서,

동시에 4개의 코드 조각을 실행할 수 있습니다.

병렬 및 비동기 코드를 사용하는 프로그램을 한 번에 여러 작업을 수행합니다.

이는 외부 시스템을 기다리는 작업을 일시 중지하고 memory-safe 한 코드를 쉽게 작성할 수 있게 합니다.

 

병렬 또는 비동기 코드에 의해 추가되는 스케줄링 유연성은 프로그램의 복잡성을 증가시킵니다.

Swift는 컴파일 타임에 확인 가능하게 하는 방식으로 의도한 코드를 표현하도록 해줍니다.

예를 들어, 가변적인 상태(mutable state)에 안전하게 접근하기 위해 actors를 사용할 수 있습니다.

하지만, 느리거나 버그가 있는 코드에 동시성을 추가하면서 빨라지거나 정확해질 것이라는 보장은 하지 않습니다.

오히려 동시성을 추가하면 코드를 디버깅하기 더 어려워질 수도 있습니다.

하지만 동시성이 필요한 코드에서 Swift 언어 수준에서 제공하는 동시성을 사용하면

Swift가 컴파일 타임에 문제를 잡는데 도움을 줄 수 있습니다.

 

이 문서의 동시성이라는 용어는 비동기 및 병렬 코드의 조합을 의미합니다.

이전에 동시성 코드를 작성한 적이 있다면, Thread로 작업하는데 익숙할 수 있습니다.
Swift의 동시성 모델은 Thread 위에서 구축되었지만, 직접 상호작용 하지는 않습니다.
Swift의 비동기 함수는 실행 중인 Thread를 포기하여 첫 번째 함수가 차단된 동안 해당 Thread에서 비동기 함수를 실행할 수 있습니다.

 

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

비동기 함수 또는 비동기 메서드는 실행을 통해 부분적으로 일시 중지될 수 있습니다.

이는 완료될 때까지 실행되거나, 오류가 발생하거나, 반환되지 않는 일반적인 동기 함수 및 메서드와는 차이가 있습니다.

비동기 함수 또는 메서드 역시 이러한 세 가지 작업 중 하나를 수행하지만, 무언가를 대기하는 동안 중간에 일시 정지할 수 있습니다.

비동기 함수 또는 메서드의 본문 내부에 실행이 일시 중지될 수 있는 위치를 표시합니다.

 

함수 또는 메서드가 비동기임을 나타내려면 해당 선언에서 매개변수 뒤에 async 키워드를 작성합니다.

함수 또는 메서드가 값을 반환하는 경우에는 -> 앞에 async를 작성합니다.

예를 들어 갤러리에서 사진 이름을 가져오는 방법은 다음과 같습니다.

func listPhotos(inGallery name: String) async -> [String] {
    let result = // ... some asynchronous networking code ...
    return result
}

두 키워드가 모두 필요한 함수 또는 메서드의 경우는 throws 전에 async를 작성합니다.

 

비동기 메서드를 호출하면 메서드가 반환될 때까지 실행이 일시 중지됩니다.

가능한 일시중지 지점을 표시하기 위해 해당 함수 또는 메서드 호출 지점 앞에 await를 작성합니다.

비동기 메서드 내부에서 실행 흐름은 오직 다른 비동기 메서드를 호출할 때만 일시 정지되는데,

일시중지는 암시적이거나 우선적이지 않기 때문에 가능한 모든 일시중지 지점은 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(inGallery:)와 downloadPhoto(named:) 모두 네트워크 요청을 해야 하기 때문에,

완료되기까지 상대적으로 오랜 시간이 걸릴 수 있습니다.

화살표(->) 전에 async를 적어 비동기적으로 만들면

이 코드가 사진 준비를 대기하는 동안 앱의 나머지 코드는 계속 실행됩니다.

 

위 예제의 동시성은 다음과 같은 실행 순서로 이루어집니다.

  1. 코드는 첫 번째 줄에서 실행되어 첫 번째 await까지 실행됩니다.
    listPhotos(inGallery:)를 호출하고 해당 함수의 반환을 기다리는 동안 실행을 일시 중지합니다.
  2. 코드의 실행이 일시 중지되는 동안, 같은 프로그램의 다른 동시성 코드가 실행됩니다.
    예를 들어, long-running 백그라운드 작업은 새로운 사진 갤러리 목록을 계속 업데이트할 수 있습니다.
    그 코드 또한 await로 표시된 다음 일시중지 지점 또는 작업 완료될 때까지 실행됩니다.
  3. listPhotos(inGallery:)가 반환된 후 이 코드는 해당 시점부터 실행을 계속합니다.
    반환된 값을 photoNames에 할당합니다.
  4. sortedNames와 names를 정의하는 줄은 규칙적이고 동기적입니다.
    이 줄은 await로 표시되지 않았으므로 일시 중지하지 않습니다.
  5. 다음 await는 downloadPhoto(named:) 호출에 있습니다.
    이 코드는 해당 함수가 반환될 때까지 다시 일시 중지하여 다른 동시성 코드에 실행 기회를 제공합니다.
  6. downloadPhoto(named:) 반환 후 photo에 값이 할당되고 show(:)를 호출할 때 인수로 전달됩니다.

await로 표시된 일시중지가 가능한 지점은 현재 코드가 비동기 함수 또는 메서드의 반환을 기다리는 동안

실행을 일시 중지할 수 있음을 나타냅니다.

이것을 Thread 양보(yielding the thread)라고 하는데,

Swift가 현재 Thread에서 코드 실행을 일시 중지하고 해당 Thread에서 다른 코드를 실행하기 때문입니다.

await 키워드가 사용된 코드는 실행을 일시 중지할 수 있어야 하기 때문에,

프로그램의 특정 장소에서만 비동기 함수나 메서드를 호출할 수 있습니다.

  • 비동기 함수, 메서드, 프로퍼티의 본문에 있는 코드
  • @main으로 표시된 구조체, 클래스, 열거형의 static main() 메서드에 있는 코드
  • detached child task에 있는 코드(Unstructured Concurrency 문단에서 다룸)

 

중단이 가능한 지점 사이의 코드는 다른 비동기 코드의 중단 가능성 없이 순차적으로 실행됩니다.

아래 코드는 한 갤러리의 사진을 다른 곳으로 이동시키는 예시입니다.

let firstPhoto = await listPhotos(inGallery: "Summer Vacation")[0]
add(firstPhoto toGallery: "Road Trip")
// At this point, firstPhoto is temporarily in both galleries.
remove(firstPhoto fromGallery: "Summer Vacation")

add(_:toGallery:)와 remove(_:fromGallery:) 사이에 실행되는 다른 코드는 없습니다.

그 시간 동안 첫 번째 사진은 양쪽 갤러리에 모두 나타나며 앱의 불변성 중 하나를 암시적으로 위반합니다.

이 코드에 await가 확실히 추가되지 말아야 한다는 것을 나타내기 위해

아래처럼 코드를 동기 함수로 리팩터링 할 수 있습니다.

func move(_ photoName: String, from source: String, to destination: String) {
    add(photoName, to: destination)
    remove(photoName, from: source)
}
// ...
let firstPhoto = await listPhotos(inGallery: "Summer Vacation")[0]
move(firstPhoto, from: "Summer Vacation", to: "Road Trip")

위의 예제에서 move(_:from:to:)는 동기 함수기 때문에

중단 가능한 지점을 포함하지 않는다는 것을 보장할 수 있습니다.

이 함수에 중단 가능한 지점을 도입하기 위해 비동기 코드를 추가하면

버그가 아닌 컴파일 에러가 발생합니다.

Task.sleep(until:clock:)는 동시성 작동 방식을 배우기 위해 간단한 코드를 작성할 때 유용합니다.
이 메서드는 아무런 동작도 하지 않지만 반환되기 전에 주어진 나노 단위의 초만큼 기다립니다.
다음은 네트워크 작업 대기를 시뮬레이션하기 위해 sleep(until:clock:)을 사용하는 listPhoto(inGallery:) 버전입니다.
func listPhotos(inGallery name: String) async throws -> [String] {
    try await Task.sleep(until: .now + .seconds(2), clock: .continuous)
    return ["IMG001", "IMG99", "IMG0404"]
}​

 

Asynchronous Sequences

이전 섹션에서 listPhoto(inGallery:)는 비동기적으로 배열의 모든 요소가 준비된 후에 전체 배열을 한 번에 반환합니다.

또 다른 접근 방식은 비동기 시퀀스(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 루프는 다음 요소를 사용할 수 있을 때까지 기다리고

각 반복이 시작될 때 잠재적으로 실행을 일시 중단합니다.

 

Sequence 프로토콜에 준수성을 추가하여 for-in 루프에서 자체 타입을 사용할 수 있는 것과 같은 방식으로

AsyncSequence 프로토콜에 준수성을 추가하여 for-await-in 루프에서 자체 타입을 사용할 수 있습니다.

 

Calling Asynchronous Functions in Parallel

await를 사용하여 비동기 함수를 호출하면 코드는 한 번에 한 부분만 실행됩니다.

비동기 코드가 실행되는 동안 호출자는 다음 코드 줄을 실행하기 위해 이동하기 전에

해당 코드가 끝나기를 기다립니다.

예를 들어, 갤러리에서 처음 3장의 사진을 가져오려면 다음과 같이 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)

이 예제에서 downloadPhoto(named:)를 호출하는 세 가지 상수는 이전 코드가 완료되길 기다리지 않고

사용할 수 있는 시스템 자원이 충분하면 동시에 실행할 수 있습니다.

이러한 함수 호출은 함수의 결과를 기다리기 위해 일시 중단되지 않기 때문에 await를 표시하지 않습니다.

대신 photos가 정의된 줄까지 실행이 됩니다.

이 시점에서 프로그램은 비동기 호출의 결과를 필요로 하므로 await를 작성해 세 사진의 다운로드가 완료될 때까지

실행을 일시 중지합니다.

 

두 접근법의 차이점에 대해 고민하고, 적절하게 사용할 수 있는 방법은 다음과 같습니다.

  • 다음 줄의 코드가 해당 함수의 결과에 따라 달라지는 경우, await를 사용해 비동기 함수를 호출하세요. => 순차 수행
  • 다음 코드에서 결과가 필요하지 않을 때, async-let을 사용해 비동기 함수를 호출하세요. => 병렬 수행
  • await와 async-let 모두 일시 중지된 동안 다른 코드를 실행할 수 있게 합니다.
  • 두 경우 await를 사용해 모든 일시중지 가능 지점을 표시하여 필요한 경우 비동기 함수가 반환될 때까지 실행이 일시 중지됨을 의미합니다.

또한 같은 코드에 두 가지 방법을 혼합해서 쓸 수 있습니다.

 

Tasks and Task Groups

Task는 프로그램의 일부로 비동기로 실행될 수 있는 작업의 단위입니다.

모든 비동기 코드는 여러 task의 일부로 실행됩니다.

이전 단원에서 설명한 async-let 구문은 child task를 생성합니다.

또한 task group을 생성하고 해당 그룹에 child tasks를 추가하여 우선순위 및 해제를 보다 세부적으로 제어할 수 있으며,

동적으로 여러 task를 생성할 수 있습니다.

 

task는 계층 구조로 정렬됩니다.

task group의 각 task에는 동일한 parent task가 있으며,

각 task는 child task를 가질 수 있습니다.

task와 task group 간의 명시적인 관계 때문에 이 접근법은 구조화된 동시성(structured concurrency)라고 합니다.

정확성에 대한 일부 책임은 개발자에게 있지만,

parent-child 관계 덕분에 Swift는 해제 전파(propagating cancellation)와 같은 일부 동작을 처리하고,

컴파일 타임에 일부 오류를 감지할 수 있습니다.

await withTaskGroup(of: Data.self) { taskGroup in
    let photoNames = await listPhotos(inGallery: "Summer Vacation")
    for name in photoNames {
        taskGroup.addTask { await downloadPhoto(named: name) }
    }
}

task groups에 대한 자세한 내용은 TaskGroup를 확인하세요.

 

Unstructured Concurrency

방금 알아본 동시성에 대한 구조화된 접근법 외에도 Swift는 구조화되지 않은 동시성을 지원합니다.

Task group의 일부인 tasks와 달리 구조화되지 않은 task에는 parent task가 없습니다.

프로그램에 필요한 방식으로 구조화되지 않은 task를 관리할 수 있도록 완전한 유연성을 갖추고 있지만,

정확성에 대한 책임은 전부 개발자에게 있습니다.

 

현재 actor에서 실행되는 구조화되지 않은 task를 생성하려면 Task.init(priority:operation:) 초기화 구문을 호출해야 합니다.

현재 actor의 일부가 아닌 구조화되지 않은 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

 

detached tasks 관리에 대한 자세한 내용은 Task를 참고 바랍니다.

 

Task Cancellation

Swift 동시성은 협동 해제 모델(cooperative cancellation model)을 사용합니다.

각 task는 실행 중 적절한 지점에서 해제 여부를 확인하고, 적절한 방법으로 해제해야 합니다.

수행 중인 작업에 따라 일반적으로 다음 중 하나를 합니다.

  • CancellationError와 같은 오류 발생
  • nil 또는 빈 컬렉션 반환
  • 부분적으로 완료된 작업 반환

task가 해제된 경우 CancellationError를 발생시키는 Task.checkCancellation()을 호출하거나

Task.isCancelled 값을 확인하고 코드에서 직접 해제를 처리합니다.

예를 들어 갤러리에서 사진을 다운로드하는 task는 일부 다운로드를 삭제하고 네트워크 연결을 닫아야 할 수 있습니다.

해제를 수동으로 하려면 Task.cancel()을 호출해야 합니다.

 

Actors

클래스와 마찬가지로 actors는 참조 타입이기 때문에, 참조 타입의 특징은 actors에게도 적용됩니다.

클래스와 달리 actors는 한 번에 하나의 task만 접근할 수 있도록 허용하므로,

여러 tasks의 코드가 동일한 인스턴스와 상호작용하는 것을 안전하게 만들어줍니다.

예를 들어, 다음 코드는 온도를 기록하는 actor입니다.

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 키워드와 함께 한 쌍의 중괄호로 정의된 actor를 소개합니다.

TemperatureLogger actor에는 actor 외부의 다른 코드가 접근할 수 있는 프로퍼티가 있으며

actor 내부의 코드만 접근할 수 있는 max 프로퍼티가 있습니다.

 

구조체, 클래스와 동일한 이니셜라이저 구문을 사용해 actor의 인스턴스를 생성합니다.

actor의 프로퍼티 또는 메서드에 접근할 때 await를 사용해 일시 중지가 발생할 수 있다는 것을 표시합니다.

let logger = TemperatureLogger(label: "Outdoors", measurement: 25)
print(await logger.max)
// Prints "25"

이 예제에서 logger.max에 접근하는 것은 일시중지 시점이 될 수 있습니다.

actor는 가변적인 상태에 접근하기 위해 한 번에 하나의 task만 허용하기 때문에,

다른 task의 코드가 이미 logger와 상호작용하고 있다면

이 코드는 프로퍼티에 접근하기 위해 대기하는 동안 일시 중지됩니다.

 

이와 반대로 actor의 일부인 코드는 actor의 프로퍼티에 접근할 때 await를 작성하지 않습니다.

예를 들어, 다음 코드는 새로운 온도로 TemperatureLogger를 업데이트하는 메서드입니다.

extension TemperatureLogger {
    func update(with measurement: Int) {
        measurements.append(measurement)
        if measurement > max {
            max = measurement
        }
    }
}

update(with:) 메서드는 actor 내부에 있으므로 max 프로퍼티에 접근할 때 await 표시를 하지 않습니다.

이 메서드는 actor가 가변적인 상태와 상호작용하기 위해 한 번에 하나의 작업만 허용하는 이유를 보여줍니다.

acotr의 상태에 대한 일부 업데이트는 일시적으로 불변성을 깨뜨립니다.

TemperatureLogger actor는 온도 목록과 최대 온도를 추적하고

새로운 측정값을 기록할 때 최대 온도를 업데이트합니다.

업데이트 도중 새로운 측정값을 추가한 후 max를 업데이트하기 전에

TemperatureLogger는 일시적으로 불일치(inconsistent) 상태가 됩니다.

여러 task가 동일한 인스턴스에 상호 작용하는 것을 방지하는 것은 다음과 같은 문제를 예방합니다.

  1. 코드에서 update(with:) 메서드를 호출하여 mesaurements 배열을 먼저 초기화합니다.
  2. max를 업데이트하기 전에, 다른 코드가 max 값과 mesaurements 배열을 읽습니다.
  3. 코드는 max를 변경하여 업데이트를 완료합니다.

이 경우 다른 곳에서 실행되는 코드는 max 값이 업데이트되기 전에 max를 읽어 잘못된 정보를 가집니다.

Swift actors를 사용할 때는 상태에 대해 한 번에 하나의 task만 허용하고,

await가 표시하는 위치에서만 코드가 일시 중지될 수 있기 때문에

이런 문제를 예방할 수 있습니다.

 

만약 클래스의 인스턴스를 사용하는 것처럼, actor 외부에서 이러한 프로퍼티에 접근하려고 하면

컴파일 에러가 발생합니다.

print(logger.max)  // Error

actor의 프로퍼티가 해당 actor의 격리된(isolated) 로컬 상태의 일부이기 때문에

await 작성 없이 logger.max에 접근할 수 없습니다.

Swift는 actor 내부의 코드만 actor의 로컬 상태에 접근할 수 있도록 보장합니다.

이를 actor isolation이라고 합니다.

 

Sendable Types

Tasks와 actors를 이용해 프로그램을 동시에 안전하게 실행할 수 있는 조각으로 나눌 수 있습니다.

tasks 또는 actors의 인스턴스 내에서 변수와 프로퍼티와 같은 변경 가능한 상태(mutable state)를 포함하는

프로그램의 일부분을 동시성 도메인(concurrency domain)이라고 부릅니다.

어떤 데이터는 데이터가 mutable 상태를 포함하지만 동시 접근에 대해 보호되지 않으므로

동시성 도메인 간에 공유될 수 없습니다.

 

한 동시성 도메인에서 다른 동시성 도메인으로 공유될 수 있는 타입을

Sendable 타입이라고 합니다.

예를 들어, actor 메서드로 호출될 때 인자로 전달되거나 task의 결과로 반환될 수 있습니다.

이 챕터의 앞부분에 있는 예제들은 항상 안전한 간단한 값 타입을 사용하기 때문에 Sendable 합니다.

반대로 일부 타입은 동시성 도메인 간에 전달하는 것이 안전하지 않습니다.

예를 들어, mutable 프로퍼티를 포함하고 해당 프로퍼티에 순차적으로 접근하지 않는 클래스는

서로 다른 task 인스턴스에 전달될 때 결과를 예상할 수 없고 잘못된 결과를 생성할 수 있습니다.

 

Sendable 프로토콜을 선언하여 Sendable 타입으로 표시합니다.

이 프로토콜은 어떠한 코드 요구사항도 없지만

Swift가 적용하는 의미론적 요구사항이 있습니다.

일반적으로 타입을 Sendable로 나타내기 위한 세 가지 방법이 있습니다.

  • 값 타입이고 mutable 상태는 다른 Sendable 데이터로 구성됩니다. 예를 들어, 전송 가능한 저장 프로퍼티가 있는 구조체 또는 Sendable 값을 가진 열거형이 있습니다.
  • mutable 상태가 없으며 immutable 한 다른 Sendable 데이터로 구성됩니다. 예를 들어 read-only 프로퍼티만 있는 구조체 또는 클래스가 있습니다.
  • @MainActor로 표시된 클래스나 특정 Thread나 Queue에서 프로퍼티에 순차적으로 접근하는 클래스는 mutable 상태의 안정성을 보장합니다.

의미론적 요구사항의 자세한 리스트는 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
}

 

 

 

참고

https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html

 

 

반응형