study record

[Swift] Concurrency - 2 본문

Swift/스위프트 정리

[Swift] Concurrency - 2

asong 2022. 11. 12. 22:46

Calling Asynchronous Functions in Parallel

await으로 비동기 함수를 호출하는 것은 한 번에 코드의 한 조각만을 실행한다. 비동기 코드가 실행하는 동안, 호출자는 다음 라인의 코드를 실행하러 이동하기 전에 코드가 끝나기를 기다린다. 예를 들어, 갤러리로부터 처음 3가지 사진을 가져오기 위해서, downloadPhoto(named:) 함수에 3번의 호출을 기다릴 수 있다.

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)

이 접근은 중요한 결점을 가진다. 비록 다운로드가 비동기이지만 진행되는 동안 또 다른 작업이 일어나게 된다. 각각의 사진이 다음의 다운로드를 시작하기 전에 완전히 다운로드한다. 그러나 작동들을 기다릴 필요가 없다. 

비동기 함수를 호출하고 병렬적으로 실행하기 위해서, asynclet 앞에 사용하고 각 constant를 사용할 때 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:)의 3번의 호출은 이전의 호출이 완료되기를 기다리는 것 없이 시작한다. 만약 충분한 시스템 자원이 이용가능하다면, 그들은 동시에 실행될 수 있다. 이 함수 호출은 await으로 마크되지 않는다. 코드가 함수의 결과를 기다리는 것을 중지하지 않기 때문이다. 대신에, 실행은 photos가 정의된 곳에서 비동기 호출로부터의 결과가 프로그램이 필요하므로, await을 쓰고 세 사진이 다운로드되기가지 실행을 중지한다.

 

두 접근 사이에서 차이에 대해 생각할 수 있다.

- 실행되는 코드의 순서가 함수의 결과에 의존할 때 await과 함께 비동기 함수를 호출한다. 이것은 순차적으로 작업이 실행되게 한다.

- 코드 순서에 맞추어 결과가 필요하지 않을 때, async-let과 함께 비동기함수를 호출한다. 이것은 병렬적으로 작업이 실행되게 한다.

- awaitasync-let 둘 다 그들이 중지되는 동안 또다른 코드가 실행되도록 허락한다.

- 두 경우 모두, await으로 실행이 멈출 수 있는 포인트를 마크한다. 만약 필요하다면, 비동기 함수가 리턴할 때까지.

같은 코드에서 두 접근 모두를 섞어 사용할 수 있다.

 

Tasks and Task Groups

task는 프로그램의 파트로서 비동기적으로 싫애될 수 있는 작업단위이다. 모든 비동기 코드는 어떤 task의 파트로서 실행된다. async-let 구문은 하위 task를 만든다. task group을 만들 수 있고, 각 group에 child task를 만들 수 있다. 우선순위 조절과 취소도 가능하다.

 

Task는 계층적으로 정리된다. task group에서 각 task는 같은 parent task를 가진다. 각 task는 child tasks를 가진다. task와 task groups 사이의 명시적인 관계 때문에, 이 접근은 structured concurrency라고 불린다. 

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를 가지지 않는다. 프로그램이 필요한 무엇이든 unstructured task를 유연하게 관리한다. 현재의 액터 위에서 실행할 unstructured task를 만들기 위해서는 Task.init(priority:operation:)을 호출한다. 현재 액터의 부분이 아닌 unstructured task를 만들기 위해서는, detached 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

 

Task Cancellation

Swift concurrency는 협력적인 취소 모델을 사용한다. 각 task는 적절한 포인트에서 취소되었는지 아닌지를 체크한다. 그리고 적절한 방식으로 취소에 반응한다. 무슨 작업을 하느냐에 따라서 의미는 다음과 같다.

 

- Throwing an error like CancellationError

- Returning nil or an empty collection

- Returning the partially completed work

 

취소를 위한 체크를 위해 Task.checkCancellation()를 호출한다. 만약 task가 취소되었다면 CancellationError를 던진다. 또는 Task.isCancelled의 값을 체크해 취소를 핸들링한다. 예를 들어, 갤러리로부터 사진을 다운로드하는 작업은 부분적인 다운로드를 삭제할 필요가 있을지도 모른다. 

취소를 직접적으로 전달하고자하면 Task.cancel()를 호출한다.

 

Actors

tasks를 격리된 상태로 분리할 수 있다. Tasks는 각각 고립된다. 이것은 그들로 하여금 동시에 실행하는 것을 안전하게끔 만든다. 그러나 때때로 tasks사이에서 정보를 공유할 필요가 있다. Actors는 안전하게 병렬 코드에서 정보를 공유하게 해준다.

 

classes 같이, actors는 참조 타입이다. 그래서 값 타입과 비교하여 참조 타입은 classes 같이 적용된다. classes와 달리, actors는 한 번에 변경가능한 상태에 접근이 오직 하나의 task만 허락이 된다. 이것은 같은 actor 인스턴스에 상호작용하기 위한 다양한 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를 도입한다. TemepratureLogger actor는 actor 바깥의 코드가 접근할 수 있는 프로퍼티들을 가지고, max 프로퍼티는 오직 actor 내부에서만 업데이트할 수 있도록 제한한다.

 

actor의 인스턴스를 structure나 class처럼 만든다. actor의 메서드나 프로퍼티에 접근할 때, await을 사용하여 잠재적 중지 포인트를 마크한다.

 

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

 

예를 들어, logger.max에 접근하는 것은 가능한 중지 포인트이다. actor가 변경가능한 상태에 접근하는 것읜 하나의 task만 허락하기 때문에 또다른 task가 이미 logger에 접근 중이라면 코드는 프로퍼티에 접근하기 위해서 기다리는 동안 중지될 것이다.

 

반대로, actor의 파트가 await을 쓰지 않는 코드가 있다.

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

 

update(with:) 메서드는 이미 actor 위에서 실행되고 있고, max를 await과 함께 접근하고 있지 않다. 이메서드는 왜 actor가 한 번에 하나의 변경가능한 상태와 접근하도록 하는지 보여주는 이유들 중 하나이다. actor의 상태에 업데이트들이 일시적으로 불변성을 깨기 때문이다. TemperaturLogger actor는 온도의 목록을 유지하고 최대 온도를 기록한다. 그리고 새 측정을 기록할 때 최대 온도를 업데이트한다. 업데이트의 중간에서, 새 측정을 추가하고 max 값을 업데이트하기 전에, temperatur logger는 일시적인 비일관성 상태이다. 같은 인스턴스에 동시에 상호작용하는 것을 막기 위해 다음의 시퀀스 같이 문제를 막는다.

 

1. update(:) 메서드를 호출한다. 이것은 measurements 배열을 먼저 업데이트한다.

2. 코드가 max를 업데이트하기 전에, 다른 코드가 최대 값과 temperatures 배열을 읽는다.

3. max에 대한 업데이트를 끝낸다.

 

이 경우에서, 다른 곳에서 실행하는 코드는 부정확한 정보를 읽을 것이다. 그 이유는 actor에 대한 접근이 update(with:) 호출 중간에 떠나졌기 때문이다. 이같은 문제를 Swift actors를 사용할 때 막을 수 있다. 그들은 오직 상태를 하나의 작동에만 허락하기 때문이다. update(with:)는 어떤 중지포인트를 포함하지 않아서 업데이트 중간에 접근할 수 없다.

 

만약 actor 바깥에서 프로퍼티에 접근을 시도한다면, 컴파일 에러를 얻게 될 것이다.

 

print(logger.max)  // Error

 

await을 쓰지 않고 logger.max에 접근하는것은 실패한다. 그이 유는 actor의 프로퍼티들은 고립된 로컬 상태에 있기 때문이다. Swift는 오직 actor 내부의 코드만 actor의 로컬 상태에 접근할 수 있도록 보장한다. 이 보장은 actor isolation으로 알려져 있다.

 

 

Sendable Types

Tasks과 actors는 프로그램을 병렬적으로 안전하게 실행할 수 있는 조각들로 나눈다. actor의 인스턴스나 task 안에서, 변경가능한 상태를 포함하는 프로그램의 파트는 concurrency domain이라고 불린다. 몇몇 데이터의 종류는 concurrency domains 사이에서 공유되지 않는다. 그 이유는 변경가능한 상태를 포함하는 데이터는 접근을 오버래핑하는 것을 보호하지 않기 때문이다.

 

concurrency domain으로부터 보호될 수 있는 타입은 sendable type으로 알려져있다. 예를 들어, actor 메서드를 호출하거나 task의 결과로서 리턴될 수 있는 요소로 전달될 수 있다. 이 챕터의 예시들은 전달성에 대해 이야기하지 않았다. 예시들이 단순한 값 타입만을 다루기 때문이다. 반대로, 몇몇 타입들은 concurrency domains를 넘어 전달하는 것이 안전하지 않다. 예를 들어, 변경가능한 프로퍼티를 포함하는 클래스는 이 프로퍼티들에 접근을 일련화하지 않는다. 이것은 다른 tasks 사이에서 클래스 인스턴스를 전달할 때  예상가능하지 못한 결과를 만들어낼 수 있다. 

 

Sendable protocol을 채택함으로써 sendable이 되는 타입을 표시할 수 있다. 이 프로토콜은 어느 코드 요구사항을 가지지 않는다. 그러나 구문적인 요구사항을 가진다. 일반적으로 세가지 방법이 있다.

 

- 타입이 값 타입이다. 그리고 변경가능한 상태가 sendable data로 구성된다. 예를 들어, sendable 연관값을 가진 enum이나 sendable 저장 프로퍼티를 가진 struct

- 타입이 변경가능한 상태를 가지지 않는다. 그리고 불변한 상태가 다른 sendable 데이터로 구성된다. 예를 들어, 오직 읽기 프로퍼티로 구성된 struct나 class

- 변경가능한 상태의 안전성을 보장하는 타입. @MainActor로 표시된 클래스나 특정 큐에서만 프로퍼티에 접근하도록 일련화된 클래스

 

구문적 요구사항의 리스트는 Sendable protocol 레퍼런스를 참고하자.

 

몇몇 타입은 항상 sendable하다. sendable 프로퍼티나 enum 만을 가진 struct이다.

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)

 

TemparatureReading은 오직 sendable한 프로퍼티를 가진 struct이다. 그리고 그 struct는 public이나 @usableFromInline으로 표시되지 않는다. 이것은 암묵적으로 sendable이다. 여기에 Sendable 프로토콜이 내재된 struct 버전이 있다.

struct TemperatureReading {
    var measurement: Int
}

 

 

참고 : 

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

'Swift > 스위프트 정리' 카테고리의 다른 글

[Swift] SwiftUI란?  (0) 2022.11.23
[Swift] Concurrency 사용하기  (0) 2022.11.15
[Swift] Concurrency - 1  (0) 2022.10.30
[Swift] 클로저, 탈출 클로저와 메모리  (0) 2022.05.23
[Swift] Swift 메모리 관리 - ARC란?  (0) 2022.05.18