 Apple Lover Developer & Artist

영속적인 디자인에 현대의 공감을 채워넣는 공방입니다

 Apple/Swift Programming Language

[Swift] 공식문서 씹어먹기: Concurrency

singularis7 2023. 3. 18. 15:22
반응형

Overview

스위프트에는 비동기 및 병렬 코드를 구조화된 방식으로 코딩할 수 있는 기능이 있다. 한번에 프로그램의 오직 한 부분만 실행할 수 있음에도 비동기 코드는 중단(suspend) 되거나 재개(resume)될 수 있다. 비동기 코드가 중단되거나 재개될 수 있다는 부분은 네트워크 통신과 파일 탐색과 같은 시간이 오래 소요되는 연산을 지속하며 UI 업데이트와 같이 시간이 짧게 소요되는 연산을 지속할 수 있도록 한다. 병렬 코드는 여러 조각의 코드가 동시에 실행됨을 의미한다. 가령 4개 코어 프로세서를 갖고 있는 컴퓨터는 4부분의 코드를 동시에 실행시킬 수 있다. 이 과정에서 각 코어는 하나의 작업을 처리하게 된다. 중단된 연산은 외부 시스템에서 대기하며 이런 방식의 코드를 memory-safe 방식으로 손쉽게 작성할 수 있다.

병렬 및 비동기 코드가 더하는 유연한 스케줄링은 복잡성을 높이는 비용을 초래한다. Swift는 사용자의 의도를 컴파일 시점의 확인 기법을 통해 표현할 수 있도록 돕는다. 가령, 사용자는 액터(Actor)를 활용해 mutable 상태에 안전하게 접근할 수 있다. 하지만 느리고 버그가 있는 코드에 동시성을 더하는 것은 빠르거나 정확하다는 점과 직결되진 않는다. 사실, 동시성을 더한다는 것은 코드를 디버그하기 어려워짐을 의미하기도 한다. 그러나, Swift의 언어 레벨의 동시성 지원 기능을 사용하는 것은 Swift가 컴파일 시점에서 발생될 문제를 찾는데 도움줄 수 있다는 점이다.

이 챕터의 나머지에 사용되는 용어 "concurrency"는 비동기 코드와 병렬 코드의 일반적인 조합을 지칭한다.


확인할 점이 있다. (역자도 계속 이해가 잘 안되고 있는 부분이다)

만약 사용자가 이전에 동시성 코드를 작성해보았다면 threads를 활용했을 것이다. Swift의 동시성 모델은 threads 위에서 동작하지만 사용자는 스레드와 직접 상호작용할 수 없다. Swift에서의 비동기 함수는 진행(running)중인 thread를 포기할 수 있다. 이를 통해 다른 비동기 함수가 해당 thread에서 실행될 수 있도록 처리되며 기존의 함수는 블락(Blocked)된다. 비동기 코드가 재개되면 Swift는 해당 함수가 어떤 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의 순서로 정리되어 작성되었기 때문이다. 사용자는 결국 중첩된 클로져(closure)를 활용해 코딩해야 한다. 이런 스타일은 중첩 과정이 깊어질수록 더 복잡해지고 다루기 어려워 진다.

Defining and Calling Asynchronous Functions

비동기 함수 혹은 비동기 메서드는 함수나 메서드의 일종인데 실행도중에 중단(suspended)될 수 있다. 이 점은 보통의 동기적 코드나 메서드가 완료될 때까지 실행되고 에러를 던지고, 반환해야한다는 점에서 대조적이다. 비동기 함수나 메서드는 위 3가지 경우 중 하나를 하다가 중단될 수 있다. 무언가를 위해 대기하고 있는 것이다. 비동기 코드나 메서드의 구현 몸체에 사용자는 실행이 중단될 수 있는 지점을 표시할 수 있다.

함수나 메서드가 비동기적임을 지칭하기 위해 사용자는 async 키워드를 선언할 수 있다. 에러 던질 때 사용하는 throw 키워드를 적는 것과 유사하게 함수의 파라미터 옆에 선언해주면 된다. 만약 함수나 메서드가 값을 반환한다면, 사용자는 async 키워드 옆에 반환 화살표 -> 를 코딩해주면 된다. 가령, 다음의 예시를 참조해볼 수 있다.

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

함수나 메서드 모두 비동기적으로 실행되고 에러를 던질 수 있다. 이 때에는 async 전에 throws를 적어줘야 한다.

비동기 코드를 호출할 때에는 메서드가 반환될 때까지 실행이 중단된다. 사용자는 호출의 전두에 await을 적어주어 중단 지점을 표시할 수 있다. 이것은 마치 오류를 던지는 함수를 호출할 때 try를 적어주어 에러 핸들링 처리해주는 것과 유사하다. 비동기 코드 내부에서 실행 흐름이 중단될 수 있는 부분은 오직 다은 비동기 코드를 호출했을 때 뿐이다. 중단(suspension)은 절때 암묵적이거나 선택적(preemptive)이지 않다. 이점은 모든 중단부 앞에 await를 표시해줘야 함을 의미한다. 다음의 코드를 살펴보자

let photoNames = await listPhotos(inGallery: "Summer Vacation")
let sortedNames = photoNames.sorted()
let name = sortedNames[0]
let photo = await downloadPhoto(named: name)
show(photo)

listPhoto 및 downloadPhoto 함수가 네트워크 요청을 사용하기 때문에 완료전까지 상대적으로 긴 시간이 소요된다. 둘다 async를 코딩해 비동기적으로 만들어주어 사진이 준비되는 동안 대기하며 앱의 나머지 코드가 실행될 수 있도록 처리할 수 있다.

동시성 환경을 이해하기 위해 제공한 위 예시에서 가능한 실행 순서는 다음과 같다.

  1. 첫번째 줄의 코드가 실행된 후 첫번째 await 를 만난다. 이 때 listPhoto 함수를 호출한 뒤 이 함수의 반환값을 받기 전까지 현재 함수는 중단(suspend) 된다.
  2. 현재 코드의 실행이 중단되는 동안 다은 동시성 코드는 같은 프로그램 내에서 처리되고 있다. 가령, 시간이 오래 소요되는 background 작업은 지속하여 새로운 사진첩 명단을 갱신한다. 이 코드는 다음 중단 지점까지 실행되거나 완료된다.
  3. listPhoto 함수가 반환되면 위 코드의 중단지점으로 다시 돌아와 지속하여 실행된다. 이 때 변수에 반환값이 저장된다.
  4. sortedNames와 name 같은 보통의 동기적 코드가 선언되어있다. 여기선 별도의 await 표기가 없기 때문이다. 즉, 중단 지점이 존재하지 않는다.
  5. 다음으로 await 지점을 만나게 되었다. downloadPhoto 함수가 해당된다. 이 코드는 해당 함수가 반환될 때까지 실행이 중단되며 이 과정에서 다른 동시성 코드가 실행될 기회를 얻게 된다.
  6. downloadPhoto 함수가 반환되면 반환값을 주어진 변수에 할당하고 show 함수의 파라미터에 반환값을 전달하며 마무리된다.

코드에서 가능한 중단 지점은 await로 표시된 지점이며 이는 현재 코드 조각의 실행이 다른 함수나 메서드가 반환될 때까지 중단될 수 있음을 의미한다. 이런 개념을 yielding the thread라고 부른다. 왜냐면 무대 뒤에서 Swift가 현재 thread에서 사용자의 코드 실행을 중단하고 해당 thread에서 다른 코드를 실행하기 때문이다. 왜냐하면 await 코드는 실행의 중단이 가능해야 하기에 프로그램의 특정 지검에서만 비동기 함수나 메서드를 호출할 수 있다.

  • 비동기 함수, 메서드, 프로퍼티의 몸체 내부의 코드
  • @main 표시된 구조체, 클래스, 열거 타입의 static main() 메서드 내부의 코드
  • unstructured child task의 내부 (아래에서 소개된다)

코드의 중단 가능 지점은 순차적으로 실행된다. 여기서 다른 동시성 코드로 부터 방해(interruption) 받을 가능성은 없다. 가령, 다음의 코드를 살펴보자

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 와 remove 사이에서 다른 코드가 실행될 수 있는 방법은 없다. 이 시점 동안 첫번째 사진은 두 갤러리에서 보여서 임시적으로 앱의 불변성(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(firstPhotom from: "Summer Vacation", to: "Road Trip")

위ㅣ 예시에서 move 함수가 동기적이기 때문에 사용자는 중단 가능 지점이 발생하지 않도록 보장할 수 있다. 미해에 만일 사용자가 이 함수에 동시성 코드를 추가하고자 한다면, 버그 대신 컴파일 시점의 오류를 보게 된다.


확인할 점이 있다.

Task.sleep(until:tolerance:clock:) 메서드는 동시성이 어떻게 동작하는지 파악하는데 유용하다. 이 메서드는 아무것도 하지 않는다. 대신 주어진 숫자만큼의 나노초 동안 대기 후 반환한다. 다음은 네트워크 연산을 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

이전 섹션에서 본 listPhoto 함수는 전체 배열을 비동기적으로 한번에 반환하였다. 결과적으로 배열의 모든 요소들은 준비된다. 다른 접근법도 있는데 asynchronous sequence를 활용해 컬렉션의 한 요소를 한번에 하나씩 기다리는 것이다. 다음은 asynchronous sequence를 통해 반복(iterating) 하는 코드의 예이다.

import Foundation

let handle = FileHandle.standardInput
for try await line in handle.bytes.lines {
  print(line)
}

보통의 for-in 루프를 사용하는 것 대신, 위 예시는 for과 함께 await를 사용하였다. 비동기 함수나 메서드를 호출하는 것과 유사하게 await 를 적는 것은 중단 가능 지점을 의미한다. for-await-in 루프는 잠재적으로 각 반복(iteration)의 시작부에서 실행이 중단될 수 있으며 다음의 element가 준비될 때까지 대기하게 된다.

같은 방식으로 사용자 지정 타입에도 for-in 루프를 사용할 수 있다. 방법은 Sequence 프로토콜을 채택하는 것이다. 사용자 지정 타입에 for-await-in 루프를 사용할 수 있는데, 방법은 AsyncSequence 프로토콜을 채택하는 것이다.

Calling Asynchronous Functions in Parallel

await를 활용해 비동기 함수를 호출하는 것은 한번에 한조각의 코드를 실행한다. 비동기 코드가 실행되는 동안 호출자는 다음의 코드를 실행하기 전에 기존의 코드가 완료될 때까지 대기해야 한다. 가령 다음의 코드를 살펴보자.

let firstPhoto = await downloadPhoto(named: photoNames[0])
let secondPhto = await downloadPhoto(named: photoNames[1])
let thirdPhoto = await downloadPhoto(named: photoNames[2])

let photos = [firstPhoto, secondPhoto, thirdPhoto]
show(photos)

이 접근 방식은 중요한 문제점이 있다. 다운로드 과정이 비동기적임에도 불구하고 한번에 오직 하나의 download 만 처리할 수 있다는 점이다. 각 사진 다운로드 기능은 이전의 다운로드가 완료되어야지만 시작될 수 있다. 하지만 생각해보면 기다릴 필요가 없다는 점이다. 각 사진은 독립적으로 혹은 동시에 다운로드 될 수 있다.

비동기 코드를 병렬적으로 실행될 수 있도록 호출하기 위해 코드의 전두에 async 를 적어준다. 상수를 사용한다면 let 키워드 앞에 써준다. 마무리로 상수를 사용하는 지점의 코드 앞에 await 를 적어준다.

async let firstPhoto = await downloadPhoto(named: photoNames[0])
async let secondPhto = await downloadPhoto(named: photoNames[1])
async let thirdPhoto = await downloadPhoto(named: photoNames[2])

let photos = await [firstPhoto, secondPhoto, thirdPhoto]
show(photos)

위의 예시에서 3개의 다운로드 함수 호출은 이전 과정이 완료되는 것을 기다리지 않고 실행된다. 만약 충분한 시스템 자원이 있다면, 동시에 실행될 수 있다. 어떠한 함수 호출도 await 표시가 되어있지 않다. 왜냐면 함수의 결과를 받기 위해 대기하는 등 중단될 필요가 없기 때문이다. 대신에 실행 흐름은 photos가 선언된 부분까지 쭉 진행한다. 이 지점은 프로그램이 비동기 코드의 결과를 알 필요가 있는 지점이다. 그래서 사용자는 await 를 입력하여 모든 다운로드가 완료될 때까지 실행을 중단시킨다.

다음을 통해 두가지 접근법이 어떤 차이를 갖고 있는지 생각해볼 수 있는지 알수 있다.

  • await를 활용해 비동기 코드를 호출할 때는 다음의 라인이 함수의 결과에 의존하는 경우다. 이것은 순차적인 작업의 수행을 의미한다.
  • async-let을 활용해 비동기 코드를 호출할 때는 코드 직후 결과값이 필요하지 않는 경우다. 이것은 병렬적인 작업의 수행을 의미한다.
  • await와 async-let 모두 그들이 중단될 때 다른 코드가 실행될 수 있도록 허락한다.
  • 두 경우, 필요하다면 비동기 함수가 반환될 때까지도 사용자는 중단 가능한 지점에 await 표기를 하여 실행이 중단될 수 있음을 지칭할 수 있다.

사용자는 이들 접근법 모두 같은 코드에서 혼합하여 사용할 수 있다.

Tasks and Task Groups

task 는 작업의 단위로 프로그램의 일부를 비동기적으로 실행할 수 있도록 한다. 모든 비동기 코드는 task의 일부로 실행된다. 이전 섹션의 async-let 문법은 자식 task를 생성하는 과정을 묘사한다. 사용자는 task group을 생성하여 해당 그룹에 자식 task를 추가할 수 있다. 이 과정을 통해 priority와 cancellation을 제어하고 동적으로 여러 task를 생성할 수 있다.

task는 계층적으로 나열되어있다. task group 내부의 개별 task는 같은 부모 task를 갖고 있다. 그리고 각 task는 자식 task를 갖는다. 왜냐면 task와 task group의 명시적인 관계 때문이다. 이런 접근법은 structured concurrency 라고 부른다. 비록 사용자가 정확성의 책임을 갖고 있음에도, task 간의 명시적인 부모-자식 관계는 swift가 일부 행위를 전파하는 것을 돕는다. 예를 들어 취소 기능을 전파하는 것이 있다. 그리고 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) }
  }
}

Unstructured Concurrency

동시성의 구조적 접근(structured approaches)에 더해 Swift는 비구조적 동시성(unstructured concurrency)도 지원한다. task group을 구성하는 task들과 다르게 unstructured task 부모 task를 갖고 있지 않다. 이를 사용하여 완전한 유연함을 가질 수 있다. 그러나 여전히 사용자는 정확성에 관한 책임을 지닌다. 비구조적 동시성을 현재 actor에서 생성하기 위해 Task.init(priority:operation:)생성자를 활용한다. 현재 actor의 부분이 아닌 unstructured task를 생성하기 위해 detached task를 이해할 필요가 있다. 이를 호출하는 방법은 Task.detached(priority:operation:) 클래스 메서드를 활용하는 것이다. 두 연산 모두 상호작용할 수 있는 task를 반환한다. 가령, 결과를 기다리거나 자신을 취소할 수 있다.

let newPhoto = // ... some photo data ...
let handle = Task {
  return await add(newPhoto, to GalleryNamed: "Spring Adventures")
}
let result = await handle.value

Task Cancellation

Swift 동시성은 협력적인 취소(cancellation) 모델을 사용한다. 각 task는 실행의 실행 과정의 적합한 지점에서 종료될 수 있는지 확인한다. 적절하다면 취소에 응답한다. 사용자의 작업에 의존하여 보통 다음중 하나를 의미한다.

  • Cancellation Error와 같은 에러를 던진다.
  • nil 혹은 빈 컬렉션을 반환한다.
  • 부분적으로 완료된 작업을 반환한다.

취소(cancellation)를 확인하기 위해 Task.checkCancellation() 을 호출할 수 있다. 혹은 Task.isCancelled 값을 확인해볼 수 있다. 가령, 사진첩에 사진을 다운로드하는 것은 부분적인 다운로드를 지우고 네트워크 연결을 닫을 필요가 있다. 수동으로 취소를 전파하기 위해 Task.cancel() 를 활용할 수 있다.

Actors

사용자는 task를 프로그램에 고립된(isolated) 동시실행 조각(concurrent pieces)으로 나눌 수 있다. 작업들은 서로로 부터 고립되어있다. 즉, 동시에 실행될 때 서로로 부터 안전하도록 만든다는 의미이다. 하지만 때로는 작업간에 정보를 교환해야될 수 있다. Actor은 동시성 코드간에 안전하게 정보를 교류할 수 있도록 돕는다.

class와 같이 actor는 참조 타입(reference type)이다. 그래서 class와 마찬가지로 actor도 값과 참조 타입을 비교하는 것이 적용된다. 클래스와 다르게 actor는 한번에 오직 한 task만 actor의 mutable state에 접근할 수 있따. 즉, actor의 동일한 인스턴스에 여러개의 task가 상호작용할 때 코드를 더욱 안전하게 만들 수 있다. 다음의 예시를 살펴보자

actor TemperatureLogger {
  let label: String
  var measurements: [Int]
  private(set) var max: Int

  init(label: String, measurement: Int) {
    self.label = label
    self.measurement = [measurement]
    self.max = measurement
  }
}

actor 키워드를 통해 actor를 소개했다. 괄호안에 정의가 담겼다. TemperatureLogger actor는 프로퍼티를 갖고 있는데 actor 외부의 다른 코드가 접근할 수 있다. max 프로퍼티의 경우 오직 actor 내부에서만 최대값을 갱신할 수 있도록 제약이 걸려있다.

사용자는 actor의 인스턴스를 구조체나 클래스와 동일한 생성자 문법을 활용해 생성할 수 있다. 사용자가 액터의 프로퍼티 혹은 메서드에 접근할 때, 잠재적인 중단 지점에 await 를 표기해야한다. 예시는 다음과 같다.

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

위 예시에서 logger.max에 접근하는 것은 가능한 중단 지점이다. 왜냐면 actor는 한번에 오직 하나의 task 만 mutable state에 접근할 수 있도록 처리하기 때문이다. 만약 logger에 다른 코드가 이미 접근하고 있다면 현재 코드는 프로퍼티를 사용할 수 있을 때까지 중단(suspend)된다.

그에 반해, 액터의 일부인 코드를 작성하면서 actor의 프로퍼티에 접근할 때 await를 사용하지 않는 경우가 있다. 예시는 다음과 같다.

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

위 update 메서드는 actor에서 이미 실행중이다. 그래서 max 프로퍼티에 접근하기 위해 await 를 걸어줄 필요가 없다. 이 메서드는 왜 액터가 그들의 mutable state에 상호작용할 때 한번에 한 task씩만 가능하도록 했는지 이유를 보여준다. 일부 update는 일시적으로 액터의 불변성을 깬다. TemperatureLogger actor도 temperature 명단과 최대 temperature을 추적한다. 그리고 새로운 기록이 생길 때마다 최대 온도를 갱신한다. 갱신의 중간 과정에서 새로운 measurement가 추가된 직후이자 최대값이 갱신되기 전에 temperature logger는 임시적으로 모순되는(inconsistent) state를 갖는다. 여러 task가 동시에 같은 인스턴스에 상호작용하는 것을 막아 다음의 문제를 예방한다.

  1. 사용자의 코드가 update 메서드를 호출했다. 이는 measurements 배열을 갱신한다.
  2. 사용자의 코드가 최대값을 갱신하기 전에 다른 코드가 최대값을 읽어간다.
  3. 당신의 코드는 최대값을 변경해 갱신 작업을 완료한다.

이 경우, 다른 부분에서 동작하는 코드는 잘못된 정보를 읽어간다. 왜냐면 데이터가 옳지 않은 업데이트 중간 과정에서 액터에 접근했기 때문이다. Swift actor를 사용하면 이 문제를 예방할 수 있다. 왜냐면 코드는 오직 await 표시가 있는 곳에서만 중지되기 때문이다. update의 어디서도 중단 지점을 포함하지 않기 때문에 다른 코드는 갱신 과정의 중간에 개입할 수 없다.

만약 사용자가 액터 밖에서 이들 프로퍼티에 접근하고자 한다면, 가령 클래스의 인스턴스와 같이, 사용자는 컴파일 시점의 에러를 보게 된다. 다음의 예시를 보자.

print(logger.max) // Error

await 없이 logger.max에 접근하는 것은 실패한다. 왜냐면 액터의 프로퍼티는 액터의 고립된 로컬 상태이기 때문이다. Swift는 오직 액터 내부의 코드만 액터의 로컬 상태에 접근할 수 있도록 보장한다. 이런 보장은 actor isolation으로 부른다.

Sendable Types

Task와 actors는 사용자가 프로그램을 조각으로 나누어 동시에 안전하게 구동할 수 있도록 한다. task 혹은 actor의 내부에서 부분의 프로그램은 mutable state를 갖고 있다. 가령 변수와 프로퍼티를 생각해볼 수 있는데 이를 concurrency domain 이라고 부른다. 일부 종류의 데이터는 동시성 도메인간에 공유할 수 없다. 왜냐면 mutable state를 포함하기 때문이다. 그러나 중첩된 접근에 저항해 보호하지는 않는다.

하나의 concurrency domain부터 다른 것까지 공유될 수 있는 타입이 있다. sendable 타입으로 알려진다. 예를 들어, 이 타입은 액터 메서드의 파라미터로 전달되거나 task의 결과로 반환될 수 있다. 이장의 앞부분에 있는 예시는 동시성 도메인 간에 전달되는 데이터에 대해 항상 안정하게 공유할 수 있는 간단한 값 타입을 사용하기 때문에 sendable에 관하여 논의하지 않았다. 반면, 일부 타입은 동시성 도메인을 통과하는 것이 안전하지 않다. 예를 들어 mutable state를 포함하고 해당 속성에 대한 접근을 직렬화하지 않는 클래스는 다른 작업 간에 해당 클래스의 인스턴스를 전달할 때 예측할 수 없으며 잘못된 결과를 도출할 수 있다.

sendable 프로토콜을 충족하도록 선언함으로써 사용자의 타입이 sendable 하도록 만들 수 있다. 이 프로토콜은 어떠한 요구사항도 갖고있지 않다. 하지만 Swift가 강제하는 문법적 요구사항이 있다. 보통 3가지 방식의 sendable 타입이 있다.

  • 이 타입은 값타입이며 변경 가능한 상태는 다른 전송 가능한 데이터로 구성되었다. 예를 들어 전송 가능한 저장된 프로퍼티가 있는 구조체 혹은 전송 가능한 관련 값이 있는 열거형이다.
  • 이 타입은 변경 가능한 상태가 없으며, 불변 상태는 다른 전송 가능한 데이터(예: 읽기 전용 속성만 있는 구조 또는 클래스)로 구성되어있다.
  • 이 타입은 @Main 표시된 클래스 혹은 특정 스레드 혹은 대기열에서 프로퍼티에 대한 접근을 직렬화하는 클래스와 같이 변경 가능한 상태의 안전을 보장하는 코드가 있다.

자세한 문법적 사항은 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
}

마무리

Swift 언어 기능을 활용한 동시성 병렬성을 활용할 수 있게 되었다. Task와 TaskGroup을 활용해 계층 구조의 작업을 일관된 명령으로 제어할 수 있게 되었다. actor 를 통해 mutable state에 대한 data race를 예방할 수 있는 개념을 파악할 수 있게 되었다. 서로 다른 동시성 도메인끼리 정보를 주고 받을 수 있는 sendable 타입에 대해 알게 되었다.

반응형