/
index.html
288 lines (258 loc) · 36.9 KB
/
index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Getting Started with Structured Concurrency in Swift - Swift on server</title>
<meta name="description" content="Learn how to apply structured concurrency in your applications, using task groups and other structured concepts.">
<meta property="og:title" content="Getting Started with Structured Concurrency in Swift - Swift on server">
<meta property="og:description" content="Learn how to apply structured concurrency in your applications, using task groups and other structured concepts.">
<meta property="og:url" content="https://swiftonserver.com/getting-started-with-structured-concurrency-in-swift/">
<meta property="og:image" content="https://swiftonserver.com/images/assets/getting-started-with-structured-concurrency-in-swift/cover.jpg">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Getting Started with Structured Concurrency in Swift - Swift on server">
<meta name="twitter:description" content="Learn how to apply structured concurrency in your applications, using task groups and other structured concepts.">
<meta name="twitter:image" content="https://swiftonserver.com/images/assets/getting-started-with-structured-concurrency-in-swift/cover.jpg">
<link rel="stylesheet" href="https://swiftonserver.com/css/style.css">
<link rel="stylesheet" href="https://swiftonserver.com/css/syntax.css">
<link rel="shortcut icon" href="https://swiftonserver.com/images/icons/favicon.ico" type="image/x-icon">
<link rel="shortcut icon" href="https://swiftonserver.com/images/icons/icon-320.png" type="image/png">
<link rel="apple-touch-icon" href="https://swiftonserver.com/images/icons/apple-touch-icon.png">
<link rel="apple-touch-icon" sizes="57x57" href="https://swiftonserver.com/images/icons/apple-touch-icon-57x57.png">
<link rel="apple-touch-icon" sizes="72x72" href="https://swiftonserver.com/images/icons/apple-touch-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="https://swiftonserver.com/images/icons/apple-touch-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="https://swiftonserver.com/images/icons/apple-touch-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="https://swiftonserver.com/images/icons/apple-touch-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="https://swiftonserver.com/images/icons/apple-touch-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="https://swiftonserver.com/images/icons/apple-touch-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="https://swiftonserver.com/images/icons/apple-touch-icon-180x180.png">
</head>
<body>
<header id="page-header">
<a href="https://swiftonserver.com/">
<figure>
<picture>
<source
srcset="https://swiftonserver.com/images/logos/logo~dark.png"
media="(prefers-color-scheme: dark)"
>
<img
id="logo-image"
width="150"
height="150"
src="https://swiftonserver.com/images/logos/logo.png"
alt="Logo of Swift on server"
title="Swift on server"
>
</picture>
</figure>
</a>
</header>
<main>
<article>
<header>
<section id="post-header" class="content-wrapper">
<time datetime="2024/03/19">2024/03/19</time>
<h1 class="title">Getting Started with Structured Concurrency in Swift</h1>
<p class="excerpt">Learn how to apply structured concurrency in your applications, using task groups and other structured concepts.</p>
<div class="meta">
<span class="tag">Swift</span>
<span class="tag">Structured Concurrency</span>
</div>
<img src="https://github.com/joannis.png" alt="Joannis Orlandos" class="author">
<p>
<span class="author">Written by: <a href="https://x.com/JoannisOrlandos" target="_blank">Joannis Orlandos</a> @
<span class="author"><a href="https://unbeatable.software/" target="_blank">Unbeatable Software B.V.</a></span><br>
<span class="reading-time">Reading time: 30 minutes</span><br>
</p>
</section>
</header>
<section class="content-wrapper">
<hr>
</section>
<section id="contents" class="content-wrapper">
<h1>Structured Concurrency in Swift</h1><p>Swift 5.5 introduced structured concurrency. The new way to write concurrent code that is more maintainable and easier to reason about. A lot of developers have been adopting concurrency in Swift. But few people understand what '<strong>structured</strong>' means in this context, and how it helps you.</p><p>This guide will teach you all you need to know about structured concurrency in Swift. We'll cover the basics of concurrency, and how structured concurrency is different from other concurrency models. By the end of this guide, you'll be able to write any application in Swift using structured concurrency.</p><h2>What is Concurrency?</h2><p>Concurrency is the ability of different parts your code to run out-of-order or in partial order, without affecting the outcome. This allows for parallel execution of the concurrent units, which can improve the overall speed of the execution.</p><p>Imagine that you're shopping for groceries with a friend. You both have a list of items to buy, and you decide to split up to save time. You both go to different parts of the store, and pick up the items on your list. You both finish at slightly different times, and meet up at the checkout. Instead of having to go through all the aisles together, you're both able to solve part of the puzzle at the same time. The end result is the same, but you've saved time.</p><h3>Pre-Swift 5.5 Concurrency</h3><p>Concurrency has been a part of Swift for a long time, for example, through the use of <code>DispatchQueue</code> and <code>OperationQueue</code>. In these models, you can submit work to a queue, and the queue will execute the work in the background. Often times, you'll have to wait for the work to finish, either successfully or with an error.</p><pre><code class="language-swift"><span class="type">DispatchQueue</span>.<span class="call">global</span>().<span class="call">async</span> {
<span class="comment">// Offload some (heavy) work</span>
}</code></pre><p>In these models, you're responsible for managing the lifecycle of the work. You'll need to ensure that work is properly cancelled when it's no longer needed.</p><p>When implementing a function that has callbacks, you're responsible for calling the completion handler when the work is done. This can make it hard to debug and reason about the code, especially when you're working with concurrent units.</p><p>Take the following example:</p><pre><code class="language-swift"><span class="keyword">func</span> fetchImage(at url: <span class="type">URL</span>, completion: <span class="keyword">@escaping</span> (<span class="type">Result</span><<span class="type">UIImage</span>, <span class="type">Error</span>>) -> <span class="type">Void</span>) {
<span class="type">URLSession</span>.<span class="property">shared</span>.<span class="call">dataTask</span>(with: url) { data, response, error <span class="keyword">in
if let</span> error {
<span class="call">completion</span>(.<span class="call">failure</span>(error))
<span class="keyword">return</span>
}
<span class="keyword">guard let</span> data = data, <span class="keyword">let</span> image = <span class="type">UIImage</span>(data: data) <span class="keyword">else</span> {
<span class="call">completion</span>(.<span class="call">failure</span>(<span class="type">NetworkError</span>.<span class="property">missingImage</span>))
<span class="keyword">return</span>
}
<span class="call">completion</span>(.<span class="call">success</span>(image))
}
}</code></pre><p>In this example, various bugs can arise. For example, in <code>if let error</code>, omitting the <code>return</code> statement will cause the completion handler to be called twice.</p><h3>Race Conditions</h3><p>When accessing shared state from concurrently running code, it's critical to ensure that the state is accessed in a safe way. If the same value is accessed and modified at the same time, you can run into crashes called 'race conditions'.</p><p>Race conditions need to be carefully and correctly solved. When using a mutex/lock to protect shared state, you need to ensure that this lock starts and ends at the right time. And when working with locks in long running calls such as network calls, you need to be careful to avoid performance bottlenecks. Finally, you can cause deadlocks when multiple functions that call each other access the same lock. Take the following example:</p><pre><code class="language-swift"><span class="keyword">final class</span> ImageCache {
<span class="keyword">private var</span> cache: [<span class="type">URL</span>: <span class="type">UIImage</span>] = [:]
<span class="keyword">private let</span> lock = <span class="type">NSLock</span>()
<span class="keyword">func</span> image(for url: <span class="type">URL</span>) -> <span class="type">UIImage</span>? {
lock.<span class="call">lock</span>()
<span class="keyword">defer</span> { lock.<span class="call">unlock</span>() }
<span class="keyword">return</span> cache[url]
}
<span class="keyword">func</span> loadImage(for url: <span class="type">URL</span>) {
lock.<span class="call">lock</span>()
<span class="keyword">defer</span> { lock.<span class="call">unlock</span>() }
<span class="comment">// This is covered by the lock</span>
<span class="keyword">if</span> cache.<span class="property">keys</span>.<span class="call">contains</span>(url) {
<span class="keyword">return</span>
}
<span class="call">fetchImage</span>(at: url) { image <span class="keyword">in
guard case</span> .<span class="dotAccess">success</span>(<span class="keyword">let</span> image) = image <span class="keyword">else</span> { <span class="keyword">return</span> }
<span class="comment">// This is not covered by the lock</span>
cache[url] = image
}
}
}</code></pre><p>The above example is non-trivial. It's not always obvious that you need to lock access to <code>image</code> twice. There are not one, but four traps here.</p><ol><li>It's easy to forget to lock access to the cache.</li><li>One might lock access to the cache, but omit either the check for an existing image - or the assignment of the image.</li><li>When locking access to the cache, one might forget to unlock the lock. When returning a value, as seen in the <code>image(for:)</code> function, the lock should be unlocked after accessing the value, but before returning.</li><li>Finally, when locking access to the cache, unlocking could be implemented only after the fetching has completed.</li></ol><p>These are all common mistakes, and they're hard to debug and reason about. This is where structured concurrency comes in.</p><p>Over the years, many patterns and abstractions have emerged to solve these problems. For example, the <code>Future</code> and <code>Promise</code> pattern is a common way to solve the problem of waiting for a value to be available. These abstractions are not part of the standard library, and are not always easy to work with or reason about. They're also not part of the standard library, leading to a fragmented ecosystem.</p><h2>Structured Concurrency</h2><p>Swift has always been focused on safety and maintainability through <em>local reasoning</em>. Common examples are found in the type system, such as the use of value types. Because Array and Dictionary are value types, you can reason about them locally. You don't need to know about other parts of the code, and how those other parts might be modifying a reference to the same array or dictionary. Because value types are copied when passed around, you can reason about them locally.</p><p>Similarly, Structured Concurrency is a language feature that is designed to write concurrent code that is more maintainable and easier to reason about. It's designed to solve these problems, and is the recommended to write concurrent code that is maintainable and easy to reason about.</p><p>You're probably familiar with structured programming, as it's a paradigm that every Swift developer uses. By making use of a <em>structured control flow</em> through constructs such as if-statements, for-loops and switch-statements, you're able to write code that is easy to reason about and maintain.</p><p>Structured Concurrency is the same concept, but applied to concurrent code. Functions in structured concurrency still have a clear entry and exit point. In Swift, this is done through the use of <code>async</code> functions and the <code>await</code> keyword.</p><h3>Async Functions</h3><p>An <code>async</code> function is a function that can pause and resume. Think of it as a function that can be split up into multiple parts.</p><p>When you order a pizza, you don't have to wait for the pizza to be made and delivered. You can continue watching your favourite show, while the pizza is delivered to your doorstep. Just like async functions. Should you need to know when the pizza is delivered, you can <code>await</code> the delivery.</p><pre><code class="language-swift"><span class="keyword">func</span> watchTelevision() <span class="keyword">async throws</span> {
<span class="keyword">let</span> store = <span class="keyword">await</span> <span class="type">PizzaStore</span>.<span class="call">discover</span>()
<span class="keyword">let</span> pizza = <span class="keyword">await</span> store.<span class="call">orderPizza</span>()
<span class="keyword">let</span> show = <span class="call">startWatchingTV</span>()
<span class="keyword">try await</span> pizza.<span class="call">eat</span>()
<span class="keyword">await</span> show.<span class="call">watchUntilDone</span>()
show.<span class="call">stopWatchingTV</span>()
}</code></pre><p>Since structured concurrency leverages the structured programming paradigm, handling errors works the same as in synchronous code.</p><pre><code class="language-swift"><span class="keyword">func</span> watchTelevision() <span class="keyword">async throws</span> {
<span class="keyword">let</span> pizza = <span class="keyword">await</span> store.<span class="call">orderPizza</span>()
<span class="keyword">let</span> show = <span class="call">startWatchingTV</span>()
<span class="keyword">do</span> {
<span class="keyword">let</span> show = <span class="call">startWatchingTV</span>()
<span class="keyword">try await</span> pizza.<span class="call">eat</span>()
<span class="keyword">await</span> show.<span class="call">watchUntilDone</span>()
} <span class="keyword">catch</span> <span class="type">PizzaError</span>.<span class="call">notHungry</span> {
<span class="comment">// No problem, we'll eat it later</span>
} <span class="keyword">catch</span> <span class="type">PizzaError</span>.<span class="call">burnt</span> {
<span class="comment">// Something went wrong, we'll have to stop watching TV</span>
show.<span class="call">stopWatchingTV</span>()
<span class="keyword">await</span> store.<span class="call">complain</span>(about: pizza)
<span class="keyword">throw</span> error
}
show.<span class="call">stopWatchingTV</span>()
}</code></pre><h2>Structured Tasks</h2><p>The <code>Task</code> object is not the only way to run concurrent work. The simplest way of running an <code>async</code> function in parallel is using the <code>async let</code> construct. This is a <em>structured</em> way to start a task and let it run until you need the result:</p><pre><code class="language-swift"><span class="keyword">func</span> buyBooks(from bankAccount: <span class="type">BankAccount</span>) <span class="keyword">async throws</span> -> [<span class="type">Book</span>] {
<span class="comment">// Resolve this concurrently</span>
<span class="keyword">async let</span> balance = <span class="keyword">await</span> bankAccount.<span class="call">checkBalance</span>()
<span class="keyword">let</span> store = <span class="keyword">await</span> <span class="type">BookStore</span>.<span class="call">discover</span>()
<span class="keyword">var</span> budget = <span class="keyword">await</span> balance
<span class="keyword">var</span> boughtBooks: [<span class="type">Book</span>] = []
<span class="keyword">for await</span> book <span class="keyword">in</span> store.<span class="call">broweBooks</span>() <span class="keyword">where</span> book.<span class="property">price</span> <= budget {
<span class="keyword">let</span> order = <span class="keyword">try await</span> book.<span class="call">buy</span>()
<span class="keyword">let</span> book = <span class="keyword">await</span> order.<span class="call">delivery</span>()
budget -= book.<span class="property">price</span>
boughtBooks.<span class="call">append</span>(book)
}
<span class="keyword">return</span> boughtBooks
}</code></pre><p>When this <code>async let</code> is not <code>await</code>ed for, it will continue to run in the background until the end of the function. If the function returns without awaiting the <code>async let</code>, the task will be cancelled.</p><p>The <code>async let</code> pattern is helpful for individual pieces of work that need to run concurrently. But it doesn't help when needing to run multiple pieces of work concurrently in a structured way. For that, there are <strong>task groups</strong>.</p><h3>Sequences</h3><p>Like how a for-loop iterates over a sequence of items, a for-await-in loop iterates over a sequence of async items.</p><pre><code class="language-swift"><span class="keyword">struct</span> Books: <span class="type">AsyncSequence</span> {
<span class="keyword">typealias</span> Element = <span class="type">Book</span>
...
}
<span class="keyword">func</span> browseBooks() -> <span class="type">Books</span> {}
<span class="keyword">func</span> buyAllBooks() <span class="keyword">async throws</span> {
<span class="keyword">for await</span> book <span class="keyword">in</span> <span class="call">browseBooks</span>() {
<span class="keyword">let</span> order = <span class="keyword">try await</span> book.<span class="call">buy</span>()
<span class="keyword">await</span> order.<span class="call">delivery</span>()
}
}</code></pre><p>This is a powerful feature, as it allows you to easily reason about <em>streams</em> of data. On iOS, this can be a stream of keyboard events, StoreKit purchases, notifications or sensor data. For backend developers, this can be a WebSocket, a database query or the incoming connections on a TCP server. If you're interested in that, please check out our tutorial on <a href="/using-swiftnio-channels" target="_blank">writing a SwiftNIO TCP Server</a>.</p><p>If you're familiar with the Combine framework, this might sound similar to a <code>Publisher</code>. AsyncSequences have many of the same features as Combine's Publishers. Especially with <a href="https://github.com/apple/swift-async-algorithms" target="_blank">swift-async-algorithms</a>, AsyncSequence receive many of the same perks that a Publisher has.</p><p>AsyncSequences are part of the standard library, and are designed similarly to the existing <code>Sequence</code> protocol. You can create an <code>AsyncIterator</code> from them. The iterator has a <code>mutating func next() async throws -> Element?</code>.</p><p>This allows you to write a longer control flow that expect multiple results, such as the head and body of an HTTP request. You can use a <code>for-await-in</code> loop to iterate over the sequence of results, or manually iterate over the sequence using the <code>next()</code> method.</p><p>Now, a common request; "How can I await the delivery of these books concurrently?"</p><h2>Tasks</h2><p>A task is a concurrent unit of work. In concurrency, many tasks can run in parallel.</p><p>The <em>easiest</em> way to create a task is using the <strong>unstructured</strong> <code>Task</code> type. It's used to run a piece of code concurrently in the background, similar to <code>DispatchQueue.global().async {}</code>. In addition, you can manage it's lifecycle by <code>cancel()</code>ing it. Finally, you can also <code>await</code> its <code>value</code> for it to finish.</p><pre><code class="language-swift"><span class="keyword">func</span> buyBooks() {
<span class="keyword">let</span> store = <span class="keyword">await</span> <span class="type">BookStore</span>.<span class="call">discover</span>()
<span class="keyword">for await</span> book <span class="keyword">in</span> store.<span class="call">browseBooks</span>() {
<span class="type">Task</span> {
<span class="keyword">let</span> order = <span class="keyword">try await</span> book.<span class="call">buy</span>()
<span class="keyword">await</span> order.<span class="call">delivery</span>()
}
}
}</code></pre><p>This looks great in theory, but the <code>Task</code> object is <em>not</em> structured. It's not clear when the task starts, when it ends, and what happens when it's cancelled. You're required to manage the lifecycle of the task yourself, and don't even need to await the result or handle errors. This is inherently unsafe and re-introduces the problems that structured concurrency is designed to solve.</p><p>It is very much a part of the structured concurrency model. But think of it as an "escape hatch" when there's no other context or task in which your code can run. In almost every application, you'll have <em>some</em> entrypoint at which you can start with a task. For example, your <code>@main</code> annotated entrypoint can be marked as <code>static func main() async throws</code> and you can start your application from there.</p><p>In SwiftUI apps, concurrent work can be started from within the <code>.task { }</code> view modifier. Not only does this allow running <code>async</code> work, but it also cancels that task when the view is no longer needed. That way your dependencies can discard heavy work initiated by the view that the user is no longer interested in.</p><h3>The Task Hierarchy</h3><p>Tasks in structured concurrency are part of a hierarchy. This means that a task can create child tasks, and that the child tasks are automatically cancelled when the parent task is cancelled. Both structured and unstructured tasks are part of this hierarchy. Unstructured tasks do not reap all of the same benefits as structured tasks.</p><p>Like in structured programming, structured concurrency has a stack. This allows reading a stack trace to understand the flow of the program. This is especially helpful when debugging, or when reading a crash or error report.</p><p>When using unstructured tasks, you're not able to see the stack trace outside of the spawned task. This makes it harder to debug, and loses your ability to leverage some Xcode Instruments.</p><h3>Task Local Values</h3><p>Since tasks can run on many different threads, there is (generally) no guarantee that a task will run on the same thread between suspension points. This means that thread-local is not available to store values that are specific to a task in structured concurrency.</p><p>A replacement for thread-local storage is task-local storage. This is a way to store values that are specific to a task. By using the <code>TaskLocal</code> property wrapper to store values that are specific to a task.</p><p>Here's an example of how a <code>TaskLocal</code> stores the currently authenticated user in a web server:</p><pre><code class="language-swift"><span class="keyword">struct</span> UserMiddleware: <span class="type">Middleware</span> {
<span class="keyword">@TaskLocal static var</span> currentUser: <span class="type">User</span>?
<span class="keyword">let</span> db: <span class="type">Database</span>
<span class="keyword">func</span> handleRequest(
<span class="keyword">_</span> request: <span class="type">HTTPRequest</span>,
next: <span class="type">HTTPResponder</span>
) <span class="keyword">async throws</span> -> <span class="type">HTTPResponse</span> {
<span class="keyword">let</span> token = <span class="keyword">try</span> request.<span class="call">parseJWT</span>()
<span class="keyword">let</span> user = <span class="keyword">try await</span> db.<span class="call">getUser</span>(byId: token.<span class="property">sub</span>)
<span class="keyword">return try await</span> <span class="type">HTTPServer</span>.<span class="property">$currentUser</span>.<span class="call">withValue</span>(user) {
<span class="keyword">return try await</span> next.<span class="call">respond</span>(to: request)
}
}
}</code></pre><p>Now that the TaskLocal variable is set, it's accessible from any code called within the <code>withValue</code> block. For example:</p><pre><code class="language-swift"><span class="keyword">func</span> respond(to request: <span class="type">HTTPRequest</span>) <span class="keyword">async throws</span> -> <span class="type">HTTPResponse</span> {
<span class="keyword">guard let</span> currentUser = <span class="type">UserMiddleware</span>.<span class="property">currentUser</span> <span class="keyword">else</span> {
<span class="keyword">throw</span> <span class="type">HTTPError</span>.<span class="property">unauthorized</span>
}
<span class="comment">// ...</span>
}</code></pre><h3>Task Cancellation</h3><p>In structured concurrency, tasks are automatically cancelled when their parent task is cancelled. This is a powerful feature, as it allows you to cancel all of a task's dependencies at once. This is especially helpful when you're writing a server.</p><p>Let's say you're writing a web server, where your route generates a huge excel file. If the client cancels the request, you'll want to cancel the generation of the excel file. Continuing to generate the file is a waste of resources, and can lead to intentional and unintentional denial of service attacks.</p><p>In structured concurrency, you can use the <code>Task</code> object to cancel a task. This is a structured way to cancel a task, and it's clear when the task is cancelled. You can also use the <code>Task</code> object to check if a task is cancelled, and to handle the cancellation.</p><pre><code class="language-swift"><span class="keyword">if</span> <span class="type">Task</span>.<span class="property">isCancelled</span> {
<span class="keyword">return</span>
}</code></pre><p>This is a structured way to check if a task is cancelled, and to handle the cancellation. It's clear when the task is cancelled, and you can handle the cancellation in a structured way.</p><p>You can also check if a task is cancelled using the <code>Task.checkCancellation</code> method. This is a structured way to check if a task is cancelled, and to handle the cancellation. It's clear when the task is cancelled, and you can handle the cancellation in a structured way.</p><pre><code class="language-swift"><span class="keyword">try</span> <span class="type">Task</span>.<span class="call">checkCancellation</span>()</code></pre><p>This will throw a <code>CancellationError</code> if the task is cancelled. You can catch this error and handle the cancellation in a structured way.</p><h3>Blocking and Sleeping Tasks</h3><p>If you have blocking or heavy work that you want to run concurrently, you'll need to do so outside of the structured concurrency model. This is because blocking or heavy work can cause a performance bottleneck in the global concurrent executor. SwiftNIO has the <code>NIOThreadPool</code> that you can use to run blocking work concurrently. For iOS users, it may be wise to use a <code>DispatchQueue</code> for these scenarios.</p><p>If you do decide to add computationally heavy code in structured concurrency, you can use <code>await Task.yield()</code> to yield the current task. This will allow your Task Executor to run other tasks. Doing so can prevent lag spikes, such as UI freezes those that happen on iOS when blocking the main thread.</p><p><strong>Note:</strong> Swift 6 will be able to address these issues, through the addition of custom Task Executors. More on that later.</p><p>When finding yourself in a situation where you need to delay a task, you can use the <code>Task.sleep</code> method. It's similar to your regular <code>sleep</code> function, but rather than blocking the entire thread, it only suspends the task.</p><pre><code class="language-swift"><span class="keyword">try await</span> <span class="type">Task</span>.<span class="call">sleep</span>(for: .<span class="call">seconds</span>(<span class="number">10</span>))</code></pre><p>An extra feature of <code>Task.sleep</code> is that it can be cancelled. If the task is cancelled while it's sleeping, the sleep will be interrupted and throw a <code>CancellationError</code>.</p><h3>Cancellation Handlers</h3><p>When a task is cancelled, you might want to clean up resources or perform some other action to handle the cancellation. You can use a cancellation handler to do this. A cancellation handler is a piece of code that is run when a task is cancelled.</p><pre><code class="language-swift"><span class="keyword">func</span> getData() <span class="keyword">async throws</span> -> <span class="type">HTTPResponse</span> {
<span class="keyword">let</span> httpClient = <span class="keyword">try await</span> <span class="type">HTTPClient</span>.<span class="call">connect</span>(to: <span class="string">"https://api.example.com"</span>)
<span class="keyword">return try await</span> <span class="call">withTaskCancellationHandler</span> {
<span class="comment">// This will run normally, and does the actual work
// On cancellation, it will still find that `Task.isCancelled == true`
// In addition, Task.sleep will throw a CancellationError
// But if the HTTPClient doens't support cancellation,
// it will continue to run until it's done</span>
<span class="keyword">return try await</span> httpClient.<span class="call">get</span>(<span class="string">"/data"</span>)
} onCancel: {
<span class="comment">// If the task is cancelled, this callback will run
// and clean up the HTTP client
// This allows users to implement cancellation manunally if needed</span>
httpClient.<span class="call">shutdown</span>()
}
}</code></pre><h2>Task Groups</h2><p>One can order ten items off your favourite book store. But in the real world, you don't want to <code>await</code> for the first book before ordering the next one. For that, we can use a <code>TaskGroup</code>:</p><pre><code class="language-swift"><span class="keyword">func</span> buyBooks() <span class="keyword">async throws</span> {
<span class="keyword">let</span> store = <span class="keyword">await</span> <span class="type">BookStore</span>.<span class="call">discover</span>()
<span class="keyword">try await</span> <span class="call">withThrowingTaskGroup</span>(of: <span class="type">Book</span>.<span class="keyword">self</span>) { taskGroup <span class="keyword">in
for await</span> book <span class="keyword">in</span> store.<span class="call">browseBooks</span>() {
taskGroup.<span class="call">addTask</span> {
<span class="keyword">try await</span> book.<span class="call">buy</span>()
}
}
<span class="comment">// The task group will automatically await all tasks</span>
}
}</code></pre><p>This is a structured way to run multiple pieces of work concurrently. It's clear when the tasks start and when they end. You can run many pieces of work in parallel. And you can await all tasks being completed, and get an error if any one of them fails.</p><p>The above task group can throw errors, but not all task groups need to throw. If you use <code>withTaskGroup</code>, you'll be able to run tasks that don't throw, and you won't need to handle errors.</p><p>In the above example, <code>withThrowingTaskGroup(of: Book.self)</code> specifies that each task <em>must</em> produce a <code>Book</code> result if successful. In some cases, the result of the task is not necessary. In this case however, the results are helpful to collect the books that were bought.</p><p>To solve that, use the <code>reduce</code> function on the task group. This is a structured way to run multiple pieces of work concurrently, and to reduce the results into a single value.</p><pre><code class="language-swift"><span class="keyword">func</span> buyBooks() <span class="keyword">async throws</span> -> [<span class="type">Book</span>] {
<span class="keyword">let</span> store = <span class="keyword">await</span> <span class="type">BookStore</span>.<span class="call">discover</span>()
<span class="keyword">return try await</span> <span class="call">withThrowingTaskGroup</span>(of: <span class="type">Book</span>.<span class="keyword">self</span>) { taskGroup <span class="keyword">in
for await</span> book <span class="keyword">in</span> store.<span class="call">browseBooks</span>() {
taskGroup.<span class="call">addTask</span> {
<span class="keyword">try await</span> book.<span class="call">buy</span>()
}
}
<span class="comment">// Completes when all tasks have completed</span>
<span class="keyword">return try await</span> taskGroup.<span class="call">reduce</span>(into: []) { books, book <span class="keyword">in</span>
books.<span class="call">append</span>(book)
}
}
}</code></pre><h3>Discarding Task Groups</h3><p>In some cases, you might not be interested in the result of the task group. For example, you might want to run a number of tasks concurrently, but these tasks don't return results. In that case, you can use <code>withDiscardingTaskGroup</code> and <code>withThrowingDiscardingTaskGroup</code> from iOS 17 and macOS 14. This is a structured way to run multiple pieces of work concurrently, without needing to retain results.</p><p>The regular task groups create a collection of results, which you can then iterate over. In some cases, such as a TCP server, this collection of results is not needed and grow indefinitely. In that case, you'll want to use a discarding task group to prevent an ever-growing collection of results. Note that <code>Void</code> results are still stored and occupy a small amount of memory!</p><h2>Conclusion</h2><p>Structured concurrency is a powerful feature that was introduced with Swift 5.5. When writing your concurrenct code in a structured way, it's easier to reason about your code and maintain it.</p><p>Almost every application that you write will also have some form of shared state. In next week's article, we'll cover how Swift's actors, actor isolation and Sendable checking empower you to write race-condition free code.</p>
</section>
<section id="about-author" class="content-wrapper">
<h4>About Joannis Orlandos</h4>
<p>Joannis is a SSWG member and co-founder of <a href="https://unbeatable.software/">Unbeatable Software B.V.</a> and provides Full-Stack Swift Training and Consultation.</p>
<a href="mailto:joannis@unbeatable.software" target="_blank" class="author-cta">Get Training or Consultation</a>
</section>
</article>
</main>
<footer>
<section class="content-wrapper">
<figure>
<picture>
<source
srcset="https://swiftonserver.com/images/logos/logo~dark.png"
media="(prefers-color-scheme: dark)"
>
<img
id="logo-image"
width="80"
height="80"
src="https://swiftonserver.com/images/logos/logo.png"
alt="Logo of Swift on server"
title="Swift on server"
>
</picture>
</figure>
<p>This site was generated using the <a href="https://swift.org/" target="_blank">Swift</a> programming language.</p>
<p class="small">Created by <a href="https://x.com/JoannisOrlandos" target="_blank">Joannis Orlandos</a> & <a href="https://x.com/tiborbodecs">Tibor Bödecs</a> © 2024.</p>
<p>
<a href="https://swiftonserver.com/">Home</a> ·
<a href="https://swiftonserver.com/rss.xml" target="_blank">RSS</a> ·
<a href="https://swiftonserver.com/sitemap.xml" target="_blank">Sitemap</a>
</p>
</section>
</footer>
</body>
</html>