Synchronous Asynchronous
Fetching and updating multiple resources (waiting)
Closures
func updatePersonInfo(_ person: Person) {
lblName.text = person.name
/$ Synchronous, blocking
let image = person.loadImage()
let inviteCount = "Has \(person.loadInvitesCount()) invites"
let friends = person.loadFriends()
imgAvatar.image = image
btnInvite.setTitle(
"You have \(inviteCount) invites”,
for: .normal
)
self.friends = friends
lblWebsite.text = person.website
}
func updatePersonInfo(_ person: Person) {
lblName.text = person.name
/$ Asynchronous, pyramid-of-doom
person.loadImage { image in
person.loadInvitesCount { count in
person.loadFriends { [weak self] friends in
self?'imgAvatar.image = image
self?'btnInvite.setTitle(
"You have \(count) invites”,
for: .normal
)
self?'friends = friends
}
}
}
}
( Swift pre-async/await
Slide 13
Slide 13 text
1v
Fetching and updating multiple resources (parallel)
Dispatch Groups
func updatePersonInfo(_ person: Person) {
let group = DispatchGroup()
var image: UIImage?
var invitesCount: Int?
var friends = [Friend]()
group.enter()
person.loadImage { fetchedImage in
defer { group.leave() }
image = fetchedImage
}
group.enter()
person.loadInvitesCount { count in
defer { group.leave() }
invitesCount = count
}
group.enter()
person.loadFriends { allFriends in
friends = allFriends
}
group.notify(queue: DispatchQueue.main) { [weak self] in
/$ Have all 3 values only here,
/$ what about errors though ?!
self?'imgAvatar.image = image
self?'btnInvite.setTitle(
"You have \(invitesCount) invites",
for: .normal
)
self?'friends = friends
}
}
Other options:
Semaphore
DispatchWorkItems
Combine Publishers
How do we deal with accumulated errors here?
How do we short-circuit?
How do we cancel work?
Which queue are we running on?
Slide 14
Slide 14 text
Startin
Starting off iOS in 2010, Concurrency / Asynchrony was the hardest topic for me to grasp.
Not obvious, too many methodologies, too many dark corners, and I’m not the only one:
Fan moment: Matt Thomposon (NSHipster) created Alamofire and AFNetworking
* tl;dr: Async is HARD
Slide 15
Slide 15 text
* tl;dr: Async is HARD
Startin
With the rise of SwiftUI, Swift and other modern standards -
working with asynchronous work is an obvious standard
But it didn’t get any easier. Yet …
Our code is modern now, sort of …
Slide 16
Slide 16 text
# Concurrency
Good
Easy to read and follow
Predictable
Simple error handling
Bad
Serial
Blocking
Single thread
Poor resource utilization
Good
Concurrent
Non-blocking
Multi-threaded
Superior resource optimization
Bad
Difficult to follow
(sometimes) unpredictable
Handling errors is involved / error-prone
Synchronous Asynchronous
Slide 17
Slide 17 text
+Concurrency Manifesto
Many others thought the state of async work is non-optimal in Swift
For example, this very smart person:
Main author of LLVM, Clang & Swift
Chris Lattner
https://gist.github.com/lattner/31ed37682ef1576b16bca1432ea9f782
Swift Concurrency Manifesto:
First commit: August 11, 2017
Slide 18
Slide 18 text
What is
async/await?
❓
Slide 19
Slide 19 text
❓What is async/await?
A sink
Slide 20
Slide 20 text
❓What is async/await?
A weight
A sink
Slide 21
Slide 21 text
❓What is async/await?
A weight
A sink
Slide 22
Slide 22 text
❓What is async/await?
It is a well-defined standard used across many
other programming languages
async/await is a new syntax and part of a set of language features
titled “Swift Concurrency”.
(each with its own implementation)
and more …
Slide 23
Slide 23 text
❓What is async/await?
async lets you define asynchronous functions
func loadImage(completion: (Result) -) Void)
func loadImage() async throws -) UIImage
Slide 24
Slide 24 text
❓What is async/await?
async lets you define asynchronous functions
func loadImage(completion: (Result) -) Void)
func loadImage() async throws -) UIImage
Slide 25
Slide 25 text
❓What is async/await?
async lets you define asynchronous functions
func loadImage(completion: (Result) -) Void)
func loadImage() async throws -) UIImage
Slide 26
Slide 26 text
await lets you wait for the results of an asynchronous function
It also marks a suspension point
person.loadImage { [weak self] fetchedImage in
switch fetchedImage {
case .success(let image):
self?'image = image
case .failure(let error):
handleError(error)
}
}
do {
self.image = try await person.loadImage()
} catch {
handleError(error)
}
❓What is async/await?
Slide 27
Slide 27 text
You might ask yourself:
“Can I just wait for the result of these asynchronous functions
anywhere?”
❓What is async/await?
Slide 28
Slide 28 text
Where can you run an async function?
An async function can only be called from within an
asynchronous context.
This means either from a function which itself is async, or a
task.
func updatePersonInfo() async throws {
let image = try await person.loadImage()
}
func updatePersonInfo() {
Task {
do {
let image = try await person.loadImage()
} catch {
/$ handle error
}
}
}
Called from an async function:
Called from a Task:
❓What is async/await?
Slide 29
Slide 29 text
person.loadImage { image in
person.loadInvitesCount { count in
person.loadFriends { [weak self] friends in
/$ access image, count and friends
}
}
}
let image = await person.loadImage()
let count = await person.loadInvitesCount()
let friends = await person.loadFriends()
/$ Access image, count and friends
This might be confusing, but from a functionality perspective,
these two pieces of code are equal:
person.loadImage { image in
person.loadInvitesCount { count in
person.loadFriends { [weak self] friends in
/$ access image, count and friends
}
}
}
❓What is async/await?
Slide 30
Slide 30 text
This might be confusing, but from a functionality perspective,
these two pieces of code are equal:
These only seem
synchronous, but each of
them suspends and waits for
its result before resuming to
the next steps
❓What is async/await?
let image = await person.loadImage()
let count = await person.loadInvitesCount()
let friends = await person.loadFriends()
/$ Access image, count and friends
Slide 31
Slide 31 text
This might be confusing, but from a functionality perspective,
these two pieces of code are equal:
These only seem
synchronous, but each of
them suspends and waits for
its result before resuming to
the next steps
❓What is async/await?
let image = await person.loadImage()
let count = await person.loadInvitesCount()
let friends = await person.loadFriends()
/$ Access image, count and friends
Slide 32
Slide 32 text
Good
Easy to read and follow
Predictable
Simple error handling
Bad
Serial
Blocking
Single thread
Poor resource utilization
Good
Concurrent
Non-blocking
Multi-threaded
Superior resource optimization
Bad
Difficult to follow
(sometimes) unpredictable
Handling errors is involved / error-prone
Synchronous Asynchronous
+ = async/await
❓What is async/await?
Slide 33
Slide 33 text
But…
How does it work?!
❓What is async/await?
Slide 34
Slide 34 text
under the hood
async/await
⚙
Slide 35
Slide 35 text
⚙ How async/await works
Task
A Task represents a unit of asynchronous work which could be a child of a different Task
It has several initializers that let us create new asynchronous contexts:
Task { … }
Task(priority:) { ." }
Creating tasks that inherit context, task values, etc:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
Task {
/$ Executes asynchronously,
/$ but on the main thread
print(Thread.isMainThread) /$ true
}
}
}
Slide 36
Slide 36 text
⚙ How async/await works
Task
Task.deatched { … }
Task.detached(priority:) { ." }
Creating tasks that are detached / independent:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
Task.detached {
/$ Executes asynchronously,
/$ not on the main thread
print(Thread.isMainThread) /$ false
}
}
}
A Task represents a unit of asynchronous work which could be a child of a different Task
It has several initializers that let us create new asynchronous contexts:
Slide 37
Slide 37 text
⚙ How async/await works
Task
Task is actually generic over a Success and Failure,
But the default for these initializers (and most common scenario
here) is Void and Never, accordingly
Task { ..+ } = Task { ..+ }
let task = Task {
return someExpensiveComputation()
}
let result = await task.value
It also exposes an await-able value getter:
Slide 38
Slide 38 text
⚙ How async/await works
Swift Concurrency uses a Cooperative Thread Pool model
A pool of threads available to run asynchronous functions !
Tasks must always make forward progress or suspend
No more than one thread per CPU core (as opposed to GCD)
Avoid thread explosion
Avoid the overhead and performance penalty of thread switching
Can suspend tasks and resume them later to prioritize other, more
important, pieces of work, thanks to a lightweight abstraction
called a Continuation
Slide 39
Slide 39 text
⚙ How async/await works
Marking a function as async lets Swift know it can “give up the thread”
This means an async function can suspend, wait for results,
and then resume as needed, while letting other async
functions utilize threads in the pool
func getFoo() async {
let item = await getAmazingItem()
doSomething(with: item)
}
suspends getFoo() and gives up its current thread
(possibly) suspend getAmazingItem() or immediately run
depending on thread availability and priorities
Slide 40
Slide 40 text
⚙ How async/await works
Marking a function as async lets Swift know it can “give up the thread”
This means an async function can suspend, wait for results,
and then resume as needed, while letting other async
functions utilize threads in the pool
This results in optimum CPU utilization, prevents thread hops
and avoids possible over-committing of threads
func getFoo() async {
let item = await getAmazingItem()
doSomething(with: item)
}
suspends getFoo() and gives up its current thread
(possibly) suspend getAmazingItem() or immediately run
depending on thread availability and priorities
When getAmazingItem() finishes, getFoo() resumes
(It may resume on any thread)
Slide 41
Slide 41 text
.
More
async/await
amazingness
Slide 42
Slide 42 text
. More async/await
person.loadImage { image, error1 in
person.loadInvitesCount { count, error2 in
person.loadFriends { [weak self] friends, error3 in
/$ Somehow deal with the accumulation of all errors
/$ What about cancellation and early short circuit?!
}
}
}
Error handling pre-async/await is … painful, to say
the least. It’s very easy to miss handling errors, and
handle all the edge cases
Error handling
Slide 43
Slide 43 text
Error handling
do {
try await person.loadImage()
try await person.loadInvitesCount()
try await person.loadFriends()
} catch {
/$ handle error
}
async/await makes error handling feel natural to Swift,
and identical to synchronous functions syntactically
If any of the tasks fail, consecutive tasks will simply
not run (or cancel)
. More async/await
Slide 44
Slide 44 text
The compiler watches over you
The compiler and runtime also prevents us from writing
incorrect code:
func loadInsanelyLargeResource(completion: (Result) -) Void) {
guard let resource = loadOptionalResource() else { return }
completion(.success(resource))
}
. More async/await
Slide 45
Slide 45 text
The compiler watches over you
The compiler and runtime also prevents us from writing
incorrect code:
func loadInsanelyLargeResource(completion: (Result) -) Void) {
guard let resource = loadOptionalResource() else { return }
completion(.success(resource))
}
Oops, forgot to complete with an error!
. More async/await
Slide 46
Slide 46 text
The compiler watches over you
func loadInsanelyLargeResource() async throws -) Resource {
guard let resource = loadOptionalResource() else { return }
return resource
}
The compiler and runtime also prevents us from writing
incorrect code:
Impossible to forget error handling
. More async/await
Slide 47
Slide 47 text
The compiler watches over you
func loadInsanelyLargeResource() async throws -) Resource {
guard let resource = loadOptionalResource() else {
throw Error.invalidResource
}
return resource
}
The compiler and runtime also prevents us from writing
incorrect code:
. More async/await
Slide 48
Slide 48 text
Async getters
async isn’t exclusive to functions, you can also make
properties async, and even throw from them!
struct Person {
let avatarURL: URL
}
let shai = Person(..+)
userImage.image = try await shai.avatar
. More async/await
var avatar: UIImage? {
get async throws {
let (data, _) = try await URLSession.shared.data(from: avatarURL)
return UIImage(data: data)
}
}
Slide 49
Slide 49 text
With async/await, very convoluted flows become trivial to
write and reason about
. More async/await
Refresh
token about to
expire?
Renew refresh token #
Refresh
access
token
/
Fetch user
and return
0
Error handling for each phase
Yes
No, refresh token is valid
Slide 50
Slide 50 text
func refreshTokenIfNeeded(_ token: JWT, completion: (Result) -) Void) {
if token.isAboutToExpire(within: 15) {
service.renew { error in
if let error = error {
completion(.failure(error))
return
}
refreshTokenIfNeeded(token, completion: completion)
}
return
}
service.getAuthenticatedUser(for: token) { user in
switch user {
case .success(let user):
completion(.success(user))
case .failure:
service.notifyExpiration { error in
if let error = error {
completion(.failure(error))
}
}
}
}
}
JWT Refresh with Closures
We have to recurse back into the
function since we can’t “continue” the
control flow after a closure
Messy error handling and hard to
follow
. More async/await
Slide 51
Slide 51 text
func refreshTokenIfNeeded(_ token: JWT) async throws -) User {
do {
if token.isAboutToExpire(within: 15) {
try await service.renew()
}
return try await service.getAuthenticatedUser(for: token)
} catch {
try await service.notifyExpiration()
}
}
No need for recursion, the function
simply suspends, waits for the
renewal and proceeds
Trivial error handling and control flow,
like synchronous Swift code
. More async/await
JWT Refresh with async/await
Slide 52
Slide 52 text
person.loadImage { image in
person.loadInvitesCount { count in
person.loadFriends { [weak self] friends in
/$ access image, count and friends
}
}
}
let image = await person.loadImage()
let count = await person.loadInvitesCount()
let friends = await person.loadFriends()
/$ Access image, count and friends
Asynchronous execution with straight-line “synchronous”
code is awesome. But, it still waits for each task to complete.
We can do better!
person.loadImage { image in
person.loadInvitesCount { count in
person.loadFriends { [weak self] friends in
/$ access image, count and friends
}
}
}
let image = await person.loadImage()
let count = await person.loadInvitesCount()
let friends = await person.loadFriends()
/$ Access image, count and friends
Back to our person info example…
. More async/await
Slide 53
Slide 53 text
person.loadImage { image in
person.loadInvitesCount { count in
person.loadFriends { [weak self] friends in
/$ access image, count and friends
}
}
}
func updatePersonInfo(_ person: Person) async {
async let image = person.loadImage()
async let count = person.loadInvitesCount()
async let friends = person.loadFriends()
await handleInfo(image, count, friends)
}
func updatePersonInfo(_ person: Person) {
let group = DispatchGroup()
var image: UIImage?
var invitesCount: Int?
var friends = [Friend]()
group.enter()
person.loadImage { fetchedImage in
defer { group.leave() }
image = fetchedImage
}
group.enter()
person.loadInvitesCount { count in
defer { group.leave() }
invitesCount = count
}
group.enter()
person.loadFriends { allFriends in
friends = allFriends
}
group.notify(queue: DispatchQueue.main) { [weak self] in
/$ Have all 3 values only here,
/$ what about errors though ?!
handleInfo(image, invitesCount, friends)
}
}
async let bindings let you run multiple
independent tasks in parallel, while awaiting all
of their results together
. More async/await
Slide 54
Slide 54 text
What about Cancellation?
Similarly to the “Cooperative Thread Pool”, async/await uses a
“Cooperative Cancellation” mechanism.
struct Person {
let avatarURL: URL
var avatar: UIImage? {
get async throws {
let (data, _) = try await URLSession.shared.data(from: avatarURL)
return UIImage(data: data)
}
}
}
This basically means each task is responsible to short-circuit its own
execution if it’s canceled.
. More async/await
Slide 55
Slide 55 text
What about Cancellation?
Similarly to the “Cooperative Thread Pool”, async/await uses a
“Cooperative Cancellation” mechanism.
struct Person {
let avatarURL: URL
var avatar: UIImage? {
get async throws {
let (data, _) = try await URLSession.shared.data(from: avatarURL)
return UIImage(data: data)
}
}
}
Task might have been canceled by the time
fetching the data finished
This basically means each task is responsible to short-circuit its own
execution if it’s canceled.
. More async/await
Slide 56
Slide 56 text
What about Cancellation?
Similarly to the “Cooperative Thread Pool”, async/await uses a
“Cooperative Cancellation” mechanism.
This basically means each task is responsible to short-circuit its own
execution if it’s canceled.
struct Person {
let avatarURL: URL
var avatar: UIImage? {
get async throws {
let (data, _) = try await URLSession.shared.data(from: avatarURL)
try Task.checkCancellation()
return UIImage(data: data)
}
}
}
Throws a special CancellationError if the
task was canceled and stop the control flow
. More async/await
Slide 57
Slide 57 text
What about Cancellation?
Sometimes you might want to perform a different flow based on
whether or not the task was cancelled:
struct Person {
let avatarURL: URL
var avatar: UIImage? {
get async throws {
let (data, _) = try await URLSession.shared.data(from: avatarURL)
if Task.isCancelled {
return UIImage(named: "placeholder")
}
return UIImage(data: data)
}
}
}
. More async/await
Slide 58
Slide 58 text
What about Cancellation?
Lastly, you can use withTaskCancellationHandler to have a closure
immediately execute upon Task cancellation
var avatar: UIImage? {
get async throws {
try await withTaskCancellationHandler(
operation: {
let (data, _) = try await URLSession.shared.data(from: avatarURL)
return UIImage(data: data)
},
onCancel: {
MyCache.shared.clear(for: avatarURL)
}
)
}
}
. More async/await
Slide 59
Slide 59 text
1
Creating
Task Gr ps
Slide 60
Slide 60 text
1 Task groups
Let’s say we have a list of people, and we want to fetch all
of their images, and arrange them into an array of assets
(many) async requests
You might be tempted to do it this way:
func getAllUserPhotos() async -) [UIImage] {
var result = [UIImage]()
for person in people {
await result.append(person.loadAvatar())
}
return result
}
Slide 61
Slide 61 text
1 Task groups
This will work, but … will take quite some time
since it’s serial:
(many) async requests
func getAllUserPhotos() async -) [UIImage] {
var result = [UIImage]()
for person in people {
await result.append(person.loadAvatar())
}
return result
}
Suspends and waits for each
image in its turn
Slide 62
Slide 62 text
1 Task groups
(many) concurrent async requests
Your instinct to perform many concurrent tasks might be to
reach for async let bindings
But this isn’t feasible / useful when we’re talking about
varying / dynamic amount of async functions
/$ ???
func getAllUserPhotos() async -) [UIImage] {
var result = [UIImage]()
for person in people {
async let thing = person.loadAvatar()
}
return result
}
/" ???
Slide 63
Slide 63 text
1 Task groups
Your instinct to perform many concurrent tasks might be to
reach for async let bindings
(many) concurrent async requests
func getAllUserPhotos() async -) [UIImage] {
var result = [UIImage]()
for person in people {
async let thing = person.loadAvatar()
result.append(thing)
}
return result
}
But this isn’t feasible / useful when we’re talking about
varying / dynamic amount of async functions
Slide 64
Slide 64 text
1 Task groups
Your instinct to perform many concurrent tasks might be to
reach for async let bindings
(many) concurrent async requests
func getAllUserPhotos() async -) [UIImage] {
var result = [UIImage]()
for person in people {
async let thing = person.loadAvatar()
result.append(thing)
}
return result
}
But this isn’t feasible / useful when we’re talking about
varying / dynamic amount of async functions
Slide 65
Slide 65 text
1 Task groups
Enter
Task Gr ps
Slide 66
Slide 66 text
1 Task groups
A task group groups together other child tasks, in a
hierarchical way
Task groups
await withTaskGroup(of: ..+) { group in
await withThrowingTaskGroup(of: ..+) { group in
Type of each child task’s result, or - each individual resource we
want to await in the group
await withTaskGroup(of: UIImage.self) { group in
Slide 67
Slide 67 text
1 Task groups
Simply add as many async Tasks as you need to the group
Task groups
func getAllUserPhotos() async -) [UIImage] {
await withTaskGroup(of: UIImage.self) { group in
for person in people {
group.addTask { await person.loadAvatar() }
}
var result = [UIImage]()
for await image in group {
result.append(image)
}
return result
}
}
Slide 68
Slide 68 text
1 Task groups
Then, you can easily iterate over the async results, as if they
were a simple Swift Collection
Task groups
func getAllUserPhotos() async -) [UIImage] {
await withTaskGroup(of: UIImage.self) { group in
for person in people {
group.addTask { await person.loadAvatar() }
}
var result = [UIImage]()
for await image in group {
result.append(image)
}
return result
}
}
Slide 69
Slide 69 text
1 Task groups
You can even prioritize some tasks over others:
Task groups
func getAllUserPhotos() async -) [UIImage] {
await withTaskGroup(of: UIImage.self) { group in
for person in people {
group.addTask(priority: person.isImportant ? .high : nil) {
await person.loadAvatar()
}
}
var result = [UIImage]()
for await image in group {
result.append(image)
}
return result
}
}
Slide 70
Slide 70 text
1 Task groups
Results might return in any order, so it’s worth sorting or
keeping track of the different tasks somehow
Task groups
func getAllUserPhotos() async -) [UUID: UIImage] {
await withTaskGroup(of: (UUID, UIImage).self) { group in
for person in people {
group.addTask { await (person.id, person.loadAvatar()) }
}
return await group.reduce(into: [UUID: UIImage]()) { $0[$1.0] = $1.1 }
}
}
Slide 71
Slide 71 text
1 Task groups
Structured & Unstructured Concurrency
When we talk about Structured Concurrency, we talk about hierarchy.
Having this dependency hierarchy lets Swift do smart things such as propagate cancellation
to child tasks.
Child Task 1
Child Task 2
Child Task 3
Child Task 4
Task
Task
Task
Task
TaskGroup
Parent task
Task groups are a prime example of this, as well as async let bindings
Slide 72
Slide 72 text
1 Task groups
Structured & Unstructured Concurrency
With Unstructured concurrency - we can create Tasks that aren’t part
of any hierarchy, and don’t have a parent task.
We can store them, and cancel them or interact with them later on.
A good example we’ve already talked about:
Task { … }
Task.detached { … }
Slide 73
Slide 73 text
2
Creating
AsyncSequence
Slide 74
Slide 74 text
2 AsyncSequence
Async Sequences
Up until now, we’ve been dealing with async functions that return a
single result.
But often, you will have asynchronous work that might return multiple
values - Sockets, Publishers, File handles, and more…
Time for
Async Sequences
Slide 75
Slide 75 text
2 AsyncSequence
Async Sequences
AsyncSequence is similar to a regular Swift Sequence, except that
every value is awaited asynchronously
You’ve already seen an example of this with Task Groups:
await withTaskGroup(of: UIImage.self) { group in
for person in people {
group.addTask { await person.loadAvatar() }
}
var result = [UIImage]()
for await image in group {
result.append(image)
}
return result
}
Slide 76
Slide 76 text
2 AsyncSequence
Async Sequences
Swift includes various built-in methods that leverage Async Sequences
Let’s explore a few of these
!
Files
"
URL Requests
Slide 77
Slide 77 text
2 AsyncSequence
Async Sequences
Let’s say we have a huge CSV file with hundreds of thousands of lines,
and we want to fetch and parse it into a struct:
1,Shai
2,Elia
3,Ethan
4,Ariel
5,Natan
6,Yuval
7,Igor
..+
300000,Steve
Slide 78
Slide 78 text
2 AsyncSequence
Async Sequences
A naive way would be fetching the file in its entirety and then iterating
over all of its available lines:
let (data, _) = try await URLSession.shared.data(for: URLRequest(url: largeCSV))
var people = [Person]()
let lines = String(data: data, encoding: .utf8)?'components(separatedBy: "\n") ?, []
for line in lines {
people.append(Person(csvLine: line))
}
Greedily fetch the entire file, parse it as a single large String and split
into lines
Slide 79
Slide 79 text
2 AsyncSequence
Async Sequences
But there’s a better way, with the new bytes(for:) API:
let (bytes, _) = try await URLSession.shared.bytes(for: URLRequest(url: largeCSV))
var people = [Person]()
for try await line in bytes.lines {
people.append(Person(csvLine: line))
}
Processes the lines lazily, while the data transfer is ongoing
Slide 80
Slide 80 text
2 AsyncSequence
Async Sequences
We can do the same for reading local files:
Reads the entire file synchronously, and then parses the lines
let data = FileManager.default.contents(atPath: localFile.absoluteString) ?, Data()
var people = [Person]()
let lines = String(data: data, encoding: .utf8)?'components(separatedBy: "\n") ?, []
for line in lines {
people.append(Person(csvLine: line))
}
Slide 81
Slide 81 text
2 AsyncSequence
Async Sequences
We can do the same for reading local files:
let handle = try FileHandle(forReadingFrom: localFile)
var people = [Person]()
for try await line in handle.bytes.lines {
people.append(Person(csvLine: line))
}
Read the lines one-by-ones using the new bytes API on FileHandle
Slide 82
Slide 82 text
AsyncSequence
3 More async/await
It’s simply a protocol, so you can easily conform to it and make your
own async sequences - but we’ll get to that in a bit :)
Slide 83
Slide 83 text
4
Creating
Act s
Slide 84
Slide 84 text
Actors
4 Actors
Working with Actors is a very wide and deep topic, so we’ll
only touch some of the “must knows” of it
It’s worth digging up the documentation and relevant WWDC
Sessions (WWDC21-10133)
Slide 85
Slide 85 text
Trea
Actors
4 Actors
One of the trickiest things to get right in asynchronous
programming is Data Races
A data race usually involves shared mutable state, with two or
more threads trying to access that state at the same time - and
at least one of these accesses is a write
Shared State
One of these threads might get the
wrong piece of data, or flat-out crash
and burn 5
Thread
1
Thread
2
Slide 86
Slide 86 text
Actors
4 Actors
While value types can help tremendously in data races, usually
we solve them with more involved mechanisms:
NSLock
NSRecursiveLock
os_unfair_lock
Mutex
Serial Dispatch Queue
These work and provide mutually exclusive access, but very
hard to get just right
Slide 87
Slide 87 text
Actors
4 Actors
Actors are reference-type objects (like Classes) that provide
automatic synchronization and isolation for its state
Actor
Read/Write
Synchronous
Different
Actor
Its origins are rooted in high-scale languages such as Akka
and Erlang
Read/Write
Asynchronous
Slide 88
Slide 88 text
Actors
4 Actors
Concurrent read/write races are extremely common in even the
simplest of scenarios:
class DataSource {
let items = ["Shai", "Elia", "Ethan",
"Ariel", "Natan", "Yuval", "Igor"]
private var index = 0
func next() {
print(items[index])
if items.indices.contains(index + 1) {
index += 1
} else {
index = 0
}
}
}
let ds = DataSource()
DispatchQueue.global().async { ds.next() }
DispatchQueue.global().async { ds.next() }
DispatchQueue.global().async { ds.next() }
Shai
Shai
Shai
Prints:
Slide 89
Slide 89 text
Actors
4 Actors
Concurrent read/write races are extremely common in even the
simplest of scenarios:
actor DataSource {
let items = ["Shai", "Elia", "Ethan",
"Ariel", "Natan", "Yuval", "Igor"]
private var index = 0
func next() {
print(items[index])
if items.indices.contains(index + 1) {
index += 1
} else {
index = 0
}
}
}
let ds = DataSource()
DispatchQueue.global().async { ds.next() }
DispatchQueue.global().async { ds.next() }
DispatchQueue.global().async { ds.next() }
Slide 90
Slide 90 text
Actors
4 Actors
actor DataSource {
let items = ["Shai", "Elia", "Ethan",
"Ariel", "Natan", "Yuval", "Igor"]
private var index = 0
func next() {
print(items[index])
if items.indices.contains(index + 1) {
index += 1
} else {
index = 0
}
}
}
let ds = DataSource()
Task.detached { await ds.next() }
Task.detached { await ds.next() }
Task.detached { await ds.next() }
Shai
Elia
Ethan
Prints:
When working “outside the actor”, you must access the data
asynchronously:
Slide 91
Slide 91 text
@MainActor
4 Actors
MainActor.run
There’s another special kind of actor called The Main Actor. In
essence, it represents the Main Thread.
DispatchQueue.main.async
=
Slide 92
Slide 92 text
@MainActor
4 Actors
You can annotate specific methods or even entire types with
@MainActor, to make sure they always run on the main thread:
@MainActor class AwesomeViewModel {
func doesUIThing1() {
/$ Runs on Main Thread
}
func doesUIThing2() {
/$ Runs on Main Thread
}
}
Slide 93
Slide 93 text
@MainActor
4 Actors
You can opt-out specific methods so they don’t run on the
main actor by marking them as nonisolated:
@MainActor class AwesomeViewModel {
func doesUIThing() {
/$ Runs on Main Thread
}
nonisolated func doesNonUIThing() {
/$ Not isolated to Main Actor
}
}
Slide 94
Slide 94 text
A few more topics
4 Actors
We only looked into a portion of the Actors story, there are
many topics we haven’t looked into at all, such as:
⚪ @Sendable
⚪ @globalActor
⚪ @preconcurrency
⚪ nonisolated
⚪ Using the Thread Sanitizer with Actors
⚪ And much more …
Slide 95
Slide 95 text
7
Creating
Using
async/await
in an existing code base
Slide 96
Slide 96 text
Deployment Target
7 Using in your codebase
Swift Concurrency is available on iOS 13 and up
It is built into the SDK on iOS 15, and back-deployed in older
OS versions by the concurrency compatibility library
Slide 97
Slide 97 text
Creating your own continuations
So far, you’ve learned about existing async functions, but
you’ll often want to create your own to wrap other closure-
based functions, for example:
You can do this using these built-in functions:
withCheckedContinuation
withCheckedThrowingContinuation
withUnsafeContinuation
withUnsafeThrowingContinuation
7 Using in your codebase
Slide 98
Slide 98 text
Creating your own continuations
Given this closure-based function:
You can wrap it as an async function like so:
func loadImage(completion: (Result) -) Void)
func loadImage() async throws -) UIImage {
try await withCheckedThrowingContinuation { continuation in
loadImage { result in
switch result {
case .success(let image):
continuation.resume(returning: image)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
7 Using in your codebase
Slide 99
Slide 99 text
Creating your own continuations
There’s even an overload to resume with a Result:
func loadImage() async throws -) UIImage {
try await withCheckedThrowingContinuation { continuation in
loadImage { result in
continuation.resume(with: result)
}
}
}
Note: You mustn’t resume the continuation more than once!
7 Using in your codebase
Slide 100
Slide 100 text
Creating your own Async Sequences
enum StockChange {
case change(Stock)
case finished
}
func monitorStocks(stockChange: (StockChange) -) Void) {
..+
}
7 Using in your codebase
As mentioned before, AsyncSequence is simply a protocol.
But luckily, you don’t have to implement it yourself.
You can simply use AsyncStream, instead
Slide 101
Slide 101 text
Creating your own Async Sequences
func monitorStocks() -) AsyncStream {
AsyncStream { continuation in
monitorStocks { change in
switch change {
case .change(let stock):
continuation.yield(stock)
case .finished:
continuation.finish()
}
}
}
}
Task {
for await stock in monitorStocks() {
print("Current stock: \(stock)")
}
}
7 Using in your codebase
As mentioned before, AsyncSequence is simply a protocol.
But luckily, you don’t have to implement it yourself.
You can simply use AsyncStream, instead
Slide 102
Slide 102 text
x
Objective-C Interoperability
- (void)fetchNumbers:(void(^)(NSArray
Slide 103
Slide 103 text
Objective-C Interoperability
You can further refine their name and usage using
NS_SWIFT_ASYNC_NAME and NS_REFINED_FOR_SWIFT_ASYNC
- (void)fetchNumbers:(void(^)(NSArray
Slide 104
Slide 104 text
Objective-C Interoperability
As you can imagine, this means that most Apple-built code that has
completion handlers gets free bridging to async/await
7 Using in your codebase
Slide 105
Slide 105 text
SwiftUI
To execute async functions from within your SwiftUI code, simply
use the new .task modifier (iOS 15+)
7 Using in your codebase
struct MyView: View {
@State var numbers = [Int]()
var body: some View {
Text("\(numbers.count) numbers")
.task {
self.numbers = await fetchNumbers()
}
}
}
iOS 15+
struct MyView: View {
@State var numbers = [Int]()
var body: some View {
Text("\(numbers.count) numbers")
.onAppear {
Task { @MainActor in
self.numbers = await fetchNumbers()
}
}
}
}
iOS 13+
Slide 106
Slide 106 text
And what about Combine?
You might’ve thought to yourself - this AsyncSequence thing is pretty
similar to a Combine Publisher!
7 Using in your codebase
Well, you’re absolutely right - and you can even use a Publisher this way
using the values property:
Combine & Sink: Combine & async/await:
numbers
.map { $0 * 2 }
.prefix(3)
.dropFirst()
.sink(
receiveCompletion: { _ in print("Done!") },
receiveValue: { print("Value \($0)") }
)
.store(in: &subscriptions)
let publisher = numbers
.map { $0 * 2 }
.prefix(3)
.dropFirst()
for await number in publisher.values {
print("Value \(number)")
}
print("Done!")
Slide 107
Slide 107 text
Combine without Combine?
But what about all of these awesome operators?! How can I use them in async/
await, without Combine?
7 Using in your codebase
Apple seems to be shifting its focus heavily towards async/await, as evident by
their latest release of swift-async-algorithms: A set of Combine-like operators
for AsyncSequences:
https://github.com/apple/swift-async-algorithms
Slide 108
Slide 108 text
Le n m e?
Want to
83
Slide 109
Slide 109 text
8 Want to learn more? 3
https://SwiftAsyncAwait.com
Slide 110
Slide 110 text
Free b k time!
https://bit.ly/async-free-book
Slide 111
Slide 111 text
Shai Mishali
freak4pc
Thank y !
https://bit.ly/async-free-book