Swift Structured Concurrency

YLabZ
19 min readJul 19, 2021

--

Shoreline Lake Mt. View CA

Presented as part of the 360iDev Conference.

360i|Dev Workshop

2021 & 2022

Topics Covered

  1. Async / Await [Section 1]
  2. Async sequences (Sequences / Streams)
  3. Effectful read-only properties
  4. Structured concurrency
  5. async let bindings
  6. Continuations for interfacing async tasks with synchronous code
  7. Task & Task Groups [Section 2]
  8. Actors [Section 3]
  9. Global Actors
  10. Sendable and @Sendable closures
  11. Instruments 14 beta — Product — Profile —Swift Concurrency

Resources below → at the end of the article 👇🏽

New from WWDC22

  • Eliminate data races using Swift Concurrency
  • Visualize and optimize Swift concurrency
  • Meet distributed actors in Swift
  • Meet Swift Async Algorithms

Video:

Video Series:

Everything Well Explained!

Playground:

Thanks to Nick Sarno for the wonderful material!

https://github.com/SwiftfulThinking/Swift-Concurrency-Bootcamp

Thanks to Paul Hudson for this wonderful material!

Reference: https://github.com/twostraws/whats-new-in-swift-5-5

Playground Code: https://github.com/developerY/MulitSwift

CodeLab:

“Update a sample app” from WWDC21:

Trying to follow the video is very hard and has some errors. We have a Git repo where every step is a commit. This makes it much easier to follow.

Example of Clean Arch with structured concurrency bike sharing App

Video: <YouTube Link>

Details below in the Codelab section below.

Background: Understanding the problem

We can say that there are quite a lot of problems with GCD approach:

  • Context switches are expensive operations
  • Spawning multiple threads can lead to thread explosions
  • You can (accidentally) block threads and prevent further code execution
  • You can create a deadlock if multiple tasks are waiting for each other
  • Dealing with (completion) blocks and memory references are error prone
  • It’s really easy to forget to call the proper synchronization block

At present, if you are writing concurrent code, you are probably using Swift’s thread sanitizer to detect data races like the one listed above.

Turn on thread sanitizer under: Edit Scheme →Run/Debug →Diagnostics turn on ✅ Thread Sanitizer.

Memory & Threads

Each thread has it’s own copy of a (call) Stack but share the Heap. And that is why we must use [weak self] when accessing classes/escaping closures which can be removed when navigating away from the current screen.

https://www.backblaze.com/blog/whats-the-diff-programs-processes-and-threads/

Review from above video:

Value Types

  • Struct, Enum, String, Int, etc …
  • Stored in the Stack
  • Faster
  • Thread Safe
  • When you assign or pass a value type a new copy of data is created.

Reference Types

  • Classes, Functions, Actors
  • Stored in the Heap
  • Slower (Swift optimizes Structs) but more memory efficient.
  • Usually NOT tread safe (Actors are)
  • When you assign or pass a reference no new data is created just a reference is passed (the address of the instance (pointer) is copied not the data)

Stack (Call Stack)

  • Fixed at compile time and does not grow as the application runs
  • Push / Pop from stack as instructions run
  • Stores value types
  • Variables allocated on the Stack are stored directory to the memory, and access is very fast. Very important for SwiftUI.
  • Each thread has it’s own stack

Heap

  • Size changes as the program runs.
  • Stores Reference Type
  • Shared across threads

Struct

  • Based on Values
  • Immutable — mutated makes a new copy to replace it
  • Pushed / Popped on the (call) Stack

Classes

  • Based on REFERENCES (INSTANCES)
  • Stored in the Heap!
  • Inherit from other classes (Use composition over Inheritance)

Actors

  • Same as Class, but thread safe!
  • Must run in a async / await environment

When to use what:

Structs → Data, Views

Classes → ViewModel (ObservableObject)

Actors → Shared Repository across threads.

Async / Await in Swift

WWDC Video: → Meet async/await in Swift ← https://developer.apple.com/videos/play/wwdc2021/10132/

Today we need to pass a completion handler.

This causes complex code with multiple places to produce bugs.

WWDC 2021 Meet async/ await in Swift

Quoted from above: What’s new is Swift5.5:

It’s possible for those functions to call their completion handler more than once, or forget to call it entirely.

The parameter syntax @escaping (String) -> Void can be hard to read.

At the call site we end up with a so-called pyramid of doom, with code increasingly indented for each completion handler.

Until Swift 5.0 added the Result type, it was harder to send back errors with completion handlers.

  • Forget to call the completion handler when an error occurs.

We can turn this jumbled mess to “structured code” to help with local reasoning:

async / await with extension function

Look at converting the old to the new way :-)

Thanks to Paul Hudson for the example code!

These are suspend points not concurrency.

We can now start to use Swift Structured Concurrency to make things better!

Async / Await — Section 1

Async — functions that do not block. When we call them with await they release to the system. Might start on another thread.

Await — “run this function asynchronously and wait for its result to come back before continuing.”

The “await” keyword is not needed by the Swift compiler but is forced by the compiler so the program can use local reasoning to understand the program.

This is not concurrency. It is a way to handle a suspend function so the system is free to do other tasks.

Main Points:

  • Async enables a function to suspend.
  • Await marks where an async function may suspend execution.
  • Other work can happen during a suspension.

This is an async function so any await will suspend the current task. This runs in a blocking way for the program but non-blocking for the OS.

WWDC 2021

Apple is moving all code that used to use completion handlers to async / await and renaming them by removing the leading “get” from the method call.

Continuations are used to bridge the gap between current code and new async / await.

Continuations

continuations to solve this problem, allowing us to create a bridge between older functions with completion handlers and newer async code.

Bridge gap between old and new condition.

Running in Parallel

Calling Asynchronous Functions in Parallel

WWDC Video: → Explore structured concurrency in Swift ← https://developer.apple.com/videos/play/wwdc2021/10134/

Call async let with NO await — runs the code in parallel!

  • Call asynchronous functions with await when the code on the following lines depends on that function’s result. This creates work that is carried out sequentially.
  • Call asynchronous functions with async-let when you don’t need the result until later in your code. This creates work that can be carried out in parallel.
  • Both await and async-let allow other code to run while they’re suspended.
  • In both cases, you mark the possible suspension point with await to indicate that execution will pause, if needed, until an asynchronous function has returned.

async let bindings

Understand await (serial) & async let (parallel)

  • Use await when it’s important you have a value before continuing.
  • use async let when your work can continue without the value for the time being.
The try is used when you access the variable

The addition of async/await fits perfectly alongside try/catch,

Task Cancellation

A parent can only finish if all the child tasks are finished— Canceling all children will cancel the parent. To keep tasks from leaking we must check the cancelation. Call in from task that can be canceled.

Cancellation is cooperative:

  • Tasks are not stopped immediately when cancelled
  • Cancellation can be checked from anywhere
  • Design your code with cancellation in mind
This returns a partial result

Task & TaskGroup — Section 2

  • Task: one or two independent pieces of work to start,
  • TaskGroup: split up one job into several concurrent operations (Homogeneous Data Type)

Both Task and TaskGroup can be created with one of four priority levels: 1.High, 2. Default, 3. Low, and 4.background

Task

How to create a Task.

`Sample code for this description … coming soon`

In this case there are potentially four thread swaps happening in our code:

  • All UI work runs on the main thread, so the button’s action closure will fire on the main thread.
  • Although we create the task on the main thread, the work we pass to it will execute on a background thread.
  • Inside loadMessages() we use await to load our URL data, and when that resumes we have another potential thread switch — we might be on the same background thread as before, or on a different background thread.
  • Finally, the messages property uses the @State property wrapper, which will automatically update its value on the main thread. So, even though we assign to it on a background thread, the actual update will get silently pushed back to the main thread.
Task with possible return error

Understanding the return value:

  1. Our task might return a string, but also might throw one of two errors. So, when we ask for its result property we’ll be given a Result<String,Error>.
  2. Although we need to use await to get the result, we don’t need to use try even though there could be errors there. This is because we’re just reading out the result, not trying to read the successful value.
  3. We call get() on the Result object to read the successful, but that’s when try is needed because it’s when Swift checks whether an error occurred or not.

When it comes to catching errors, we need a “catch everything” block at the end, even though we know we’ll only throw LoadError.

Task Group withTrowingTaskGroup code sample coming soon.

Task Group

Doing more than one download at a time:

The thumbnails[id] is not concurrent. Must use Actors to collect the data!!

Date-race safety:

  • Task creation takes a @Sendable closure
  • Cannot capture mutable variables
  • Should only capture value types, actors or classes that implement their own synchronization.

See → Protect mutable state with Swift actors

use for try await because it runs synchronously

Unstructured Tasks

Not all tasks fit a structured pattern

  • Some tasks need to launch from non-async contexts
  • Some tasks live beyond the confines of a single context (delegate using the @MainActor )

async { let <var> = await func()}

  • Inherit actor isolation and priority of the origin context
  • Lifetime is not confined to any scope
  • Can be launched anywhere, even non-async functions
  • Must be manually cancelled or awaited

Detached Tasks

— Try not to use detached task — Complex to manage —

  • Un-scoped lifetime, manually cancelled and awaited
  • Do not inherit anything from their originating context
  • Optional parameters control priority and other traits

Summery table of when to use Tasks/Group/Detached

Summery table of tasks

Async Sequences

Sequences and Streams

WWDC Video: → Meet AsyncSequence ← https://developer.apple.com/videos/play/wwdc2021/10058/

A type that provides asynchronous, sequential, iterated access to its elements.

An AsyncSequence doesn’t generate or contain the values; it just defines how you access them.

Code: for try await line in url.lines {print(“Received user: \(line)”)}

ADD CODE SAMPLE HERE: Coming soon … Let afoo = async { // concurrent do { for try await fun() } catch …} afoo.cancel()

Async loop.
  • notifications
  • filters

Code:// for loop suspends on every line.

for try await line in handle.bytes.lines {print(line)}

Just like any sequence BUT:

  • Suspend on each element as it is delivered asynchronous
  • Suspend on each value or error ( can throw on each value)
  • If throw will exit / if not throw will continue until done

The AsyncSequence protocol also provides default implementations of a variety of common methods, such as map(), compactMap(), allSatisfy(), and more.

Filters on sequences

URLSession with async / await

WWDC Video: → Use async/await with URLSession ← https://developer.apple.com/videos/play/wwdc2021/10095/

Swift concurrency provides:

  • Linear
  • Concise
  • Native error handling

Existing Code:

The old way of doing it

We have three different execution contexts in total.

  • Outmost layer runs on whatever thread or queue of the caller
  • URLSessionTask CompletionHandler runs on session’s delegate queue
  • The final completion handler runs on the main queue.

Since the compiler cannot help us, we have to use extreme caution to avoid any threading issues such as data races.

This has three (3) mistakes:

  • The calls to the completion handler are not consistently dispatched to the main queue.
  • Missing an early return here. The completionHandler can be called twice if we got an error.
  • UIImage completion could fail

New way of doing it.

New way of getting the photo

Upload and Download

Upload / Download

Using the URLSession to get a photo using async bytes.

Consume URL response line by line as received

AsyncSequence

  • Built-in transformations
  • System frameworks support

Authentication has builtin support.

Effectful read-only properties

Coming soon …

Actors — Section 3

WWDC Video: → Protect mutable state with Swift actors ← https://developer.apple.com/videos/play/wwdc2021/10133/

Actor Type

→ Isolate their instance data from the rest of the program ←

  • Actors provide synchronization for shared mutable state
  • Actors isolate their state from the rest of the program
  • All access to that state goes through the actor
  • The actor ensures mutually-exclusive access to its state

Actors — offer data isolation within a type in such a way that the code you write cannot create data races.

Actors = single thread Class.

This way the actor itself accesses its data synchronously, but any other code from outside is required asynchronous access (with implicit synchronization) to prevent data races.

→ Actors can protect internal state through data isolation ensuring that only a single thread will have access to the underlying data structure at a given time.

First of all, actors are reference types, just like classes. They can have methods, properties, they can implement protocols, but they don’t support inheritance.

Actors and classes have some similarities:

  • Both are reference types, so they can be used for shared state.
  • They can have methods, properties, initializers, and subscripts.
  • They can conform to protocols, be generic and have extensions.
  • Any properties and methods that are static behave the same in both types, because they have no concept of self and therefore don’t get isolated.

Beyond actor isolation, there are two other important differences between actors and classes:

  • Actors do not currently support inheritance.
  • All actors implicitly conform to a new Actor protocol.
  • Actors pass messages, not memory. — we instead send a message asking for the data and let the Swift runtime handle it for us safely.

Extensions on the Actor do not need to use await because they are already a part of the actor. They can change local state without issues.

Code sample … coming soon …

`asyncDetached { print(await counter.increment())}`

`extension Actor { // this is running in a single code`

func runSlow() { // does not need await because running on the Actor}}

Actor Reentrancy

Notice private var cache: [URL: Image] = [:] // cache[url] can be corrupted!

Make sure you check your assumptions — await (suspension) things can change.

  • Perform mutation in synchronous code. (restore concurrency after an await)
  • Expect that the actor state could change during suspension
  • Check your assumptions after an await
  • Global State, Clocks, Timers, etc … Checked after await

Actor Isolation

Actors conform to protocols.

— Can not access mutable state on the Actor. —

Compiler checks actor isolation:

Here we see that the this could be called from outside the actor and is not async.

nonisolated means the method is outside the Actor. Does not change the state of the Actor, so it is OK to do this.

Closures

Closures can be Actor isolated / or nonisolated. If the closure is isolated we are protected.

Code Example … coming soon …

Detached Task is works along as other are working

`extension account{func read() -> Int {…}

func readLater() {asyncDetached { // concurrent — not isolated to actorawait read() // must use await}}}

If runs inside Actor (isolated) if runs outside (nonisolated and must use await)`

Using structs with Actors is safe. Using Classes could cause race conditions.

Passing data into and out of Actors

Sendable / @Sendable

Sendable types are safe to share concurrently.

Sendable

  • Value Types
  • Actors types
  • Classes (have to be done carefully)
  • Immutable Classes
  • Internally-synchronized class
  • @Sendable function types

Use a Struct which is a value type we have no problems. But if we use a class and in an Actor and outside then we have a race state.

Checking Sendable — The compiler will check this in the future sending on Sendable types across Actor boundaries.

Compiler will check this: `struct book: Sendable { var title: String var [authors]}`

@Sendable functions

Check Sendable by adding a conformance … code sample needed

Propagate Sendable by adding a conditional conformance

  • @Sendable functions types conform to the Sendable protocol
  • @Sendable places restrictions on closures
  • No mutable captures
  • Captures must be of Sendable type
  • Cannot be both synchronous and actor-isolated

Types that are safe to use concurrently.

Sendable types

  • Sendable types are safe to share concurrently

Sendable types and closures help actor isolation.

Check by the compiler

Main Actor

Interacting with the main thread

The Main Thread.

  • UI rendering
  • Main run loop to processing events

Do work off the main thread whenever possible and call DispatchQueue.main.async performs updates on the main thread.

→ Running on the main thread is like running on an Actor.

DispatchQueue.main.async performs updates on the main thread.

The main actor

  • Actor that represents the main thread

async on the main thread.

Always run UI on the main thread.

Types can be placed on the @MainActor

  • Implies that all methods and properties of the type are MainActor
  • Opt out individual members with nonisolated

Actors

  • Use actors to synchronize access to mutable state
  • Design for actor reentrancy
  • Prefer value types and actors to eliminate data races
  • Leverage the main actor to protect UI interactions

SwiftUI

Pull to refresh

Behind the Scenes (Understand the details)

WWDC Video: → Swift concurrency: Behind the scenes ← https://developer.apple.com/videos/play/wwdc2021/10254/

Better mental model:

  • Threading Model (new thread pool) porting your code
  • Synchronization

Threading Model:

GCD vs Swift Concurrence

How we build apps today:

Work off the main thread and access to the DB is protected.

How we process the results:

This has many issues:

this could lead to tons of threads on the completion block.

Excessive concurrency

  • Overcommitting the system with more threads than CPU cores.
  • Thread explosion
  • Performance costs — Memory overhead / Scheduling overhead

Scheduling overhead

  • Timesharing of threads
  • Excessive context switching
  • Threading hygiene

Swift Concurrency is built with performance and efficiency. (controled / structured and safe way)

Instead of blocked threads and context switches we have continuations.

Runtime behavior / Runtime contract / Language features

Language features

  • await and non-blocking of threads
  • Tracking of dependencies in Swift task model

How are async are implemented

Language Features

  • efficient threading — await non-blocking of threads
  • Tracking of dependencies in Swift task model

reason about how threads work to allow a runtime contract that produces “forward progress of threads”

Cooperative thread pool

  • Default executor for Swift
  • Width limited to the number of CPU cores
  • Controlled granularity of concurrency
  • - worker threads don’t block
  • - Avoid thread explosion and excessive context switches

GCD =Queue per subsystem — choosing the right number of queues is important

Swift = Default runtime helps you maintain the right concurrency limits

Adoption of Swift concurrency

  • Concurrency comes with costs
  • Ensure that benefits of concurrency outweighs costs of managing it
  • Profile your code!

Await and atomicity !!!

  • NO guarantee that the tread which executed the code before the await will execute the continuation as well
  • Breaks atomicity by voluntarily descheduling the task

Cooperative thread pool

— Preserving the runtime contract ( Forward progress )

Do not use Unsafe
  • Don’t use unsafe primitives to await across task boundaries
  • Test app with debug environment variable LIBDISPATCH_COOPERATIVE_POOL_STRICT=1

Synchronization

  • Mutual exclusion

Reentrancy and prioritization — Actors — allows the program to run smoothly.

  • GSD — has priority inversion
  • Actor reentrancy: this is not a FIFO but priority: D2 started after D1 but runs first.

Main actor — is not a part of the cooperative thread pool.

Watch out for Actor Hopping!

Code Exercises

Playgrounds

CodeLab

WWDC Video: → Swift concurrency: Update a sample app ← https://developer.apple.com/videos/play/wwdc2021/10194/

We will outline the steps in the WWDC update app. Following the Apple video is difficult and has errors. The line below is not even valid code !!!!????!!!!

Apple video has bugs in it!

Each step is a commit in Git

Step 1: Understand the App.

Coffee Watch Complication:

The structure of the code is very good but the concurrency is a mess

And we will move to this

  • Coffee Data is the model for the view.

Start with async / await —

Step 1:

HealthKitContoller and look for the save function <control 6>

  • we want to change the code to have local reasoning.
  • this is true about value types

Now we can have async that can throw.

— remove the completion handler and put await in the front.

Add try and handle the error by placing it in a do / catch block

But now we are try to call sync from a normal function!

Making this function async has pushed the problem up a level.

In CoffeData model now we get the async error. Here we will spin off a new async task. this is the same as calling the global dispatch que. So everything we do must be self contained.

User Completion Handlers —

requestAuthorizatoin takes a completoin handleer

This code is not thread safe.

Cammand Shift A — code completion menu

calls into the new func with deprication warrning

This code is still not thread safe but we will fix it by using actors.

The return is a bug! but with async you must return value.

we return true for the happy path and false for the error.

NOW the whole project compiles and runs.

Commit number

Using Continuations

HealthKitContoller loadNewDataFromHealthKit

We want to await on the save so we need to break this into two parts.

instead of hopping back ands forth we want to use a continuation.

newDrinks is shared. so we need to make it immuatlabe to be shared with the

MainActor. —

Make sure we are running on the Main Thread … forget to put an assert.

Better to let the compiler to do this for you!

Try to keep the code isolated as your work.

  1. Async → make it an Actor.

References:

GoogleDoc~https://docs.google.com/document/d/1OmAzU2BcWyrRVg70IL6vfwXQ3Owpjr-vdg4JyYT02kk/edit?usp=sharing~

Important Videos:

https://www.hackingwithswift.com/articles/239/wwdc21-wrap-up-and-recommended-talks

What’s new in Swift

  • Meet async/await in Swift [x]

https://developer.apple.com/videos/play/wwdc2021/10132/

  • Explore structured concurrency in Swift [x]

https://developer.apple.com/videos/play/wwdc2021/10134/

  • Protect mutable state with Swift actors

https://developer.apple.com/videos/play/wwdc2021/10133/

  • Swift concurrency: Update a sample app

https://developer.apple.com/videos/play/wwdc2021/10194/

  • Meet AsyncSequence

https://developer.apple.com/videos/play/wwdc2021/10058/

  • Discover concurrency in SwiftUI

https://developer.apple.com/videos/play/wwdc2021/10019/

  • Use async/await with URLSession

https://developer.apple.com/videos/play/wwdc2021/10095/

  • Bring Core Data concurrency to Swift and SwiftUI

https://developer.apple.com/videos/play/wwdc2021/10017/

  • Swift concurrency: Behind the scenes
  • Eliminate data races using Swift Concurrency
  • Visualize and optimize Swift Concurrency

Post

https://www.andyibanez.com/posts/modern-concurrency-in-swift-introduction/

https://twissmueller.medium.com/the-wwdc21-guide-to-swift-concurrency-32bba1a5a98c

Tutorials:

Lets answer these questions

  1. iOS App Life Cycle, application states, AppDelegate & SceneDelegate
  2. Class VS Struct
  3. Functions and closures

A closure is said to escape a function when the closure is passed as an argument to the function, but is called after the function returns. When you declare a function that takes a closure as one of its parameters, you can write @escaping before the parameter’s type to indicate that the closure is allowed to escape.

https://docs.swift.org/swift-book/LanguageGuide/Closures.html
  1. Protocols
  2. Extensions
  3. SOLID principles with example
  4. Higher order functions
  5. Hashable & Equatable
  6. Property wrappers
  7. Static function vs class function
  8. Final keyword
  9. Access specifiers
  10. Enums
  11. Storage
  12. Multithreading
  13. Agile process
  14. CI CD — how to use it like how the process will be (not the setup)
  15. Unit testing

No responses yet