Swift 는 구조적인 방법으로 asynchronous, parallel code 를 작성할 수 있는 방법을 지원함.

Asynchronous code 는 suspend 됐다가 나중에 resume 될 수 있음. 코드를 suspending 하고 resuming 할 수 있게 하는 것은 long-running operation 을 하는 동안에도 short-term operation 을 수행할 수 있게 해줌.

Parallel code 는 여러 코드를 동시에 실행할 수 있는 것. e.g. 컴퓨터는 4-core processor 로 4개의 code piece 를 동시에 실행할 수 있음.

Async, parallel code 를 사용하는 프로그램은 한 번에 여러 작업을 수행할 수 있고, 외부 시스템을 기다리는 operation 은 중단할 수 있음.

Parallel, asynchronous code 를 스케줄링할 수 있는 것은 복잡도를 증가시킴. Concurrent code 를 작성할 때, 어떤 코드가 동시에 실행될지, 코드가 실행되는 순서를 모르는 경우도 있음. Concurrent code 의 흔한 문제는 여러 코드가 공유되는 mutable state 에 접근할 때 발생 (data race). Language-level 의 concurrency 지원을 사용해서 Swift 는 data race 를 탐지, 방지함. 대부분의 data race 는 compile-time error 를 발생시킴.

Swift 의 concurrency model 은 thread 를 기반으로 만들어졌는데, model 을 통해 직접 thread 와 소통하지 않아도 됨. Swift 의 async function 은 실행되고 있는 thread 를 포기해서 다른 async function 이 그 thread 에서 실행할 수 있게 해줌. Async function 이 resume 됐을 때 Swift 는 그 function 이 어떤 thread 에서 실행할지에 대한 보장을 하지 않음.

Swift 언어 지원 없이 concurrent code 를 작성할 수 있지만 가독성이 떨어짐.

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

Series of completion handler, nested closure 를 작성하게 됨. Nesting 이 더 많이 일어날 경우에는 더 가독성이 떨어짐

Defining and Calling Asynchronous Functions

Asynchronous function / method : 실행 도중에 suspend 될 수 있는 특별한 종류의 function / method. 기존의 일반적인 completion 을 실행하거나, error 를 throw 하거나, 절대로 return 하지 않는 synchronous function / method 와는 다름. Asynchronous function / method 는 이 세 개를 할 수 있으면서도, 다른 무언가를 기다리기 위해 멈출 수 있음. Asynchronous function/method 의 body 안에서 어디에서 실행이 suspend 될 지 그 장소를 표시할 수 있음

async 키워드를 붙여서 function / method 가 asynchronous 함을 표시함. 함수가 값을 리턴하면 return arrow ->전에 async 키워드를 붙임

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

Asynchronous 하면서 throw 하는 메서드의 경우 throws 전에 async 를 붙임

Asynchronous method 를 호출하면, method 가 return 할 때까지 execution 은 suspend 됨. 해당 호출 앞에 await 를 붙여서 가능한 suspension point 를 표시함. (try 를 throwing function 호출부 앞에 붙이는 것과 같음)

Asynchronous function 안에서, 실행 플로우는 다른 asynchronous method 를 호출하는 경우에만 suspend 됨 (suspension 은 무조건 명시적이어야 함). 이는 모든 가능한 suspension point 가 await 로 표시되어 있다는 것을 의미함. 코드에 모든 suspension point 를 표시하는 건 concurrent code 를 더 가독성 있고 이해하기 쉽게 만들어줌.

let photoNames = await listPhotos(inGallery: "Summer Vacation") // network request : 상대적으로 오래 걸릴 수 있음
let sortedNames = photoNames.sorted()
let name = sortedNames[0]
let photo = await downloadPhoto(named: name) // network request : 상대적으로 오래 걸릴 수 있음
show(photo)

Network 요청이 필요해서 상대적으로 오래 걸릴 수 있는 작업들을 둘 다 asynchronous 하게 만들어서, 호출하기 전에 async 를 붙임.

위 코드는 아래의 순서로 실행될 수 있음 (가능한 경우 중 하나)

  1. 첫 번째 await 를 만남. listPhotos(inGallery:) 를 호출하고, 해당 function 이 return 될 때까지 실행을 멈춤.
  2. 코드의 실행이 suspend 된 동안, 같은 프로그램 내 다른 concurrent code 가 실행됨. e.g. Long-running background task (이 코드 또한 await 로 표기된 다음 suspension point 까지나 완료될때까지 실행됨).
  3. listPhotos(inGallery:) 가 return 된 후에, 이 코드는 해당 지점부터 다시 실행됨. 리턴된 값을 photoNames 에 할당함
  4. 다음 두 줄은 synchronous code 로 아무런 suspension point 가 없음
  5. 다음 suspension point 인 downloadPhoto(named:) 에서 해당 함수가 return 할 때까지 실행을 다시 멈추고, 다른 concurrent code 가 실행되도록 기회를 부여함
  6. downloadPhoto(named:) 가 return 된 후에, 리턴된 값은 photo 에 할당되고, show(_:) 메서드를 호출할 때 argument 로 전달됨

await 로 표시된 가능한 suspension point 는 현재의 코드가 asynchronous function/method 가 return 되기까지를 기다리는 동안 실행이 멈출 수 있음을 암시함. 이를 Yielding the thread라 부름. 뒷단에서 Swift 는 현재 thread 에서 실행되는 코드를 중단하고, 해당 thread 에 있는 다른 코드를 실행하기 때문임. (정확히 표현하면 해당 thread 에 있는 코드를 무조건 실행시키는 건 아니고 해당 thread 는 idle 상태가 아니라, 다른 작업을 실행한다는 의미임. 그래서 await 는 thread 를 block 시키지 않는다.) await 로 표시된 코드는 실행을 suspend 시킬 수 있기 때문에, 오직 코드의 특정 장소에서만 asynchronous function / method 를 실행할 수 있음

Task.sleep(for:tolerance:clock:) 는 concurrency 가 어떻게 동작하는지 확인하기 좋은 간단한 코드임. 이 메서드는 현재의 task 를 주어진 시간 동안 suspend 시킴.

func listPhotos(inGallery name: String) async throws -> [String] {
    try await Task.sleep(for: .seconds(2))
    return ["IMG001", "IMG99", "IMG0404"]
}

// async 이면서 throwing 이라 호출부에 try, await 를 둘 다 붙임
let photos = try await listPhotos(inGallery: "A Rainy Weekend")
func availableRainyWeekendPhotos() -> Result<[String], Error> {
    return Result {
        try listDownloadedPhotos(inGallery: "A Rainy Weekend")
    }
}

반대로, synchronous code 에서 asynchronous code 를 호출해서 wrap 할 수 있는 안전한 방법은 없음. Swift 표준 라이브러리는 이런 unsafe functionality(자체적으로 코드를 구현해서 subtle race, threading issue, deadlock 으로 이어질 수 있는 상황을 만드는 것) 를 금지함. Concurrent code 를 이미 존재하는 프로젝트에 추가하는 경우, top down 으로 작성하는 것이 좋음. 특히 top-most layer 를 concurrency 를 사용하도록 바꾸고, 한 레이어씩 바꿔가면서 concurrency 를 사용하도록 바꾸는 것이 좋음. Synchronous code 는 asynchronous code 를 호출할 수 없기 때문에 bottom-up 접근법은 존재하지 않음.

func syncFunc() {
    let value = await fetchData() // 불가능
}

// Swift 에서는 sync function 내에 await 를 붙여서 실행할 수 있는 suspend/resume 모델이 없음. 표준 라이브러리에 지원되지 않음 
func syncWrapper() -> Data {
    var result: Data?
    let semaphore = DispatchSemaphore(value: 0)

	// ❗️ 이런식으로 sync 내부에 async 코드를 감싸는 게 위험함
    Task {
        result = await fetchData()
        semaphore.signal()
    }

    semaphore.wait() // thread block. sync 코드에서 async 코드를 억지로 기다림. thread가 blocking 되면서 thread 가 놀게 됨
    return result! // 결과가 필요해서 결과를 억지로 기다림
}

// 위험한 이유
// 1. thread 를 막을 수 있음 
// 2. deadlock 이 생길 수 있음
// 3. subtle race 가 생길 수 있음

Asynchronous Sequences

앞선 예시에서 listPhotos(inGallery:) 함수는 전체 배열이 return 될 때까지 기다림. 다른 방법은 asynchronous sequence 를 사용해서 collection 의 각 element 를 기다리는 것임

import Foundation

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

일반적인 for-in loop 대힌, for await 문을 써서 async sequence 를 사용함. 일반적인 async function/method 를 호출할 때와 똑같이, await 를 쓰는 것은 가능한 suspension point 를 나타냄. for-await-in loop 는 각 iteration 의 시작 부분에 실행을 suspend 시켜서, 다음 요소가 available 해질 때까지 기다릴 수 있음.

자체 type 을 Sequence protocol 을 conform 시켜서 for-in loop 에서 사용할 수 있듯이, AsyncSequence protocol 을 conform 시켜서 for-await-in loop 에서 자체 type 을 사용할 수 있음

Calling Asynchronous Functions in Parallel

Async function 을 await 와 호출하는 것은 오직 한 번에 하나의 코드만 시킴. Async code 가 실행되면, 호출한 쪽에서는 다음 코드로 넘어가기 전에 해당 코드가 끝나기를 기다려야 함.

// 세 download 가 비동기적이고, 다른 코드가 실행할 수 있도록 하지만 오직 한 번에 하나의 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)

위 예시에서 각 download 는 완전히 완료된 후에 다음 download 가 진행됨. 각 사진은 독립적으로, 동시에 다운로드 될 수 있기 때문에 매번 하나씩 기다리는 것은 비효율적임

Async function 을 호출하고, parallel 하게 실행시키고 싶으면 constant 를 정의할 때 let 앞에 async 를 쓰고, constant 를 쓰는 곳에서 await 를 작성함

// 모든 downloadPhoto 가 이전 download 를 기다리지 않고 실행될 수 있음
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)

위 예시에서는 시스템 resource 가 충분하다면 동시에 실행될 수 있음. 위 함수들에서 await 를 붙이지 않은 이유는 코드가 function 의 result 를 기다리기 위해 suspend 되지 않기 때문임. 대신 execution 은 photo 가 정의된 부분까지 실행됨. 이 지점에서는 세 개의 비동기 호출의 결과가 모두 필요하기 떄문에, 모든 async function 이 실행이 끝날때까지 기다리기 위해 await 를 붙임

Tasks and Task Groups

Task : 프로그램의 일부로 asynchronous 하게 실행될 수 있는 작업의 단위. 모든 async code 는 task 읠 일부로 실행됨. Task 자체는 오직 한 번에 하나만 할 수 있는데, 여러 개의 task 를 생성한다면 Swift 는 이를 동시에 실행하도록 스케줄링 할 수 있음

async-let 는 암묵적으로 child task 를 생성하는데, 이런 문법은 프로그램의 어떤 task 가 실행되어야 하는지 아는 경우에 잘 동작함. Task group (TaskGroup 의 인스턴스) 를 생성해서 group 에 child task 를 명시적으로 추가해서 우선순위, 취소를 더 잘 컨트롤하고 task 를 동적으로 생성할 수 있음.

Task 는 hierarchy 에서 관리됨. Task group 내 각 task 는 같은 parent task 를 가지고 있고, 각 task 는 child task 를 가질 수 있음. Task, task group 간 명시적 관계 때문에 이런 접근 방법을 structured concurrency라 부름. 이런 명시적인 parent-child 관게는 여러 장점을 가짐.

// 새로운 task group 생성
await withTaskGroup(of: Data.self) { group in
    let photoNames = await listPhotos(inGallery: "Summer Vacation")
    for name in photoNames {
	    // child task 생성. child task 가 완료되는 순서는 보장되지 않음
        group.addTask {
            return await downloadPhoto(named: name)
        }
    }

    for await photo in group {
        show(photo)
    }
}

위 예시에서 download 하는 과정이 error 를 throw 할 수 있다면 withThrowingTaskGroup(of:returning:body:) 로 대신 호출할 수 있음.

만약 result 를 return 하는 task group 의 경우 result 를 축적하는 closure 를 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] = []
    // 다음 child task 이 완료되기 까지 기다림. 
    for await photo in group {
        results.append(photo)
    }

    return results
}

Task Cancellation

Swift concurrency 는 cooperative cancellation model 을 사용함. 각 task 는 실행 도중 적절한 point 에서 취소됐는지를 확인하고, cancellation 에 적절하게 대응함. (Cancellation 은 강제 종료, kill 이 아님. 시스템이 task 를 강제로 즉시 죽이지 않고, task 에 취소되었음을 알려주고, task 가 그걸 확인해서 스스로 중단하는 방식 - why? 작업마다 멈추는 안전한 시점이 다르기 때문에 자동으로 안 멈추게 해놓음) Task 가 어떤 작업을 하는 지에 따라, cancellation 에 반응하는 것은 주로 다음 케이스 중 하나임

사진을 다운로드 하는 작업은 네트워크가 느린 경우에 오래 걸릴 수 있음. 사용자가 모든 작업을 기다리지 않고 이 작업을 취소할 수 있게 하기 위해, task 는 cancellation 을 확인하고, 취소된 경우에는 실행을 멈춰야 함. (각 task 가 ‘나 취소됐니?’ 를 확인하고 취소됐으면 스스로 멈춰야 함) 이를 위해 Task.checkCancellation() type method 를 호출하거나, Task.isCancelled type property 를 읽을 수 있음. checkCancellation() 을 호출하면 task 가 취소됐을 때 error 을 throw하고, throwing task 는 에러를 task 외부로 전파해서 task 를 멈출 수 있음. 더 유연하게 사용하려면 isCancelled property 를 사용해서, task 를 멈추는 작업의 일부로 clean-up 작업을 수행할 수 있게 해줌. (직접 작업을 분기처리할 수 있기 때문에 더 유연함) e.g. 네트워크 연결 닫거나, 임시파일 삭제

func downloadPhoto() async throws -> Data {
    try Task.checkCancellation()
    
    let data = try await fetchFromNetwork()
    
    // 작업 취소 여부 확인. 자동으로 error propagate 됨. 함수 안에서 에러를 안 잡으면 바깥으로 전달됨.
    try Task.checkCancellation()
    
    return data
}

func downloadPhoto2() async throws -> Data {
	// 직접 분기처리할 수 있개 때문에 더 유연함
	if Task.isCancelled {
	    ...
	}
}

let photos = await withTaskGroup { group in
    let photoNames = await listPhotos(inGallery: "Summer Vacation")
    for name in photoNames {
	    // cancel 된 후에 새로운 작업을 실행하는 것을 방지하기 위해
        let added = group.addTaskUnlessCancelled {
	        // 각 작업은 download 전에 cancellation 을 확인함
            Task.isCancelled ? nil : await downloadPhoto(named: name)
        }
        // 새로운 child task 가 add 됐는지를 확인
        guard added else { break }
    }


    var results: [Data] = []
    for await photo in group {
        if let photo { results.append(photo) }
    }
    return results
}

Task 가 해당 task 밖에서 취소됐음을 확인하려면 type property 대신에 Task.isCancelled instance property 를 사용

취소에 대한 즉각적인 알림이 필요한 작업의 경우 Task.withTaskCancellationHandler(operation:onCancel:isolation:) method 사용.

let task = await Task.withTaskCancellationHandler {
    // task body
} onCancel: {
	// cancellation handler
	// 얘가 실행될 때 task 는 아직 살아있음
    print("Canceled!")
}


// ... some time later...
task.cancel()  // Prints "Canceled!"

// shared state
var connection: Connection?

// task 가 아직 connection 을 쓰고 있는데, handler 가 connection 을 닫아버리는 불상사 발생 가능
await withTaskCancellationHandler {
	// shared state 에 접근
    connection = openConnection()
    await connection!.download()
} onCancel: {
	// shared state 에 접근
    connection?.close()
}

Cancellation handler 를 사용할 때 task 취소는 여전히 cooperative 함 (취소가 강제로 작업을 멈추지 않음). Task 는 completion 을 실행하거나, 취소됐는지를 확인하고 즉각 멈출 수 있음. Task 는 여전히 cancellation handler 가 시작했을 때도 여전히 실행중이기 때문에, race condition 이 발생할 수 있는 task 간의 공유 상태를 피하는 것이 좋음 (Cancellation handler 가 실행될 때도 task 는 아직 실행중일 수 있기 때문에, task 코드와 cancellation handler 가 공유 상태, 같은 상태를 동시에 건드리면 race condition 이 발생할 수 있음)

Unstructured Concurrency

Swift 는 unstructured concurrency 도 지원함. Task group 의 일부인 task 과는 달리, unstructured task 는 parent task 를 가지지 않음. 필요한 경우에 unstructured task 를 유연하게 사용할 수 있으나, 올바르게 사용하는 것은 개발자의 몫.

Unstructured task 를 생성하려면 Task.init(name:priority:operation:) initializer 호출. 새로운 task 는 default 로 현재 task 와 같은 actor isolation, priority, task-local state 를 가짐.

Surrounding code 에서 좀 더 독립적인 unstructured task 를 만드려면 Task.detached(name:priority:operation:) static method 를 호출. 새로운 task 는 actor isolation 없이 실행되고, 현재 task 의 priority 나 task-local state 를 상속받지 않음.

위 두 operation 은 interact 할 수 있는 task 를 return 함. e.g. task 를 취소하거나 result 를 기다릴 수 있음

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

Isolation

이전 섹션들은 shared data 를 변경하는 작업(앱의 UI 를 바꾸는 작업)과 같은 concurrent 작업을 나누기 위한 접근법에 대한 내용이었음. 만약 코드의 다른 부분들이 동시에 같은 데이터를 수정하면 data race 가 발생할 위험이 있음. Swift 는 data race 가 발생할 수 있는 코드를 작성하지 않도록 해줌. Data 를 읽거나 수정해야 할 때, Swift 는 다른 코드가 그 data 를 concurrent 하게 수정하지 않는 것을 보장해줌. 이를 data isolation이라 부름. Data 를 isolate 하기 위한 세 가지 방법이 있음.

The Main Actor

Actor : 변경가능한 데이터에 접근하는 코드들이 자신의 순서를 기다리게끔 강제해서 해당 데이터에 대한 접근을 보호하는 객체. 가장 중요한 actor 는 main actor.

앱에서 main actor 는 UI 를 보여주기 위해 사용되는 모든 데이터를 보호함. Main Actor 는 UI 를 렌더링하는 작업, UI event 를 handling 하는 작업, UI 를 조회하거나 업데이트하는 코드를 차례대로 실행하도록 처리함.

코드에서 concurrency 를 사용하기 전에, 대부분의 작업은 main actor 에서 실행됨. 오래 실행되거나, resource 가 많이 필요한 작업들을 정의해서, 이런 작업들을 main actor 밖에서 실행되도록 옮길 수 있음.

Main actor 는 main thread 와 밀접한 관계가 있지만 같은 것은 아님. Main actor 는 private mutable state 가 있고, main thread 는 해당 state 에 대한 접근을 serialize 함. Main actor 에서 코드를 실행하면 Swift 는 그 코드를 main thread 에서 실행시킴. 개발자의 코드는 main actor 와 interact 하지만, main thread 는 더 낮은 레벨의 구현 디테일임.

Main actor 에서 작업을 실행시키는 여러 방법이 있음. Function 이 항상 main actor 에서 실행된다는 것을 보장하기 위해 @MainActor attribute 를 붙임

@MainActor
func show(_: Data) {
    // ... UI code to display the photo ...
}

@MainActor attribute 는 show(_:) function 이 무조건 main actor 에서 실행되어야 한다는 것을 요구하도록 함. 다른 코드가 main actor 에서 실행될 때, show(_:) 를 async function 으로 호출할 수도 있음. 하지만 main actor 에서 실행중이지 않은 코드에서 show(_:) 를 호출하면 main actor 로 switching 하는 것은 가능한 suspension point 가 필요하기 때문에 await 를 포함해서 async function 으로 호출해야 함.

func downloadAndShowPhoto(named name: String) async {
    let photo = await downloadPhoto(named: name)
    await show(photo)
}

downloadPhoto(named:), show(_:) function 은 모두 호출할 때 suspend 될 수 있음. 위 코드는 흔한 패턴으로, 오래 걸리는 작업과, CPU-intensive 작업을 background 에서 실행하고, UI 를 업데이트하기 위해 main actor 로 switching 하는 패턴임. downloadPhoto(named:) function 은 main actor 에 없기 때문에, downloadPhoto(named:) 에 있는 작업은 main actor 에서 실행되지 않음. show(_:) 에 있는 작업만이 UI 를 업데이트하기 위해 main actor 에서 실행됨. (@MainActor attribute 로 마킹되었기 때문)

Closure 가 main actor 에서 실행되는 것을 보장하기 위해 list 를 capture 하기 전에 @MainActor 를 쓸 수 있음

let photo = await downloadPhoto(named: "Trees at Sunrise")
// 이 코드는 UI 가 update 되는 것을 기다리지 않음
Task { @MainActor in
    show(photo)
}

@MainActor 를 struct / class / enum 에 붙여서 모든 method, property 에 접근하는 게 main actor 에서 이루어져야 함을 보장할 수도 있음

@MainActor
struct PhotoGallery {
	// UI 에 영향을 주기 때문에 이를 변경하는 코드는 main actor 에서 실행돼서 접근이 serialize 되어야 함
    var photoNames: [String]
    func drawUI() { /* ... other UI code ... */ }
}

Framework 를 바탕으로 작업할 때, framework 의 protocol 과 base class 는 일반적으로 이미 @MainActor 로 표시되어 있기 때문에, @MainActor 를 작성하지 않아도 됨

@MainActor
protocol View { /* ... */ }


// Implicitly @MainActor
struct PhotoGalleryView: View { /* ... */ }

// Fine-grained control. main thread 에서만 접근되거나 실행되어야 하는 property / method 에만 @MainActor 를 붙일 수도 있음
struct PhotoGallery {
    @MainActor var photoNames: [String]
    var hasCachedPhotos = false


    @MainActor func drawUI() { /* ... UI code ... */ }
    func cachePhotos() { /* ... networking code ... */ }
}

Property wrapper 를 정의할 때, wrappedValue property 가 @MainActor 로 표기된 경우에, 해당 property wrapper 를 적용한 property 는 또한 암묵적으로 @MainActor 로 마킹됨

Actors

임의로 자신만의 actor 를 정의할 수도 있음. Actor 는 concurrent code 간 정보를 안전하게 공유할 수 있게 해줌.

Class 처럼 actor 는 reference type. Class 와 다르게, actor 는 그들의 mutable state 에 오직 한 번에 하나의 task 만 접근할 수 있게 해서, actor 의 같은 instance 에 여러 작업들이 interact 하려고 하는 코드를 안전하게 해줌

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
    }
}


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

actor 의 property/method 에 접근할 때 await 를 써서 potential suspension point 를 표시해야 함.

Actor 내의 코드는 actor 의 property 에 접근할 때 await 를 쓰지 않음

extension TemperatureLogger {
	// 이미 actor 에서 실행됨. 
    func update(with measurement: Int) {
        measurements.append(measurement)
        // max 에 그냥 접근
        // 이 사이에 logger 는 일시적인 inconsistent 상태가 됨 (max 와 실제 데이터간의 불일치)
        // 따라서 여러 task 를 같은 instance 와 interact 하는 것을 막아야 함
        if measurement > max {
            max = measurement
        }
    }
}
  1. 코드가 update(with:) method 를 호출함. measurements array 를 먼저 update 함
  2. update(with:) 내부에서 max 를 업데이트 하기 전에, 다른 코드가 max 값에 접근함 => 불일치 발생
  3. 코드가 max 를 업데이트하는 것을 끝냄

위 문제를 actor 는 state 에 오직 한 번에 하나의 operation 만 허용하기 때문에 방지할 수 있음.

Actor 외부의 코드가 property 에 직접적으로 접근하면 compile-time error 가 발생하게 됨

print(logger.max)  // Error

Actor 의 property 는 actor 의 isolated local state 의 일부이기 때문에 위 코드는 에러가 발생함. Property 에 접근하는 코드는 actor 의 일부로 실행되어야 하고, await 을 붙여서 실행해야 함. Swift 는 actor 에서 실행되는 코드만 actor 의 local state 에 접근할 수 있도록 보장하는데, 이를 actor isolation이라고 함.

다음의 Swift concurrency model 의 특징은 shared mutable state 를 갖고 작업하기에 더 편하게 만들어줌

actor Counter {
    var value = 0

    func increment() {
        value += 1
    }
}

// 이렇게 돼도 actor 이기 때문에 동시에 실행되지 않고 한 번에 하나의 operation 만 실행됨.
Task { await counter.increment() }
Task { await counter.increment() }

이런 보장들 때문에 await 를 포함하지 않고 actor 내부에 있는 코드는 프로그램의 다른 부분에서 임시 invalid 상태에 접근하는 리스트 없이 업데이트를 할 수 있음.

extension TemperatureLogger {
	// 내부에 await 가 없기 때문에 suspension point 가 없음
	// actor 위에서 실행되는 코드만 이 measurements 에 접근할 수 있기 때문에 일부만 변경된 상태에서 다른 코드가 여기에 접근할 수 없음
    func convertFahrenheitToCelsius() {
        for i in measurements.indices {
            measurements[i] = (measurements[i] - 32) * 5 / 9
        }
    }
}

Potential suspension point 를 제거함으로써 임시 invalid 상태에서 보호하는 actor 에 코드를 작성하는 것에 추가로 코드를 sync method 로 옮길 수 있음. 위 코드는 sync method 라 내부에 절대로 potential suspension point 를 가지지 않도록 보장됨. 이 함수는 임시적으로 data model 을 inconsistent 하게 만드는 것을 캡슐화하고, 코드를 읽는 사람으로 하여금 데이터 변환 작업 도중에 다른 코드가 실행될 수 없음을 이해하기 쉽게 함.

일반 sync 함수만으로는 안전하지 않음.

class TemperatureLogger {
    var measurements: [Int] = [86, 77, 68]
	
	// sync, 한 thread 안에서는 중간에 끊기지 않지만, 다른 thread 가 동시 접근 가능
    func convertFahrenheitToCelsius() {
        for i in measurements.indices {
            measurements[i] = (measurements[i] - 32) * 5 / 9
        }
    }
}

Sync 함수 자체는 동시 접근을 막지 못하지만, actor 는 한 번에 하나의 작업만 실행하기 때문에, 중간 상태를 읽는 것 자체가 불가능

Global Actors

Main actor 는 MainActor type 의 global singleton instance. Actor 는 일반적으로 여러 개의 instance 를 가질 수 있고, 각각이 독립적인 isolation 을 제공함. 따라서 한 actor 의 isolated data 를 그 actor 의 instance property 로 정의해줘야 함. 하지만 MainActor 는 singleton 이기 때문에 type 자체만으로도 actor 를 정의할 수 있음 - attribute 를 사용해서 main-actor isolation 을 표시하면 됨. (MainActor 는 singleton 이어서 ‘MainActor’ 자체만으로도 유일한 actor 임을 알아서 인스턴스를 명시할 필요가 없음. MainActor attribute 를 붙여서 이 코드는 MainActor 에서 실행된다는 것을 표시할 수 있다) 이런 접근방식은 더 유연하게 코드를 구성할 수 있음.

자신만의 singleton global actor 를 정의할 때 @globalActor attribute 를 사용할 수 있음

Sendable Types

Task, actor 는 프로그램을 concurrent 하게 실행할 수 있는 작은 조각들로 분해할 수 있게 해줌. Task 나 actor 의 instance 안에서 프로그램의 일부는 변수나 property 같은 mutable 상태를 포함하는 부분을 concurrency domain 이라고 함. 어떤 종류의 데이터는 concurrency domain 간에 공유될 수 없는데, 이는 데이터가 mutalbe 상태를 포함하고 있지만 여러 접근에 대한 방어를 하지 않기 때문.

한 concurrency domain 에서 다른 domain 으로 공유될 수 있는 타입을 sendable type 이라고 함. e.g. Actor method 를 호출할 때 argument 로 전달될 수 있거나 task 의 result 로 reurn 될 수 있음. 앞선 예제들은 단순한 value type 을 사용하고 있기 때문에 항상 concurrency domain 사이에 데이터를 공유할 때 안전함. 그렇지 않은 타입도 있음. e.g. Mutable property 들을 포하고 그 property 들에 대한 접근은 serialize 하지 않는 class 는 그 class 의 instance 를 다른 task 간에 전달할 때 예상하지 못한 부정확한 결과를 초래할 수 있음.

어떤 타입이 sendable 한지를 표현하려면 Sendable protocol 을 채택하면 됨. Protocol 은 code requirement 가 없지만, Swift 가 강제하는 semantic requirement 는 있음.

Sendable property 만을 가진 struct 나 sendable associated value 만을 가진 enum 같이 항상 sendable 한 type 이 있음

// public 이 아니고 @usableFromInline 으로 마킹되지 않았기 때문에 암묵적으로 sendable
struct TemperatureReading: Sendable {
    var measurement: Int // sendalbe property
}


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)

Type 이 sendable 하지 않음을 명시하려면 아래와 같이 Sendable 에 unavailable conformance 를 추가함

struct FileDescriptor {
    let rawValue: Int
}


@available(*, unavailable)
extension FileDescriptor: Sendable {}