├── .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
--------------------------------------------------------------------------------