├── .gitignore ├── README.md └── chapters ├── 01_Why_Modern_Swift_Concurrency.md ├── 02_Getting_Started_With_async_await.md ├── 03_AsyncSequence_and_Intermediate_Task.md ├── 04_Custom_Asynchronous_Sequences_With_AsyncStream.md ├── 05_Intermediate_async_await_and_CheckedContinuation.md ├── 06_Testing_Asynchronous_Code.md ├── 07_Concurrent_Code_With_TaskGroup.md ├── 08_Getting_Started_With_Actors.md ├── 09_Global_Actors.md ├── 10_Actors_in_a_Distributed_System.md └── images ├── 02-async-errors.png ├── 02-async-group-1.png ├── 02-async-group-2.png ├── 02-cancellation.png ├── 02-create-task-on-actor.png ├── 02-download-button.png ├── 02-download-error.png ├── 02-partial-task-1.png ├── 02-partial-task-2.png ├── 03-byte-accumulator.png ├── 03-combine-1.png ├── 03-combine-publisher.png ├── 03-task-cancel-1.png ├── 03-task-cancel-2.png ├── 03-task-local-1.png ├── 03-task-local-2.png └── 03-timer.png /.gitignore: -------------------------------------------------------------------------------- 1 | ### macOS ### 2 | .DS_Store 3 | .AppleDouble 4 | .LSOverride 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift Concurrency 2 | 3 | :carrot: 팀원들과 Swift Concurrency 스터디를 진행해요. 4 | 5 | ## Tools 6 | 7 | - :books: [Modern Concurrency in Swift](https://www.raywenderlich.com/books/modern-concurrency-in-swift) 8 | - Xcode 13.2 9 | 10 | ## Contents 11 | 12 | - 크게 2개의 주제로 구분해서 진행해요. 13 | - 매주 챕터 1개씩 읽고 랜덤으로 진행자를 선정해요. 14 | - 개인적으로 학습한 내용과 스터디 진행 중 나온 이야기들을 예제와 함께 정리해요. 15 | 16 | ### async/await & Task 17 | 18 | | | Chapter Name | Date | 19 | | ---- | ------------------------------------------------------------ | ------------ | 20 | | 1 | [Why Modern Swift Concurrency?](./chapters/01_Why_Modern_Swift_Concurrency.md) | Dec 21, 2021 | 21 | | 2 | [Getting Started With async/await](./chapters/02_Getting_Started_With_async_await.md) | Jan 4, 2022 | 22 | | 3 | [AsyncSequence & Intermediate Task](./chapters/03_AsyncSequence_and_Intermediate_Task.md) | Jan 11, 2022 | 23 | | 4 | [Custom Asynchronous Sequences With AsyncStream](./chapters/04_Custom_Asynchronous_Sequences_With_AsyncStream.md) | Jan 18, 2022 | 24 | | 5 | [Intermediate async/await & CheckedContinuation](./chapters/05_Intermediate_async_await_and_CheckedContinuation.md) | Jan 25, 2022 | 25 | | 6 | [Testing Asynchronous Code](./chapters/06_Testing_Asynchronous_Code.md) | | 26 | | 7 | [Concurrent Code With TaskGroup](./chapters/07_Concurrent_Code_With_TaskGroup.md) | | 27 | 28 | ### Actor 29 | 30 | | | Chapter Name | Date | 31 | | ---- | ------------------------------------------------------------ | ---- | 32 | | 8 | [Getting Started With Actors](./chapters/08_Getting_Started_With_Actors.md) | | 33 | | 9 | [Global Actors](./chapters/09_Global_Actors.md) | | 34 | | 10 | [Actors in a Distributed System](./chapters/10_Actors_in_a_Distributed_System.md) | | -------------------------------------------------------------------------------- /chapters/01_Why_Modern_Swift_Concurrency.md: -------------------------------------------------------------------------------- 1 | # 1. Why Modern Swift Concurrency 2 | 3 | Swift 5.5 에 비동기, 병렬 코드를 작성하기 위한 새로운 네이티브 모델이 추가되었습니다. 4 | 5 | 새로운 동시성 모델은 Swift 로 안전하고 성능이 뛰어난 프로그램을 작성하기 위해 필요한 것들을 제공합니다. 6 | 7 | - 구조화된 방법으로 비동기 작업을 실행하기 위한 새로운 네이티브 문법입니다. 8 | - 비동기, 동시성 코드를 설계하기 위한 표준 API 번들입니다. 9 | - libdispatch 프레임워크 내의 low-level 변경으로, 모든 high-level 변경이 os 에 직접 통합됩니다. 10 | - 안전한 동시성 코드를 만들기 위한 새로운 레벨의 컴파일러 지원입니다. 11 | 12 | 새로운 동시성 모델을 사용하려면 최신 Swift 버전을 사용하며, 특정 플랫폼 버전을 타겟으로 지정해야 합니다. 13 | (Xcode 13.2 이상 버전을 사용하는 경우 iOS 13 / macOS 10.15) 14 | 15 | ## 비동기, 동시성 코드 이해하기 16 | 17 | 대부분의 코드는 작성하는 방식과 동일하게 위에서 아래로 한 줄씩 실행됩니다. 18 | 동기 컨텍스트에서는 코드는 싱글 CPU 코어의 한 실행 쓰레드에서 동작합니다. 19 | 동기 함수의 동작은 1차선 도로 위의 자동차로 상상할 수 있습니다. 20 | 구급차 처럼 우선순위가 높은 차량이 있더라도, 다른 차량을 추월해 더 빠르게 운전할 수 없습니다. 21 | 22 | 반면 iOS 앱과 macOS 앱 본질적으로 비동기적입니다. 23 | 24 | 비동기 실행은 프로그램의 여러 요소들이 한 쓰레드에서 어떤 순서로든 실행될 수 있게 합니다. 25 | 때로는 사용자 입력과 네트워크 연결 등의 이벤트에 따라 여러 쓰레드에서 동시에 실행될 수 있습니다. 26 | 비동기 컨텍스트에서 동일한 쓰레드를 사용해야 할 때, 함수가 실행되는 정확한 순서를 알기는 어렵습니다. 27 | 28 | 비동기 호출의 한 예로 네트워크 요청을 하고 서버가 응답할 때 컴플리션 클로저를 실행하는 것이 있습니다. 29 | 컴플리션 콜백을 실행하기 위해 기다리는 동안, 앱은 다른 일을 처리합니다. 30 | 31 | 프로그램을 병렬로 실행하기 위해 동시성 API 를 사용합니다. 32 | 일부 API 는 고정된 수의 작업을 동시에 실행할 수 있도록 지원하고, 다른 API 는 동시성 그룹을 시작하고 여러 동시성 작업을 허용합니다. 33 | 34 | 이는 동시성과 관련된 여러 문제들을 일으킵니다. 35 | 예를 들어, 프로그램의 다른 부분이 서로의 실행을 블락하거나, 여러 함수가 동시에 동일한 변수에 엑세스하여 문제를 일으키는 데이터 레이스가 발생합니다. 36 | 37 | 하지만 동시성을 조심해서 사용하면 멀티 CPU 코어에서 동시에 여러 함수를 실행해 프로그램이 더 빠르게 동작하도록 만들 수 있습니다. 38 | 이는 n차선 도로에서 운전자들이 더 빠르게 이동할 수 있는 것과 유사합니다. 39 | 차선이 여러개이기 때문에 더 빠른 차량이 느린 차들을 앞서갈 수 있고, 구급차 같은 우선순위가 높은 차량은 비상 차선을 사용할 수 있습니다. 40 | 41 | 구급차 처럼 우선 순위가 높은 작업은 낮은 작업의 대기열을 점프할 수 있어서, UI 업데이트를 위한 메인 쓰레드를 블락하지 않습니다. 42 | 예를 들면 서버에서 여러 이미지를 동시에 다운로드하고, 썸네일 크기로 축소해서, 캐시에 저장해야 하는 사진 검색 사례가 있습니다. 43 | 44 | ## 기존의 동시성 프로그래밍 알아보기 45 | 46 | Swift 5.5 이전 버전에서는 아래의 도구들을 이용해 비동기 코드를 실행했습니다. 47 | 48 | 1. GCD 의 DispatchQueue 49 | 2. Operation, Thread 같은 metal 에 가까운 오래된 API 50 | 3. C 기반의 pthread 라이브러리를 직접 사용 51 | 52 | 이 API 들은 모두 프로그래밍 언어에 의존하지 않는 POSIX thread 를 사용합니다. 각 실행 흐름은 쓰레드이고, 여러 쓰레드가 동시에 겹쳐서 실행될 수도 있습니다. 53 | 54 | Operation, Thread 같은 Thread Wrapper 의 경우 쓰레드를 수동으로 관리해야 해서 오류가 발생하기 쉬웠습니다. 55 | 56 | GCD 의 큐 기반 모델은 잘 작동했지만, 간혹 이런 문제를 일으키기도 했습니다. 57 | 58 | - Thread explosion : 여러 병렬 쓰레드를 생성하면 쓰레드 사이를 계속 전환해야 합니다. 결과적으로 앱 속도가 느려집니다. 59 | - Priority inversion : 같은 큐의 우선순위가 낮은 작업이 높은 우선순위의 작업의 실행을 블락합니다. 60 | - Lack of execution hierarchy : 실행중인 작업에 접근하거나 취소하는 것이 어려웠습니다. 이는 결과 반환하는 것도 복잡하게 만들었습니다. 61 | 62 | 이런 단점을 해결하기 위해 Swift 는 새로운 동시성 모델을 도입하였습니다. 63 | 64 | ## 새로운 Swift 동시성 모델 65 | 66 | 새로운 동시성 모델은 언어 문법, Swift 런타임, Xcode 와 강하게 통합되며, 이는 개발자를 위해 쓰레드 개념을 추상화합니다. 67 | 68 | 새로운 주요 기능은 아래와 같습니다. 69 | 70 | 1. Cooperative thread pool 71 | 2. async / await 72 | 3. Structured concurrency 73 | 4. Context-aware code compilation 74 | 75 | ### 1. Cooperative thread pool 76 | 77 | 새로운 모델은 쓰레드 풀을 투명하게 관리해 사용가능한 CPU 코어 수를 초과하지 않도록 만듭니다. 78 | 79 | 이런 방식으로 런타임에서 쓰레드를 생성/제거하거나 비용이 비싼 쓰레드 전환을 계속 수행하지 않아도 됩니다. 80 | 81 | 대신 코드가 중단되었다가 풀의 사용 가능한 쓰레드에서 빠르개 재개될 수 있습니다. 82 | 83 | ### 2. async / await 84 | 85 | async / await 문법을 통해 컴파일러와 런타임은 일부 코드가 중단되었다가 다시 재개될 수 있다는 것을 알 수 있습니다. 86 | 87 | 런타임에서 이를 원활하게 처리하기 때문에, 쓰레드와 코어에 대한 걱정을 하지 않아도 됩니다. 88 | 89 | 추가로 탈출 클로저를 사용하지 않기 때문에 self 나 다른 변수를 약하게 캡처할 필요가 없어집니다. 90 | 91 | ### 3. Structured concurrency 92 | 93 | 각 비동기 작업은 부모 Task와 주어진 실행 우선순위를 가지고 계층에 속하게 됩니다. 94 | 95 | 이 계층을 통해 런타임은 부모 작업이 취소될 때 모든 하위작업을 취소할 수 있습니다. 96 | 97 | 또한 런타임이 부모 작업이 완료되기 전에 모든 자식 작업을 완료하도록 기다리게 합니다. 98 | 99 | 이는 계층 내에서 높은 우선순위의 작업이 낮은 우선순위 작업보다 먼저 실행될 수 있다는 장점과 확실한 결과를 제공합니다. 100 | 101 | ### 4. Context-aware code compilation 102 | 103 | 컴파일러는 주어진 코드가 비동기로 실행되는지 트래킹합니다. 104 | 105 | 비동기로 실행된다면 공유 상태(shared state)를 변경하는 것처럼 안전하지 않은 코드를 작성할 수 없습니다. 106 | 107 | 이런 높은 수준의 컴파일러 인식은 Actor 같은 새로운 기능을 가능하게 합니다. 108 | 109 | > Actor 는 비동기/동기 접근에서 안전하지 않은 코드를 작성하기 어렵게 만들어 데이터가 손상되는 것을 방지합니다. 110 | 111 | ## async / await 사용하기 112 | 113 | - [예제 코드](https://github.com/raywenderlich/mcon-materials/tree/editions/1.0/00-book-server) 114 | 115 | ```swift 116 | func availableSymbols() async throws -> [String] { 117 | guard let url = URL(string: "http://localhost:8080/littlejohn/symbols") else { 118 | throw "url error" 119 | } 120 | } 121 | ``` 122 | 123 | 함수 정의에 쓰이는 async 키워드는 컴파일러가 코드가 비동기 컨텍스트에서 실행된다는 것을 알 수 있게 합니다. 124 | 125 | 이것은 코드가 중단됐다가 재개될 수 있다는 것을 의미합니다. 126 | 127 | 또한 함수가 완료되는 시간과 무관하게 동기 메서드와 유사한 값을 반환합니다. (예제에서는 [String]) 128 | 129 | ```swift 130 | let (data, response) = try await URLSession.shared.data(from: url) 131 | ``` 132 | 133 | foo 함수 하단에 URLSession 을 호출해 데이터를 가져오는 코드를 추가합니다. 134 | 135 | 비동기 메서드 `URLSession.data(from:delegate:)` 를 호출하면 foo 함수가 중단되었다가 서버에서 데이터를 가져올 때 재개됩니다. 136 | 137 | await 을 사용하여 런타임에 중단 포인트를 줄 수 있습니다. 함수를 중지하고, 먼저 실행할 다른 Task 를 실행한 뒤 코드를 계속 진행합니다. 138 | 139 | ```swift 140 | guard (response as? HTTPURLResponse)?.statusCode == 200 else { 141 | throw "response error" 142 | } 143 | 144 | return try JSONDecoder().decode([String].self, from: data) 145 | ``` 146 | 147 | 다음으로 위 코드를 실행해 서버 응답 코드가 200인지 확인한 뒤 JSON 을 디코딩하여 반환합니다. 148 | 149 | ## SwiftUI 에서 async / await 사용하기 150 | 151 | - [예제 코드](https://github.com/raywenderlich/mcon-materials/blob/editions/1.0/01-hello-modern-concurrency/projects/final/LittleJohn/SymbolListView.swift) 152 | 153 | ```swift 154 | .task { 155 | do { 156 | symbols = try await model.availableSymbols() 157 | } catch { 158 | errorMessage = error.localizedDescription 159 | } 160 | } 161 | ``` 162 | 163 | SwiftUI 에서는 `task(priority:_:)` 를 사용해서 비동기 작업을 실행할 수 있습니다. 164 | task 는 onAppear 와 마찬가지로 뷰가 화면이 표시될 때 호출됩니다. 165 | 166 | 위 예제 처럼 try 와 await 을 활용해 비동기적으로 값을 반환받고 실패할 경우 에러를 핸들링할 수 있습니다. 167 | 168 | ## Asynchronous sequence 사용하기 169 | 170 | - [예제 코드](https://github.com/raywenderlich/mcon-materials/blob/editions/1.0/01-hello-modern-concurrency/projects/final/LittleJohn/TickerView.swift) 171 | 172 | ```swift 173 | let (stream, response) = try await liveURLSession.bytes(from: url) 174 | 175 | for try await line in stream.lines { 176 | let sortedSymbols = try JSONDecoder() 177 | .decode([Stock].self, from: Data(line.utf8)) 178 | .sorted(by: { $0.name < $1.name }) 179 | 180 | tickerSymbols = sortedSymbols 181 | } 182 | ``` 183 | 184 | stream 은 서버가 응답으로 보내는 바이트 시퀀스입니다. 185 | 186 | lines 는 응답의 text line 을 하나씩 제공하는 시퀀스를 추상화한 것 입니다. lines 는 반복문을 거쳐 JSON 으로 디코딩합니다. (for 문 내에 작성된 코드) 187 | 188 | tickerSymbols 가 백그라운드 쓰레드에서 변경되면서 보라색 경고를 볼 수 있습니다. (UI 는 메인 쓰레드에서 동작해야 함) 189 | 190 | ## 메인 쓰레드에서 UI 변경하기 191 | 192 | 비동기 작업을 실행하는 컨텍스트에서 tickerSymbols 를 변경하면, 코드는 풀에 있는 임의 쓰레드에서 실행됩니다. 193 | 194 | UI 업데이트를 담당하는 상태를 변경하는 경우, 아래처럼 동작시킬 수 있습니다. 195 | 196 | ```swift 197 | // AS-IS 198 | tickerSymbols = sortedSymbols 199 | 200 | // TO-BE 201 | await MainActor.run { 202 | tickerSymbols = sortedSymbols 203 | } 204 | ``` 205 | 206 | MainActor 는 코드를 메인 쓰레드에서 실행시킵니다. (`MainActor.run(_:)`을 사용) 207 | 208 | ## 동시성 작업 취소하기 209 | 210 | - [예제 코드](https://github.com/raywenderlich/mcon-materials/blob/editions/1.0/01-hello-modern-concurrency/projects/final/LittleJohn/TickerView.swift) 211 | 212 | Modern Swift Concurrency 의 장점 중 하나는 구조화된 방법으로 동시성 코드가 실행되는 것입니다. 213 | 214 | Task 들은 스트릭트한 계층에서 실행되어 런타임은 누가 Task 의 부모 Task 인지, 어떤 새로운 Task 들이 상속되어야 하는지 알 수 있습니다. 215 | 216 | ```swift 217 | .task { 218 | do { 219 | try await model.startTicker(selectedSymbols) 220 | } catch { 221 | if let error = error as? URLError, 222 | error.code == .cancelled { 223 | return 224 | } 225 | 226 | lastErrorMessage = error.localizedDescription 227 | } 228 | } 229 | ``` 230 | 231 | 위 코드의 `task(_:)` 를 살펴보면 `startTicker(_:)` 를 비동기로 호출하고 이는 비동기 시퀀스를 반환합니다. 232 | 233 | await 키워드가 있는 각 지점에서 매번 쓰레드가 변경될 수 있습니다. 전체 프로세스는 `task(_:)` 내부에서 시작하기 때문에 비동기 Task 는 실행 쓰레드나 중단 상태와 무관하게 다른 Task 들의 부모 Task 입니다. 234 | 235 | SwiftUI 의 `task(_:)` 는 뷰가 사라질 때 비동기 코드를 취소합니다. 동작을 실행한 뒤 백버튼을 눌러 동작이 취소되는 것을 확인할 수 있습니다. 236 | 237 | 취소에 대한 에러를 처리해야 하는 경우 catch 에서 처리를 해주면 됩니다. 예제의 경우 URLError 의 code 가 cancelled 인 것을 확인하였습니다. 238 | 239 | Task.sleep 과 같은 최신 비동기 API 에는 CancellationError 에러를 발생시킵니다. 커스텀 에러를 발생시키는 경우 URLSession 처럼 취소와 관련된 에러코드가 존재합니다. 240 | 241 | ## 스터디에 사용된 추가 자료 242 | 243 | - [Swift Concurrency 최소 버전 이야기](https://forums.swift.org/t/will-swift-concurrency-deploy-back-to-older-oss/49370/77) 244 | - [Concurrency Asynchronous Functions](https://forums.swift.org/t/concurrency-asynchronous-functions/41619/43) 245 | - [Async Sequence Proposal](https://github.com/apple/swift-evolution/blob/main/proposals/0298-asyncsequence.md) 246 | - [WWDC-Swift concurrency: Behind the scenes](https://developer.apple.com/videos/play/wwdc2021/10254/) 247 | - [Apple Doc - AsyncStream](https://developer.apple.com/documentation/swift/asyncstream) 248 | -------------------------------------------------------------------------------- /chapters/02_Getting_Started_With_async_await.md: -------------------------------------------------------------------------------- 1 | # Getting Started With async/await 2 | 3 | 이번 챕터에서는 실제 async/await 구문과 이것이 어떻게 비동기 실행을 조정하는지 알아보겠습니다. 4 | 5 | 추가로 Task type 과 비동기 실행 컨텍스트를 어떻게 생성하는지 알 수 있습니다. 6 | 7 | 먼저 Swift 5.5 (async/await) 이전의 동시성 프로그래밍에 대해 알아봅시다. 8 | 9 | ## async/await 이전의 비동기 프로그래밍 10 | 11 | ```swift 12 | final class API { 13 | func fetch(completion: @escaping (Data) -> Void) { 14 | URLSession.shared 15 | .dataTask( 16 | with: URL(string: "https://test.com/test")! 17 | ) { data, response, error in 18 | completion(data) 19 | } 20 | .resume() 21 | } 22 | } 23 | 24 | final class ViewModel { 25 | let api = API() 26 | 27 | var data: Data? 28 | 29 | func fetch() { 30 | api.fetch { [weak self] data in 31 | self?.data = data 32 | } 33 | } 34 | } 35 | ``` 36 | 37 | 위 예제는 API 를 호출하는 코드입니다. 38 | 39 | 이는 간단하지만, 의도를 모호하게 만들며, 여러 에러를 만들어 낼 수 있습니다. 40 | ex) completion 호출에 대해 검증할 수 없습니다. 41 | 42 | Swift 는 Objective-C 를 위해 디자인된 GCD 에 의존했기 때문에, 처음부터 언어 디자인에 비동기성을 통합하기 어려웠습니다. 43 | 44 | Objective-C 의 경우 언어가 시작된 수년 후인 iOS 4 에 블록(Swift 의 클로저와 유사함)이 도입되었습니다. 45 | 46 | 다시 위의 예제를 살펴보겠습니다. 47 | 48 | 1. 먼저 컴파일러는 `fetch()` 내에서 completion 의 호출횟수를 알 수 있는 방법이 없습니다. 따라서 메모리 사용과 수명을 최적화할 수 없습니다. 49 | 2. 해당 코드를 사용할 때 약한참조(weak)를 이용해 메모리를 직접 관리해야 합니다. 50 | 3. 컴파일러는 에러를 핸들링했는지 알 수 없습니다. completion 핸들러를 호출하지 않거나 에러를 핸들링하지 않으면 문제가 발생할 수 있습니다. 51 | 52 | --- 53 | 54 | Swift 의 Modern Concurrency Model 은 컴파일러와 런타임 모두와 긴밀하게 동작해 위의 문제를 포함한 많은 문제들을 해결합니다. 55 | 56 | Modern Concurrency Model 은 아래의 세가지 도구를 제공합니다. 57 | 58 | - async : 메서드 혹은 함수가 비동기임을 나타냅니다. 이를 이용해 비동기 메서드가 결과를 반환할 때까지 실행을 중단할 수 있습니다. 59 | - await : 코드가 async 메서드 혹은 함수가 반환되기 전까지 실행을 중지할 수 있음을 나타냅니다. 60 | - Task : 비동기 작업의 단위입니다. Task 가 완료되기를 기다리거나, 완료되기 전에 취소할 수 있습니다. 61 | 62 | 위 예제를 Modern Concurrency 를 이용해 다시 작성해보겠습니다. 63 | 64 | ```swift 65 | final class API { 66 | func fetch() async throws -> Data { 67 | let (data, _) = try await URLSession.shared.data( 68 | from: URL(string: "https://test.com/test")! 69 | ) 70 | 71 | return data 72 | } 73 | } 74 | 75 | final class ViewModel { 76 | let api = API() 77 | 78 | var data: Data? 79 | 80 | func fetch() { 81 | Task { 82 | data = try await api.fetch() 83 | } 84 | } 85 | } 86 | ``` 87 | 88 | 위 코드는 컴파일러와 런타임에게 더 명확합니다. 89 | 90 | - `fetch()` 는 실행을 중단했다 재개할 수 있는 비동기 함수입니다. async 를 사용하여 표시합니다. 91 | - `fetch()` 는 데이터를 반환하거나 에러를 throw 합니다. 컴파일 타임에 확인이 가능해서 오동작을 방지할 수 있습니다. 92 | - Task 는 주어진 클로저를 비동기 컨텍스트에서 실행해서, 컴파일러는 클로저 내에서 쓰기(변경)에 안전한지 알 수 있습니다. 93 | - await 을 사용해 런타임에게 비동기 함수를 호출할 때마다 코드를 중단하거나 취소할 수 있는 기회를 제공합니다. 이는 시스템이 현재 Task queue 의 우선순위를 지속적으로 변경할 수 있게 합니다. 94 | 95 | ## 코드를 partial tasks 로 분리하기 96 | 97 | CPU 코어와 메모리 처럼 공유 자원을 최적화하기 위해, Swift 는 코드를 partial task 혹은 partials 로 불리는 논리 단위로 분리합니다. 98 | 99 | 100 | 101 | Swift 런타임은 비동기 실행을 위해 이 조각들을 각각 스케줄링합니다. 각 partial task 가 완료되면 시스템은 보류된 task의 우선순위와 시스템 부하에 따라 코드를 계속할지 다른 task 를 실행할지 결정합니다. 102 | 103 | 따라서 await 어노테이션이 붙은 partial task 들은 시스템 재량에 따라 다른 쓰레드에서 실행될 수 있습니다. 또한 await 후에 앱의 상태를 가정해서는 안됩니다. 작성된 코드는 차례대로 나타나지만 실행시간이 많이 차이날 수도 있습니다. task 를 기다리는건 임의의 시간이 걸리며 그 사이에 앱의 상태가 크게 변경될 수 있습니다. 104 | 105 | 요약하자면 async/await 은 간단하지만 강력한 구문입니다. 이는 컴파일러가 안전하고 견고한 코드를 작성하도록 가이드하고, 런타임이 공유 시스템 자원의 사용을 최적화하도록 합니다. 106 | 107 | ### partial tasks 실행하기 108 | 109 | async, await, let 와 같은 키워드를 사용하는 것은 의도를 명확하게 표현합니다. 동시성 모델의 기반은 비동기 코드를 Executor 에서 실행하는 partial tasks 로 나누는 것을 중심으로 다룹니다. 110 | 111 | 112 | 113 | Executor 는 GCD queue 와 비슷하지만, 더 강력하고 low-level 입니다. 그리고 Executor 는 작업을 빠르게 실행하고, 실행 순서와 쓰레드 관리 같은 복잡함을 완전히 숨길 수 있습니다. 114 | 115 | ## Task 의 수명 관리하기 116 | 117 | Modern Concurrency 의 중요한 신규 기능 중 하나는 비동기 코드의 수명을 관리하는 시스템의 능력입니다. 118 | 119 | 기존 멀티 쓰레드 API 의 가장 큰 단점은 비동기 코드가 시작되면 그 코드가 제어를 포기하기 전까지, 시스템이 CPU 코어를 회수할 수 없었다는 점입니다. 이로 인해 특정 작업이 더이상 필요하지 않아도 리소스를 소비하고, 필요하지 않은 작업을 수행합니다. 120 | 121 | 서버에서 컨텐츠를 가져오는 서비스가 좋은 예제입니다. 서비스를 두 번 호출하면 시스템은 첫번째 호출이 사용한 리소스를 회수할하는 자동 메커니즘이 없어, 불필요한 리소스를 낭비하게 됩니다. 122 | 123 | 새로운 비동기 모델은 코드를 부분으로 나누어 런타임에서 체크하는 중단 지점을 제공합니다. 124 | 이는 시스템이 코드를 정지시키거나 취소할 수 있는 기회를 줍니다. 125 | 126 | 덕분에 주어진 작업을 취소할 때 런타임은 비동기 계층으로 이동할 수 있고, 하위 작업을 취소할 수 있습니다. 127 | 128 | 129 | 130 | 하지만 중단 지점 없이 긴 계산을 수행하는 작업이 있으면 어떨까요? 이런 경우 Swift 는 현재 작업이 취소되었는지 알 수 있는 API 를 제공합니다. 이 경우 수동으로 실행을 포기할 수 있습니다. 131 | 132 | 마지막으로 중단 지점은 에러를 위한 탈출 경로를 제공해 코드에서 에러를 캐치하고 핸들링하는 코드로 계층을 끌어올립니다. 133 | 134 | 135 | 136 | 새로운 비동기 모델은 잘 알려진 throw 함수 등을 이용해 동기 함수가 가진 구조와 유사하게 에러를 핸들링 할 수 있도록 제공합니다. 또한 task 가 에러를 throw 하는 즉시 메모리를 해제하도록 최적화 되었습니다. 137 | 138 | Modern Swift Concurrency Model 에서 반복되는 토픽은 안전함(safety), 최적화된 리소스 사용(optimized resource usage), 최소 구문(minimal syntax) 입니다. 뒤에서는 새로운 API 에 대해 자세히 알아보고 사용해보겠습니다. 139 | 140 | ## async / await 문법 살펴보기 141 | 142 | async 키워드는 함수 바로 다음에 올 수 있습니다. 143 | 144 | await 은 호출하려는 함수 앞에 위치하고 throwable (에러를 반환) 하다면 await 앞에 try 를 사용합니다. 145 | 146 | ```swift 147 | func myFunction() async throws -> String { 148 | ... 149 | } 150 | 151 | let myVar = try await myFunction() 152 | ``` 153 | 154 | computed property 에 async 키워드를 작성하는 경우, 아래처럼 사용할 수 있습니다. 155 | 156 | ```swift 157 | var myProperty: String { 158 | get async { 159 | ... 160 | } 161 | } 162 | 163 | print(await myProperty) 164 | ``` 165 | 166 | 클로저에 async 를 붙이는 경우 167 | 168 | ```swift 169 | func myFunction(worker: (Int) async -> Int) -> Int { 170 | ... 171 | } 172 | 173 | myFunction { 174 | return await computeNumbers($0) 175 | } 176 | ``` 177 | 178 | ## 서버에서 파일 가져오기 179 | 180 | ### Server Request 만들기 181 | 182 | 웹서버에서 JSON 파일을 가져오는 기능을 개발합니다. ([SuperStorageModel.swift](https://github.com/raywenderlich/mcon-materials/blob/editions/1.0/02-beginner-async-await/projects/final/SuperStorage/Model/SuperStorageModel.swift) 파일 참고) 183 | 184 | ```swift 185 | func availableFiles() async throws -> [DownloadFile] { 186 | guard let url = URL(string: "http://localhost:8080/files/list") else { 187 | throw "Could not create the URL." 188 | } 189 | 190 | let (data, response) = try await URLSession.shared.data(from: url) 191 | } 192 | ``` 193 | 194 | URLSession async data 함수를 사용해서 웹서버에서 응답이 오기 전까지 해당 쓰레드에서 다른 작업을 수행할 수 있도록 만들 수 있습니다. 195 | 196 | await 은 다음을 의미합니다. 197 | 198 | - 현재 코드가 실행을 중단합니다. 199 | - 시스템 부하에 따라 대기중인 메서드는 즉시 또는 나중에 실행됩니다. 더 우선순위가 높은 Task 가 있다면 기다려야할 수 있습니다. 200 | - 메서드 혹은 하위 Task 중 하나가 에러를 발생시키면, 에러는 call hierarchy 를 가장 가까운 catch 문으로 올립니다. 201 | 202 | await 을 사용하면 런타임의 central dispatch system 을 통해 모든 비동기 호출을 이동시키는데, 이는 다음과 같습니다. 203 | 204 | - 우선순위 지정 205 | - 취소 전파 206 | - 에러 올리기 207 | 208 | ### 응답 상태 확인하기 209 | 210 | - [예제 코드](https://github.com/raywenderlich/mcon-materials/blob/d2916bebbf69895a34a96575a98a1ff3002cd348/02-beginner-async-await/projects/final/SuperStorage/Model/SuperStorageModel.swift#L105) 211 | 212 | ```swift 213 | guard (response as? HTTPURLResponse)?.statusCode == 200 else { 214 | throw "The server responded with an error." 215 | } 216 | 217 | guard let list = try? JSONDecoder() 218 | .decode([DownloadFile].self, from: data) else { 219 | throw "The server response was not recognized." 220 | } 221 | ``` 222 | 223 | 비동기 호출이 완료되고 서버 응답을 반환하면, 응답 상태를 확인해 Data 를 디코딩할 수 있습니다. 224 | 225 | 위 소스코드를 보면 status code 200 을 확인하고, JSONDecoder 를 활용해 [DownloadFile] 로 디코딩하고 있습니다. 226 | 227 | ### 파일 목록 반환하기 228 | 229 | 반환은 간단합니다. 230 | 231 | 메서드의 실행은 비동기적이지만, 코드는 동기적으로 읽을 수 있어 동작을 유추하기 쉽습니다. 232 | 233 | ```swift 234 | return list 235 | ``` 236 | 237 | ### 목록 보여주기 238 | 239 | - [예제 코드(ListView.swift)](https://github.com/raywenderlich/mcon-materials/blob/editions/1.0/02-beginner-async-await/projects/starter/SuperStorage/ListView.swift) 에 다음 코드를 추가해봅시다. 240 | 241 | ```swift 242 | .task { 243 | guard files.isEmpty else { return } 244 | 245 | do { 246 | files = try await model.availableFiles() 247 | } catch { 248 | lastErrorMessage = error.localizedDescription 249 | } 250 | } 251 | ``` 252 | 253 | 파일 목록이 있는지 확인하고, 없다면 `availableFiles()` 를 호출해 가져옵니다. 254 | 255 | 에러가 발생했다면 lastErrorMessage 에 에러를 할당합니다. 그러면 alert 에서 에러 메시지를 표시합니다. 256 | 257 | ## 서버의 상태 얻기 258 | 259 | 서버의 상태를 가져오고, 사용자의 사용 한도를 가져옵니다. 260 | 261 | ```swift 262 | func status() async throws -> String { 263 | guard let url = URL(string: "http://localhost:8080/files/status") else { 264 | throw "Could not create the URL." 265 | } 266 | 267 | let (data, response) = try await URLSession.shared.data(from: url, delegate: nil) 268 | 269 | guard (response as? HTTPURLResponse)?.statusCode == 200 else { 270 | throw "The server responded with an error." 271 | } 272 | 273 | return String((decoding: data, as: UTF8.self) 274 | } 275 | ``` 276 | 277 | 서버 응답이 성공하면 String 을 반환합니다. 278 | 279 | 이전 예제와 동일하게 데이터를 비동기적으로 가져오고, status code 를 확인합니다. 280 | 281 | 그 이후 String 으로 디코딩하여 반환합니다. 282 | 283 | ### 서버 상태 보여주기 284 | 285 | ListView.swift 의 task 안에 아래 코드를 추가합니다. 286 | 287 | footer 영역에서 status 가 보여지는 것을 확인할 수 있습니다. 288 | 289 | ```swift 290 | status = try await model.status() 291 | ``` 292 | 293 | ## 비동기 호출 그룹화하기 294 | 295 | 지금까지 ListView.swift 의 task 안에 작성한 코드를 살펴봅시다. 296 | 297 | ```swift 298 | files = try await model.availableFiles() 299 | status = try await model.status() 300 | ``` 301 | 302 | 두 호출은 비동기적이고 이론적으로는 동시에 발생할 수 있습니다. 303 | 304 | 하지만 await 으로 사용하게 되면 `availableFiles()`에 대한 요청이 완료될 때까지 `status()` 호출이 시작되지 않습니다. 305 | 306 | 307 | 308 | 첫번째 호출의 반환값을 두번째 호출의 파라미터로 사용하려는 경우, 위 사진처럼 순차적으로 비동기 함수를 실행해야합니다. 309 | 310 | 하지만 여기서는 그렇지 않습니다. 311 | 312 | 서버 상태와 파일 목록은 서로 의존적이지 않기 때문에 두 호출은 동시에 할 수 있습니다. 313 | 314 | Swift 는 structured concurrency 의 `async let` 구문을 이용해 이 문제를 해결할 수 있습니다. 315 | 316 | ### async let 사용하기 317 | 318 | Swift 는 여러 비동기 호출을 그룹화해서 한 번에 기다릴 수 있는 구문을 제공합니다. 319 | 320 | task 안의 코드를 모두 제거하고, `async let` 구문을 사용해봅시다. 321 | 322 | ```swift 323 | guard files.isEmpty else { return } 324 | 325 | do { 326 | async let files = try model.availableFiles() 327 | async let status = try model.status() 328 | } catch { 329 | lastErrorMessage = error.localizedDescription 330 | } 331 | ``` 332 | 333 | `async let` 바인딩을 사용하면 다른 언어의 promise 개념과 유사한 지역 상수를 만들 수 있습니다. 334 | 335 | files, status 바인딩은 특정 타입의 값이나 에러를 나중에 사용가능하다고 약속합니다. 336 | 337 | 바인딩 결과를 읽으려면 `await` 을 사용해야 합니다. 이미 사용 가능한 값이라면 즉시 받을 수 있고, 그렇지 않다면 결과가 사용가능해질 때까지 일시 중단됩니다. 338 | 339 | 다음 사진은 값을 즉시 얻어오는 상황과, 중단된 상황에 대한 설명입니다. 340 | 341 | 342 | 343 | ### 두 요청에서 값 추출하기 344 | 345 | 마지막으로 추가한 코드를 보면 `await` 을 호출하기 전에 두 async 코드가 실행됩니다. 따라서 `status()` 와 `availableFiles()` 는 task 안에서 main 코드와 병렬로 실행됩니다. 346 | 347 | 두 비동기 코드를 그룹화해서 값을 가져오는 방식은 두가지 입니다. 348 | 349 | - Collection (e.g Array) 로 그룹화합니다. 350 | - 튜플로 그룹화합니다. 351 | 352 | 두 문법은 상호 교환 가능합니다. 예제에서는 바인딩이 두 개이므로 튜플을 사용합니다. 353 | 354 | ```swift 355 | let (filesResult, statusResult) = try await (files, status) 356 | ``` 357 | 358 | ## 비동기로 파일 다운로드 하기 359 | 360 | [SuperStorageModel.swift](https://github.com/raywenderlich/mcon-materials/blob/editions/1.0/02-beginner-async-await/projects/starter/SuperStorage/Model/SuperStorageModel.swift) 파일의 `download(file:)` 라는 메서드를 살펴봅시다. 361 | 362 | 다운로드를 위한 endpoint URL 을 생성하고, 빈 데이터를 반환하고 있습니다. 363 | 364 | `SuperStorageModel` 에는 앱 다운로드를 관리하는 두 개의 메서드가 있습니다. 365 | 366 | - addDownload(name:) : 진행 중인 다운로드 목록에 파일을 추가합니다. 367 | - updateDownload(name:progress:) : 파일의 progress 를 업데이트합니다. 368 | 369 | 이 두가지 메서드를 사용해 모델과 UI 를 업데이트합니다. 370 | 371 | ### 데이터 다운로드 372 | 373 | `download(file:)` 에 다음 코드를 추가합니다. 374 | 375 | ```swift 376 | addDownload(name: file.name) 377 | 378 | let (data, response) = try await URLSession.shared.data(from: url, delegate: nil) 379 | 380 | updateDownload(name: file.name, progress: 1.0) 381 | 382 | guard (response as? HTTPURLResponse)?.statusCode == 200 else { 383 | throw "The server responded with an error." 384 | } 385 | 386 | return data 387 | ``` 388 | 389 | `addDownload` 은 모델 클래스의 download 속성에 파일을 추가합니다. DownloadView 는 이를 진행 중인 다운로드 상태를 화면에 표시하는데 사용합니다. 390 | 391 | 다음 URLSession 을 활용해 데이터를 불러오고, `updateDownload` 를 사용해 진행률을 1.0 으로 업데이트 합니다. 392 | 393 | 여기서 진행률은 0% 에서 바로 100% 로 업데이트하기 때문에 유용하지 않습니다. 다음 챕터에서 이 기능을 개선할 예정입니다. 394 | 395 | ### 다운로드 버튼 396 | 397 | 398 | 399 | Silver 버튼에 대한 동작을 구현해야 합니다. 400 | 401 | 지금까지는 SwiftUI 의 task() 를 활용했습니다. 402 | 403 | 비동기 코드를 허용하지 않는 downloadSingleAction 클로저에서는 어떻게 `download(file:)` 을 사용할 수 있을까요? 404 | 405 | ```swift 406 | fileData = try await model.download(file: file) 407 | ``` 408 | 409 | 410 | 411 | 위 코드를 바로 사용하게 되면 위와 같은 에러가 발생합니다. 412 | 413 | 코드는 `() async throws -> Void` 타입이지만, 파라미터는 `() -> Void` 를 동기 클로저 타입을 기대합니다. 414 | 415 | 실행 가능한 방법 중 하나는 FileDetails 에서 async closure 를 허용하도록 변경하는 것입니다. 416 | 417 | 하지만 이 코드에 접근할 수 없다면 어떻게 할 수 있을까요? 다행히 다른 방법이 있습니다. 418 | 419 | ## non-async 컨텍스트에서 비동기 요청 실행하기 420 | 421 | ```swift 422 | Task { 423 | fileData = try await model.download(file: file) 424 | } 425 | ``` 426 | 427 | 기존 코드를 위처럼 변경합니다. 428 | 429 | 여기서 사용한 `Task` 타입은 무엇일까요? 430 | 431 | ## Task 알아보기 432 | 433 | `Task` 는 최상위 비동기 작업을 나타내는 타입입니다. 최상위 수준은 sync context 에서 시작할 수 있는 async context 를 만들 수 있다는 것을 의미합니다. 434 | 435 | 간단히 말해서, sync context 에서 비동기 코드를 실행하려고 할 때 새로운 Task 가 필요합니다. 436 | 437 | 다음 API 를 사용해서 작업 실행을 수동으로 제어할 수 있습니다. 438 | 439 | - **Task(priority:operation)** : 지정된 우선 순위로 비동기 실행을 위한 작업을 예약합니다. 지정된 우선순위를 nil로 설정하면 현재 sync context 에서 기본값을 상속합니다. 440 | - **Task.detached(priority:operation)** : 호출 context 의 기본값을 상속하지 않는다는 점을 제외하고 Task(priority:operation) 와 유사합니다. 441 | - **Task.value** : 작업이 완료될 때까지 기다리고, 값을 반환합니다. (다른 언어의 Promise 와 유사) 442 | - **Task.isCancelled** : 마지막 중단 지점 이후 작업이 취소된 경우 true 를 반환합니다. 이 값을 통해 예약된 작업의 실행을 중단해야 하는 시점을 알 수 있습니다. 443 | - **Task.checkCancellation()** : 작업이 취소된 경우 CancellationError 를 발생시킵니다. 444 | - **Task.sleep(nanoseconds:)** : 쓰레드를 블락하지 않고 nanoseconds 만큼 기다립니다. 445 | 446 | 이전 예제에서 `Task(priority:operation:)` 를 사용했습니다. 447 | 448 | 기본적으로 Task 는 현재 context 에서 우선 순위를 상속하기 때문에 생략할 수 있습니다. 449 | 450 | 우선 순위를 변경하려는 경우 priority 를 지정하면 됩니다. 451 | 452 | ### 다른 Actor 로 부터 새로운 Task 만들기 453 | 454 | `Task(priority:operation:)` 를 사용하면 task 를 호출한 actor 에서 실행됩니다. 455 | 456 | actor 의 일부가 아닌 동일한 Task 를 생성하려면 `Task.detached(priority:operation:)` 를 사용하면 됩니다. 457 | 458 | > 참고 : actor 는 이후 챕터에서 다룰 예정입니다. 459 | 460 | 지금은 코드가 메인 쓰레드에서 Task 를 생성할 때, 해당 Task 는 메인 쓰레드에서 실행된다고 이해하면 됩니다. 461 | 462 | 따라서 앱의 UI 를 안전하게 업데이트할 수 있습니다. 463 | 464 | 앱을 실행해서 JPEG 파일을 선택하고 Silver plan 버튼을 클릭하면, progress 가 표시되고 이미지의 프리뷰가 표시됩니다. 465 | 466 | 467 | 468 | 그러나 progress 가 흔들리고 때때로 중간까지만 채워지는 것을 확인할 수 있습니다. 469 | 470 | 이를 통해 백그라운드 쓰레드에서 UI 를 업데이트하고 있다는 것을 알 수 있습니다. Xcode 콘솔을 보면 로그 메시지와 함께 보라색 경고를 볼 수 있습니다. 471 | 472 | ## 메인 쓰레드에서 코드를 동작시키기 473 | 474 | `MainActor.run()` 를 사용해 코드를 메인 쓰레드에서 동작시킬 수 있습니다. 475 | 476 | `MainActor` 는 메인 쓰레드에서 코드를 실행하는 타입입니다. 이것은 기존에 사용했던 `DispatchQueue.main` 의 대안입니다. 477 | 478 | `MainActor.run()` 를 자주 사용하면 코드에 클로저가 많아져 가독성이 떨어집니다. 479 | 480 | 좀 더 좋은 방법은 `@MainActor` annotation 을 사용하는 것입니다. 이를 사용하면 주어진 함수나 프로퍼티의 호출을 자동으로 메인쓰레드로 전환합니다. 481 | 482 | ### @MainActor 사용하기 483 | 484 | 메인 쓰레드에서 UI 변경이 일어나도록 [SuperStorageModel.swift](https://github.com/raywenderlich/mcon-materials/blob/editions/1.0/02-beginner-async-await/projects/starter/SuperStorage/Model/SuperStorageModel.swift) 파일의 다운로드를 업데이트 하는 메서드에 annotation 을 추가합니다. 485 | 486 | ```swift 487 | @MainActor func addDownload(name: String) 488 | 489 | @MainActor func updateDownload(name: String, progress: Double) 490 | ``` 491 | 492 | 이 두 메서드에 대한 모든 호출은 메인 액터, 즉 메인 쓰레드에서 자동으로 실행됩니다. 493 | 494 | ### 비동기로 메서드 실행하기 495 | 496 | 위 변경으로 컴파일 에러가 발생합니다. 497 | 498 | 두 메서드를 특정 Actor 에서 실행하려면 비동기적으로 호출해야 합니다. 499 | 500 | addDownload, updateDownload 를 사용하는 코드에 await 을 추가하면 에러가 해결됩니다. 501 | 502 | ```swift 503 | await addDownload(name: file.name) 504 | 505 | await updateDownload(name: file.name, progress: 1.0) 506 | ``` 507 | 508 | ## 다운로드 화면 업데이트하기 509 | 510 | 현재까지 작성한 코드에 문제가 있습니다. 511 | 512 | 파일 목록 화면으로 돌아가서, 다른 파일을 선택하면 다운로드 화면에 이전 다운로드 progress 가 계속 표시됩니다. 513 | 514 | `onDisappear()` 에서 모델을 초기화 해서이 문제를 해결할 수 있습니다. [DownloadView.swift](https://github.com/raywenderlich/mcon-materials/blob/editions/1.0/02-beginner-async-await/projects/starter/SuperStorage/DownloadView.swift) 의 toolbar 아래에 코드를 추가해 봅시다. 515 | 516 | ```swift 517 | .onDisappear { 518 | fileData = nil 519 | model.reset() 520 | } 521 | ``` 522 | 523 | --- 524 | 525 | ### 이미지 리소스 출처 및 원문 526 | 527 | - https://www.raywenderlich.com/books/modern-concurrency-in-swift/v1.0/chapters/2-getting-started-with-async-await 528 | -------------------------------------------------------------------------------- /chapters/03_AsyncSequence_and_Intermediate_Task.md: -------------------------------------------------------------------------------- 1 | # AsyncSequence & Intermediate Task 2 | 3 | ## AsyncSequence 알아보기 4 | 5 | AsyncSequence 는 Sequence 와 유사하게 비동기 요소를 생성할 수 있는 프로토콜 입니다. 6 | 7 | Sequence 와 동일하지만, 다음 요소를 즉시 사용할 수 없기 때문에 기다려야 합니다. 8 | 9 | ```swift 10 | for try await item in asyncSequence { 11 | ... 12 | } 13 | ``` 14 | 15 | AsyncSequence 를 사용한 일반적인 코드입니다. await 을 사용해 for 문안의 Sequence 를 반복합니다. (throw 처리가 있는 경우 try 를 사용합니다) 각 루프를 반복할 때 다음 값을 얻기 위해 중단됩니다. 16 | 17 | ```swift 18 | var iterator = asyncSequence.makeAsyncIterator() 19 | 20 | while let item = try await iterator.next() { 21 | ... 22 | } 23 | ``` 24 | 25 | while 루프 문과도 사용할 수 있습니다. interactor 를 만들고 Sequence 가 끝날 때까지 await 을 사용해 반복적으로 next() 를 호출합니다. 26 | 27 | ```swift 28 | for await item in asyncSequence 29 | .dropFirst(5) 30 | .prefix(10) 31 | .filter { $0 > 10 } 32 | .map { "Item: \($0)" } { 33 | ... 34 | } 35 | ``` 36 | 37 | 표준 Sequence 메서드인 `dropFirst(_:)`, `prefix(_:)`, `filter(_:)` 등을 사용할 수 있습니다. 38 | 39 | AsyncSequence 를 따르는 커스텀 Sequence 타입을 만들거나, AsyncStream 을 활용해 기존 코드를 AsyncSequence 로 변경할 수 있습니다. 40 | 41 | 42 | 43 | ## AsyncSequence 시작하기 44 | 45 | 이전 챕터에서는 파일을 한 번에 가져온 뒤 preview 를 보여주는 동작을 구현했습니다. 46 | 47 | 이번 챕터에서는 파일이 다운로드될 때, 점진적으로 UI 를 업데이트하도록 구현합니다. 48 | 49 | 파일을 서버에서 byte AsyncSequence 로 읽어 프로그레스바를 업데이트할 수 있습니다. 50 | 51 | ### Asynchronous sequence 추가하기 52 | 53 | [SuperStorageModel.swift](https://github.com/raywenderlich/mcon-materials/blob/editions/1.0/03-async-sequences/projects/starter/SuperStorage/Model/SuperStorageModel.swift) 의 `downloadWithProgress(fileName:name:size:offset:)` 를 확인합니다. 54 | 55 | 위 코드에는 URL 을 만드는 코드와 `addDownload(name:)` 를 호출해 화면에 다운로드를 추가하는 코드가 작성되어 있습니다. 56 | 57 | 이 곳에 asynchronous sequence 를 추가하기 앞서 먼저 URLSession 의 API 에 대해 알아봅시다. 58 | 59 | ```swift 60 | guard let url = URL(string: "test.com") else { return } 61 | 62 | let result = try await URLSession.shared.data(from: url) 63 | ``` 64 | 65 | `data(for:delegate:)` 메서드를 사용하면 Data 전체를 불러와 반환합니다. 66 | 67 | ```swift 68 | guard let url = URL(string: "test.com") else { return } 69 | 70 | // let result: (downloadStream: URLSession.AsyncBytes, response: URLResponse) 71 | let result = try await URLSession.shared.bytes(for: urlRequest) 72 | ``` 73 | 74 | 반면 `bytes(for:delegate:)` 메서드를 사용하면 `URLSession.AsyncBytes` 를 반환합니다. 75 | 76 | `URLSession.AsyncBytes` 는 sequence 로, URL 요청에서 받은 byte 를 비동기적으로 제공합니다. 77 | 78 | 서버는 HTTP 프로토콜을 통해 부분 요청을 지원할 수 있습니다. 서버가 이를 지원하는 경우, 전체 응답을 한 번에 받지 않고 응답의 바이트 범위를 반환하도록 요청할 수 있습니다. 79 | 80 | 앱에서 부분 요청과 일반 요청을 모두 지원하는 예제를 작성해봅시다. 81 | 82 | ```swift 83 | // SuperStorageModel.swift 84 | 85 | guard let url = URL(string: "http://localhost:8080/files/download?\(fileName)") else { 86 | throw "Could not create the URL." 87 | } 88 | 89 | let result: (downloadStream: URLSession.AsyncBytes, response: URLResponse) 90 | 91 | if let offset = offset { 92 | // 부분 요청 93 | let urlRequest = URLRequest(url: url, offset: offset, length: size) 94 | 95 | result = try await URLSession.shared.bytes(for: urlRequest) 96 | 97 | guard (result.response as? HTTPURLResponse)?.statusCode == 206 else { 98 | throw "The server responded with an error." 99 | } 100 | } else { 101 | // 전체 요청 102 | result = try await URLSession.shared.bytes(from: url) 103 | } 104 | ``` 105 | 106 | offset 이 있는 경우 그를 활용해 URLRequest 를 만들어 `URLSession.bytes(for:delegate:)` 에 사용합니다. 107 | 108 | 그 다음 status code 가 성공적인 부분 응답을 나타내는 206 인지 확인합니다. 109 | 110 | 부분 요청, 일반 요청 모두 `URLSession.AsyncBytes` 를 사용할 수 있습니다. offset 이 없는 경우 `URLSession.bytes(from:delegate:)` 를 사용합니다. 111 | 112 | 113 | 114 | ### ByteAccumulator 사용하기 115 | 116 | 117 | 118 | 이제 [ByteAccumulator](https://github.com/raywenderlich/mcon-materials/blob/editions/1.0/03-async-sequences/projects/starter/SuperStorage/Model/ByteAccumulator.swift) 라는 타입을 사용해 sequence 로 부터 batches of byte 를 불러올 수 있습니다. 119 | 120 | ByteAccumulator 를 사용하기 위해 [SuperStorageModel.swift](https://github.com/raywenderlich/mcon-materials/blob/editions/1.0/03-async-sequences/projects/starter/SuperStorage/Model/SuperStorageModel.swift) 의 `downloadWithProgress(fileName:name:size:offset:) ` 메서드 return 전에 아래의 코드를 추가합니다. 121 | 122 | ```swift 123 | // SuperStorageModel.swift 124 | 125 | var asyncDownloadIterator = result.downloadStream.makeAsyncIterator() 126 | ``` 127 | 128 | AsyncSequence 는 sequence 에 대해 async iterator를 반환하는 `makeAsyncIterator()` 메서드를 제공합니다. 129 | 130 | `asyncDownloadIterator ` 를 활용해 bytes 를 하나씩 반복할 수 있습니다. 131 | 132 | 133 | 134 | 이제 모든 바이트를 수집하는 `accumulator` 를 추가합니다. 135 | 136 | ```swift 137 | // SuperStorageModel.swift 138 | 139 | let accumulator = ByteAccumulator(name: name, size: size) 140 | 141 | // 1번 루프 : 다운로드가 멈추지 않았는지, accumulator 가 bytes 를 더 수집할 수 있는지 142 | while !stopDownloads, !accumulator.checkCompleted() { 143 | // 2번 루프 : 배치가 모두 차거나, sequence 가 완료될 때까지 144 | while !accumulator.isBatchCompleted, 145 | let byte = try await asyncDownloadIterator.next() { 146 | accumulator.append(byte) 147 | } 148 | } 149 | ``` 150 | 151 | **1번 루프** 152 | 153 | - 1번 루프를 먼저 살펴보면 2가지의 조건을 가지고 있습니다. 154 | 155 | - 이 두가지 조건을 활용하면 다운로드가 취소되지 않을 때 다운로드가 완료될 때까지 루프문을 유지할 수 있습니다. (취소는 이후에 다뤄보겠습니다.) 156 | 157 | 158 | 159 | **2번 루프** 160 | 161 | - 2번 루프 또한 2가지의 조건을 가지고 있습니다. 162 | 163 | - accumulator 의 배치가 모두 차거나, sequence 가 완료될 때까지 accumulator 는 bytes 를 수집합니다. 164 | 165 | 166 | 167 | ### progress bar 업데이트하기 168 | 169 | 2번째 while 문 밑에 아래의 코드를 추가해 progress bar 를 업데이트 합니다. 170 | 171 | ```swift 172 | // SuperStorageModel.swift 173 | 174 | let progress = accumulator.progress 175 | Task.detached(priority: .medium) { 176 | await self.updateDownload(name: name, progress: progress) 177 | } 178 | 179 | print(accumulator.description) 180 | ``` 181 | 182 | 위 코드에서 낯선 키워드에 대해 살펴보겠습니다. 183 | 184 | **Task.detached(...)** 185 | 186 | 이것은 독자적으로 행동하는 Task 를 만드는 코드입니다. 187 | 188 | 이로 생성된 Task 는 부모의 priority, task storage, execution actor 를 상속받지 않습니다. 189 | 190 | > Task.detached(...) 는 concurrency model 의 효율성에 부정적인 영향을 미쳐 사용하지 않는 것을 권장합니다. 191 | 192 | 위 예제의 경우 priority 를 medium 으로 할당하여, 다운로드 Task 의 작업 속도가 느려질 가능성은 없습니다. 193 | 194 | 195 | 196 | 위의 과정을 통해 완성된 코드는 다음과 같습니다. 197 | 198 | ```swift 199 | // SuperStorageModel.swift 200 | 201 | while !stopDownloads, !accumulator.checkCompleted() { 202 | while !accumulator.isBatchCompleted, 203 | let byte = try await asyncDownloadIterator.next() { 204 | accumulator.append(byte) 205 | } 206 | 207 | let progress = accumulator.progress 208 | Task.detached(priority: .medium) { 209 | await self.updateDownload(name: name, progress: progress) 210 | } 211 | 212 | print(accumulator.description) 213 | } 214 | ``` 215 | 216 | 217 | 218 | ### 누적 결과 반환하기 219 | 220 | `return Data()` 를 아래의 코드로 대체해 결과를 반환합니다. 221 | 222 | ```swift 223 | // SuperStorageModel.swift 224 | 225 | return accumulator.data 226 | ``` 227 | 228 | 이제 완성된 메서드는 다운로드 시퀀스를 반복해 모든 데이터를 수집하고, 각 배치가 완료될 때 progress 를 업데이트 합니다. 229 | 230 | [DownloadView.swift](https://github.com/raywenderlich/mcon-materials/blob/editions/1.0/03-async-sequences/projects/starter/SuperStorage/DownloadView.swift) 로 이동해 downloadWithUpdatesAction 클로저 파라미터에 아래의 코드를 추가합니다. 231 | 232 | ```swift 233 | // DownloadView.swift 234 | 235 | downloadWithUpdatesAction: {} 236 | isDownloadActive = true 237 | Task { 238 | do { 239 | fileData = try await model.downloadWithProgress(file: file) 240 | } catch { } 241 | isDownloadActive = false 242 | } 243 | } 244 | ``` 245 | 246 | 위 코드는 이전 챕터에서 추가한 downloadSingleAction 와 유사하지만 downloadWithProgress 를 호출한다는 점이 다릅니다. 247 | 248 | 이제 앱을 실행하고 골드 버튼을 눌러 결과를 확인해봅시다. 249 | 250 | 251 | 252 | ## Task 취소하기 253 | 254 | Concurrency model 이 효율적으로 작동하기 위해 불필요한 Task 를 취소하는 것은 필수적입니다. 255 | 256 | 이후에 학습할 TaskGroup 또는 async let 을 사용하면, 시스템이 필요에 따라 Task 를 자동으로 취소할 수 있습니다. 257 | 258 | 하지만 아래의 Task API 를 사용해 더 세분화된 취소 전략을 구현할 수 있습니다. 259 | 260 | - Task.isCancelled 261 | - task가 취소되었다면 true를 반환합니다 262 | - Task.currentPriority 263 | - 현재 task 의 우선순위를 반환합니다. 264 | - Task.cancel() 265 | - task와 그 task의 하위 tasks들도 취소합니다 266 | - Task.checkCancellation() 267 | - task 가 취소되었다면 CancellationError를 반환합니다 268 | - Task.yield() 269 | - 현재 작업의 실행을 미루고 시스템에게 우선순위 높은 일을 처리하기 위한 기회를 줍니다. 270 | 271 | 272 | 273 | 비동기 코드를 작성할 때 throw 가 필요한지 control flow 를 직접 확인할지에 따라 사용할 API 를 선택할 수 있습니다. 274 | 275 | 276 | 277 | ## async task 취소하기 278 | 279 | 다운로드 중에 뒤로가기 버튼을 눌러도 계속해서 불필요한 다운로드가 진행되는 것을 콘솔에서 확인할 수 있습니다. 280 | 281 | 282 | 283 | ## 수동으로 Task 취소하기 284 | 285 | 지금까지 .taks(...) 안에 비동기 코드를 작성했는데, 이는 뷰가 사라지면 자동으로 코드를 취소해주었습니다. 286 | 287 | 하지만 이번에 추가한 다운로드 버튼에 대한 동작은 .task() 안에 작성하지 않아서 수동으로 Task 를 취소해야 합니다. 288 | 289 | 우선 [DownloadView](https://github.com/raywenderlich/mcon-materials/blob/editions/1.0/03-async-sequences/projects/starter/SuperStorage/DownloadView.swift) 에 아래의 프로퍼티를 추가합니다. 290 | 291 | ```swift 292 | // DownloadView.swift 293 | 294 | @State var downloadTask: Task? 295 | ``` 296 | 297 | downloadTask 에 반환값이 없고 에러가 발생할 수 있는 비동기 Task 을 할당합니다. (Task 타입) 298 | 299 | > Task 는 다른 타입과 동일하게 View, Model 혹은 다른 범위에서 저장할 수 있습니다. 300 | 301 | 302 | 303 | downloadWithUpdatesAction 의 `Task {` 를 아래와 같이 변경합니다. 304 | 305 | ```swift 306 | // DownloadView.swift 307 | 308 | downloadTask = Task { 309 | ``` 310 | 311 | 이제 Task 가 downloadTask 에 할당되어 원하는 대로 Task 를 취소할 수 있습니다 312 | 313 | 사용자가 메인 화면으로 돌아갈 때 취소시키기 위해 onDisappear(...) 의 model.reset() 코드 다음에 아래의 코드를 추가합니다. 314 | 315 | ```swift 316 | // DownloadView.swift 317 | 318 | downloadTask?.cancel() 319 | ``` 320 | 321 | downloadTask 를 취소하면 모든 하위 작업들까지 취소됩니다. 322 | 323 | 다시 앱을 실행해서 확인해보면, 메인 화면으로 돌아갈 때 콘솔의 로그가 나오지 않는 것을 확인할 수 있습니다. 324 | 325 | 326 | 327 | ## Task 에 상태 저장하기 328 | 329 | 각 비동기 Task 는 priority, actor 등으로 구성된 개별 context 에서 실행됩니다. 330 | 331 | 이러한 Task 는 다른 Task 를 호출할 수 있습니다. 332 | 333 | 각 Task 는 여러 함수들과 상호 작용할 수 있기 때문에, 런타임에서 공유 데이터를 격리하는 것은 어려울 수 있습니다. 334 | 335 | 336 | 337 | 이를 위해 Swift 는 주어진 속성을 task-local 로 표시하는 `@TaskLocal` property wrapper 를 제공합니다. 338 | 339 | SwiftUI 의 environment 에 객체를 주입하는 순간을 생각하면, 그 객체를 해당 View 뿐만 아니라 모든 child views 에서 사용할 수 있도록 합니다. 340 | 341 | 마찬가지로 TaskLocal 을 바인딩하면 해당 Task 뿐만 아니라 하위 Task 에서도 사용할 수 있습니다. 342 | 343 | 344 | 345 | 346 | 347 | 이제 task-local storage 를 사용해 호출 컨텍스트에 따라 함수의 동작을 변경하는 방법을 배우게 됩니다. 348 | 349 | 더 구체적으로, 다운로드 화면의 Cancel All 버튼에 대한 동작을 구현합니다. 350 | 351 | 352 | 353 | 354 | 355 | ### partial image preview 추가하기 356 | 357 | progressive jpeg 를 사용해 이미지를 부분적으로 디코딩할 수 있습니다. 358 | 359 | 사용자가 jpeg 이미지를 다운로드하다가 취소한 경우, 부분적으로 다운로드된 미리보기가 표시됩니다. 360 | 361 | [SuperStorageModel](https://github.com/raywenderlich/mcon-materials/blob/editions/1.0/03-async-sequences/projects/starter/SuperStorage/Model/SuperStorageModel.swift) 파일을 열고 새 프로퍼티를 추가합니다. 362 | 363 | ```swift 364 | // SuperStorageModel.swift 365 | 366 | @TaskLocal static var supportsPartialDownloads = false 367 | ``` 368 | 369 | 사용자가 jpeg 다운로드를 시작하면 supportsPartialDownloads 를 true 로 설정합니다. 370 | 371 | 그 다음 SuperStorageModel 에 코드를 추가해 플래그 값에 따른 적절한 동작을 제공합니다. 372 | 373 | > Task-local 프로퍼티는 static 같은 글로벌 프로퍼티에서 사용 가능합니다. 374 | 375 | @TaskLocal property wrapper 는 async task 에 값을 바인딩하거나, task 계층에 주입할 수 있는 withValue() 메서드를 제공합니다. 376 | 377 | DownloadView 파일의 downloadWithUpdatesAction 클로저 파라미터의 `fileData = try await model.downloadWithProgress(file: file)` 를 아래의 코드로 변경합니다. 378 | 379 | ```swift 380 | // DownloadView.swift 381 | 382 | downloadWithUpdatesAction: { 383 | ... 384 | try await SuperStorageModel 385 | .$supportsPartialDownloads 386 | .withValue(file.name.hasSuffix(".jpeg")) { 387 | fileData = try await model.downloadWithProgress(file: file) 388 | } 389 | } 390 | ``` 391 | 392 | 이처럼 TaskLocal 의 withValue 를 활용해 partialDownload 지원 여부를 바인딩합니다. 393 | 394 | 값이 바인딩되면 downloadWithProgress(file:) 을 호출합니다. 395 | 396 | 이러한 방식으로 아래처럼 여러값을 바인딩할 수 있고, 내부 바인딩 값을 덮어쓸 수 있습니다. 397 | 398 | ```swift 399 | try await $property1.withValue(myData) { 400 | ... 401 | try await $property2.withValue(myConfig1) { 402 | ... 403 | try await serverRequest() 404 | try await $property2.withValue(myConfig2) { 405 | ... 406 | } 407 | } 408 | } 409 | ``` 410 | 411 | 너무 많은 값을 바인딩하면 읽고 추론하기 어려울 수 있습니다. 412 | 413 | task storage 는 위처럼 단일 값이나 플래그를 분리해서 사용하는 것보다, 데이터 모델을 사용해 적은 값을 바인딩하는 것이 유용합니다. 414 | 415 | 416 | 417 | ### Cancel All 기능 추가하기 418 | 419 | DownloadView.swift 파일의 .toolbar(...) 코드로 이동해 Cancel All 버튼을 확인합니다. 420 | 421 | action 클로저에 아래의 코드를 추가합니다. 422 | 423 | ```swift 424 | // DownloadView.swift 425 | 426 | model.stopDownloads = true 427 | ``` 428 | 429 | 이전처럼 onDisappear 에서 다운로드 작업을 취소하는 것이 아니라 stopDownloads 플래그 값을 변경합니다. 430 | 431 | 다운로드하는 동안 이 플래그 값을 옵저빙하고, true 로 변경되면 Task 를 취소해야한다는 것을 알 수 있습니다. 432 | 433 | SuperStorageModel 파일을 열고 downloadWithProgress(fileName:name:size:offset:) 함수의 return 위에 아래의 코드를 추가합니다. 434 | 435 | ```swift 436 | // SuperStorageModel.swift 437 | 438 | if stopDownloads, !Self.supportsPartialDownloads { 439 | throw CancellationError() 440 | } 441 | ``` 442 | 443 | 커스텀한 취소 동작에 대한 동작입니다. 444 | 445 | 각 다운로드 배치 후에 stopDownloads 값이 true 인지 확인하고, 맞다면 partial preview 를 지원하는지 확인합니다. 446 | 447 | - Self.supportsPartialDownloads 가 false 라면 CancellationError 를 던져 다운로드를 즉시 중지합니다. 448 | - Self.supportsPartialDownloads 가 true 라면 부분적으로 다운로드한 파일 콘텐츠를 반환합니다. 449 | 450 | 다시 앱을 실행해서 동작을 확인해봅시다. 451 | 452 | 453 | 454 | 455 | 456 | 이처럼 progress bar 를 업데이트하지 않고 스피너를 숨기며 다운로드가 중단됩니다. 457 | 458 | 459 | 460 | jpeg 파일의 경우 위의 동작을 포함해 다운로드한 부분의 미리보기를 보여줍니다. 461 | 462 | 463 | 464 | ## Combine 과 AsyncSequence 연결하기 465 | 466 | 애플은 Combine 을 SwiftUI, Foundation, CoreData 등 여러 프레임워크에 통합하였습니다. 467 | 468 | 469 | 470 | Combine 의 Publisher 는 아래처럼 비동기로 값을 전달하고, 선택적으로 success 혹은 failure 이벤트를 통해 완료할 수 있습니다. 471 | 472 | 473 | 474 | 애플은 사용자가 작성한 Combine 코드를 사용해 async/await 기능을 사용할 수 있도록 쉬운 인터페이스를 제공합니다. 475 | 476 | 477 | 478 | ### progress timer 추가하기 479 | 480 | 다운로드 시간을 실시간으로 보여주는 타이머를 추가합니다. 481 | 482 | isDownloadActive 속성이 true 로 변경될 때마다 비동기 Task 를 만들고, Task 에서 Combine Timer 를 생성해 UI 를 주기적으로 업데이트 하는 것이 목표입니다. 483 | 484 | 먼저 DownloadView 맨 위에 import 를 추가합니다. 485 | 486 | ```swift 487 | // DownloadView.swift 488 | 489 | import Combine 490 | ``` 491 | 492 | 그 다음 DownloadView 에 작업을 취소할 수 있는 Task 속성을 추가합니다. 493 | 494 | ```swift 495 | // DownloadView.swift 496 | 497 | @State var timerTask: Task? 498 | ``` 499 | 500 | downloadTask 가 변경되면 timerTask 를 취소하고, 조건에 따라 새로운 timerTask 를 생성합니다. 501 | 502 | ```swift 503 | // DownloadView.swift 504 | 505 | @State var downloadTask: Task? { 506 | didSet { 507 | timerTask?.cancel() 508 | guard isDownloadActive else { return } 509 | let startTime = Date().timeIntervalSince1970 510 | } 511 | } 512 | ``` 513 | 514 | startTime 은 이후에 타이머의 시작 시간을 기준으로 duration 을 계산할 때 사용합니다. 515 | 516 | 517 | 518 | ### Combine-based timer 생성하기 519 | 520 | Combine Timer 를 생성하고 async value 속성을 이용해 사용할 수 있도록 만듭니다. 521 | 522 | 아래의 코드를 추가해서 startTime 세팅 후 Timer publisher 를 생성합니다. 523 | 524 | ```swift 525 | // DownloadView.swift 526 | 527 | let timerSequence = Timer 528 | .publish(every: 1, tolerance: 1, on: .main, in: .common) 529 | .autoconnect() 530 | .map { date -> String in 531 | let duration = Int(date.timeIntervalSince1970 - startTime) 532 | return "\(duration)s" 533 | } 534 | .values 535 | ``` 536 | 537 | 위 코드를 라인 별로 살펴봅시다. 538 | 539 | - Timer.publish : 1초에 한 번씩 이벤트를 보내는 Combine Publisher 를 생성합니다. 540 | - autoconnect : 구독하면 이벤트를 발행합니다. 541 | - map : date 를 계산해 duration 으로 변경합니다. 542 | - values: Publisher 를 AsyncSequence 로 변경합니다. (가장 중요) 543 | 544 | 545 | 546 | Combine Publisher 의 values 속성에 접근하면 await 과 함께 사용할 수 있습니다. 547 | 548 | values 는 Publisher 를 AsyncSequence 로 자동 랩핑합니다. 549 | 550 | > Combine의 Future 도 value 라는 async value 를 제공합니다. 551 | 552 | 553 | 554 | ### 타이머 마무리 555 | 556 | 마지막으로 timerTask 를 생성하고 시퀀스를 반복합니다. 557 | 558 | ```swift 559 | // DownloadView.swift 560 | 561 | timerTask = Task { 562 | for await duration in timerSequence { 563 | self.duration = duration 564 | } 565 | } 566 | ``` 567 | 568 | 다시 앱을 실행해서 타이머 동작을 확인해봅시다. 569 | 570 | 571 | 572 | 573 | 574 | 마지막으로 Cancel All 버튼을 눌러도 타이머가 계속 실행된다는 것을 알 수 있습니다. 575 | 576 | Button 의 action 클로저의 다음 코드를 추가합니다. 577 | 578 | ```swift 579 | // DownloadView.swift 580 | 581 | timerTask?.cancel() 582 | ``` 583 | 584 | 이제 Cancel All 버튼을 누르면 timerTask 가 취소됩니다. 585 | 586 | 587 | 588 | --- 589 | 590 | #### 이미지 리소스 출처 및 원문 591 | 592 | - https://www.raywenderlich.com/books/modern-concurrency-in-swift/v1.0/chapters/3-asyncsequence-intermediate-task 593 | -------------------------------------------------------------------------------- /chapters/04_Custom_Asynchronous_Sequences_With_AsyncStream.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OhKanghoon/swift-concurrency/c2e6353618ac822e4268be36a6190f91c1ae513a/chapters/04_Custom_Asynchronous_Sequences_With_AsyncStream.md -------------------------------------------------------------------------------- /chapters/05_Intermediate_async_await_and_CheckedContinuation.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OhKanghoon/swift-concurrency/c2e6353618ac822e4268be36a6190f91c1ae513a/chapters/05_Intermediate_async_await_and_CheckedContinuation.md -------------------------------------------------------------------------------- /chapters/06_Testing_Asynchronous_Code.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OhKanghoon/swift-concurrency/c2e6353618ac822e4268be36a6190f91c1ae513a/chapters/06_Testing_Asynchronous_Code.md -------------------------------------------------------------------------------- /chapters/07_Concurrent_Code_With_TaskGroup.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OhKanghoon/swift-concurrency/c2e6353618ac822e4268be36a6190f91c1ae513a/chapters/07_Concurrent_Code_With_TaskGroup.md -------------------------------------------------------------------------------- /chapters/08_Getting_Started_With_Actors.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OhKanghoon/swift-concurrency/c2e6353618ac822e4268be36a6190f91c1ae513a/chapters/08_Getting_Started_With_Actors.md -------------------------------------------------------------------------------- /chapters/09_Global_Actors.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OhKanghoon/swift-concurrency/c2e6353618ac822e4268be36a6190f91c1ae513a/chapters/09_Global_Actors.md -------------------------------------------------------------------------------- /chapters/10_Actors_in_a_Distributed_System.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OhKanghoon/swift-concurrency/c2e6353618ac822e4268be36a6190f91c1ae513a/chapters/10_Actors_in_a_Distributed_System.md -------------------------------------------------------------------------------- /chapters/images/02-async-errors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OhKanghoon/swift-concurrency/c2e6353618ac822e4268be36a6190f91c1ae513a/chapters/images/02-async-errors.png -------------------------------------------------------------------------------- /chapters/images/02-async-group-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OhKanghoon/swift-concurrency/c2e6353618ac822e4268be36a6190f91c1ae513a/chapters/images/02-async-group-1.png -------------------------------------------------------------------------------- /chapters/images/02-async-group-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OhKanghoon/swift-concurrency/c2e6353618ac822e4268be36a6190f91c1ae513a/chapters/images/02-async-group-2.png -------------------------------------------------------------------------------- /chapters/images/02-cancellation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OhKanghoon/swift-concurrency/c2e6353618ac822e4268be36a6190f91c1ae513a/chapters/images/02-cancellation.png -------------------------------------------------------------------------------- /chapters/images/02-create-task-on-actor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OhKanghoon/swift-concurrency/c2e6353618ac822e4268be36a6190f91c1ae513a/chapters/images/02-create-task-on-actor.png -------------------------------------------------------------------------------- /chapters/images/02-download-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OhKanghoon/swift-concurrency/c2e6353618ac822e4268be36a6190f91c1ae513a/chapters/images/02-download-button.png -------------------------------------------------------------------------------- /chapters/images/02-download-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OhKanghoon/swift-concurrency/c2e6353618ac822e4268be36a6190f91c1ae513a/chapters/images/02-download-error.png -------------------------------------------------------------------------------- /chapters/images/02-partial-task-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OhKanghoon/swift-concurrency/c2e6353618ac822e4268be36a6190f91c1ae513a/chapters/images/02-partial-task-1.png -------------------------------------------------------------------------------- /chapters/images/02-partial-task-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OhKanghoon/swift-concurrency/c2e6353618ac822e4268be36a6190f91c1ae513a/chapters/images/02-partial-task-2.png -------------------------------------------------------------------------------- /chapters/images/03-byte-accumulator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OhKanghoon/swift-concurrency/c2e6353618ac822e4268be36a6190f91c1ae513a/chapters/images/03-byte-accumulator.png -------------------------------------------------------------------------------- /chapters/images/03-combine-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OhKanghoon/swift-concurrency/c2e6353618ac822e4268be36a6190f91c1ae513a/chapters/images/03-combine-1.png -------------------------------------------------------------------------------- /chapters/images/03-combine-publisher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OhKanghoon/swift-concurrency/c2e6353618ac822e4268be36a6190f91c1ae513a/chapters/images/03-combine-publisher.png -------------------------------------------------------------------------------- /chapters/images/03-task-cancel-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OhKanghoon/swift-concurrency/c2e6353618ac822e4268be36a6190f91c1ae513a/chapters/images/03-task-cancel-1.png -------------------------------------------------------------------------------- /chapters/images/03-task-cancel-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OhKanghoon/swift-concurrency/c2e6353618ac822e4268be36a6190f91c1ae513a/chapters/images/03-task-cancel-2.png -------------------------------------------------------------------------------- /chapters/images/03-task-local-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OhKanghoon/swift-concurrency/c2e6353618ac822e4268be36a6190f91c1ae513a/chapters/images/03-task-local-1.png -------------------------------------------------------------------------------- /chapters/images/03-task-local-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OhKanghoon/swift-concurrency/c2e6353618ac822e4268be36a6190f91c1ae513a/chapters/images/03-task-local-2.png -------------------------------------------------------------------------------- /chapters/images/03-timer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OhKanghoon/swift-concurrency/c2e6353618ac822e4268be36a6190f91c1ae513a/chapters/images/03-timer.png --------------------------------------------------------------------------------