Presented as part of the 360iDev Conference.
360i|Dev Workshop
2021 & 2022
Topics Covered
- Async / Await [Section 1]
- Async sequences (Sequences / Streams)
- Effectful read-only properties
- Structured concurrency
- async let bindings
- Continuations for interfacing async tasks with synchronous code
- Task & Task Groups [Section 2]
- Actors [Section 3]
- Global Actors
- Sendable and @Sendable closures
- 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.
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.
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:
Look at converting the old to the new way :-)
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.
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.
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.
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 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
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.
Understanding the return value:
- 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>.
- 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.
- 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
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
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()
- 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.
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:
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.
Upload and Download
Using the URLSession to get a photo using async bytes.
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.
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
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 )
- 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 !!!!????!!!!
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.
- 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
- 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
- iOS App Life Cycle, application states, AppDelegate & SceneDelegate
- Class VS Struct
- 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.
- Protocols
- Extensions
- SOLID principles with example
- Higher order functions
- Hashable & Equatable
- Property wrappers
- Static function vs class function
- Final keyword
- Access specifiers
- Enums
- Storage
- Multithreading
- Agile process
- CI CD — how to use it like how the process will be (not the setup)
- Unit testing