Appearance
Swift Concurrency
The concurrency model in Swift is built on top of threads, but you don’t interact with them directly.
An asynchronous function in Swift can give up the thread that it’s running on, which lets another asynchronous function run on that thread while the first function is blocked. When an asynchronous function resumes, Swift doesn’t make any guarantee about which thread that function will run on.
It’s possible to write concurrent code without using Swift’s language support.
async/await
Ordinary, synchronous functions and methods either run to completion, throw an error, or never return.
An asynchronous function
or asynchronous method
still does one of those three things, but it can also pause in the middle when it’s waiting for something.
Inside the body of an asynchronous function or method, you mark each of these places where execution can be suspended.
You write the async
keyword in its declaration after parameters, before throws
if have and before the return arrow (->) to indicate that a function or method is asynchronous.
swift
func listPhotos(inGallery name: String) async -> [String] {
let result = // ... some asynchronous networking code ...
return result
}
When calling an asynchronous method, execution suspends until that method returns. You write await
in front of the call to mark the possible suspension point.
swift
let photoNames = await listPhotos(inGallery: "Summer Vacation")
let sortedNames = photoNames.sorted()
let name = sortedNames[0]
let photo = await downloadPhoto(named: name)
show(photo)
Because code with await needs to be able to suspend execution, only certain places in your program can call asynchronous functions or methods:
- Code in the body of an asynchronous function, method, or property.
- Code in the static
main()
method of a structure, class, or enumeration that’s marked with@main
. - Code in an unstructured child task.
When adding concurrent code to an existing project, work from the top down. Specifically, start by converting the top-most layer of code to use concurrency, and then start converting the functions and methods that it calls, working through the project’s architecture one layer at a time. There’s no way to take a bottom-up approach, because synchronous code can’t ever call asynchronous code.
completionHandler vs async/await
completionHandler
style function
swift
func fetchThumbnail(for id: String, completion: @escaping (UIImage?, Error?) -> Void) {
let request = thumbnailURLRequest(for: id)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if error = error {
completion(nil, error)
} else if (response as? HTTPURLResponse)?.statusCode != 200 {
completion(nil, FetchError.badID)
} else {
guard let image = UIImage(data, data!) else {
completion(nil, FetchError.badImage)
return
}
image.prepareThumbnail(of: CGSize(width: 40, height: 40)) { thumbnail in
guard let thumbnail = thumbnail else {
completion(nil, FetchError.badImage)
return
}
completion(thumbnail, nil)
}
}
}
task.resume()
}
async/await
style function
swift
func fetchThumbnail(for id: String) async throws -> UIImage {
let request = thumbnailURLRequest(for: id)
let (data, response) = try await URLSession.shared.data(from: request)
guard (response as? HTTPURLResponse)?.statusCode == 200 else {throw FetchError.badID}
let maybeImage = UIImage(data: data)
guard let thumbnail = await maybeImage?.thumbnail else {throw FetchError.badImage}
return thumbnail
}
extension UIImage {
var thumbnail: UIImage? {
get async {
let size = CGSize(width: 40, height: 40)
return await self.byPreparingThumbnail(ofSize: size)
}
}
}
Only read-only properties can be async
.
completionHandler
style testcase
swift
class MockViewModelSpec: XCTestCase {
func testFetchThumbnails() throws {
let expectation = XCTestExpectation(description: "mock thumbnails completion")
self.mockViewModel.fetchThumbnail(for: mockID) { result, error in
XCTAssertEqual(result?.size, CGSize(width: 40, height: 40))
expectation.fulfill()
}
wait(for: [expectation], timeout: 5.0)
}
}
async/await
style testcase.
swift
class MockViewModelSpec: XCTestCase {
func testFetchThumbnails() async throws {
let result = try await self.mockViewModel.fetchThumbnail(for: mockID)
XCTAssertEqual(result.size, CGSize(width: 40, height: 40))
}
}
continuation
Continuation with completionHandler
swift
func persistentPosts() async throws -> [Post] {
typealias PostContinuation = CheckedContinuation<[Post], Error>
return try await withCheckedContinuation { (continuation: PostContinuation) in
self.getPersistentPosts {posts, error in
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: posts)
}
}
}
}
Continuation with delegate
swift
class ViewController: UIViewController {
private var activeContinuation: CheckedContinuation<[Post], Error>?
func sharedPostsFromPeer() async throws -> [Post] {
try await withCheckedThrowingContinuation { continuaiton in
self.activeContinuation = continuaiton
self.peerManager.syncSharedPosts()
}
}
}
extension ViewController: PeerSyncDelegate {
func peerManager(_ manager: PeerManager, received posts: [Post]) {
self.activeContinuation?.resume(returning: posts)
self.activeContinuation = nil // guard against multiple calls to resume
}
func peerManager( _manager: PeerManager, hadError error: Error) {
self.activeContinuation?.resume(throwing: error)
self.activeContinuation = nil // guard against multiple calls to resume
}
}
Checked continuations:
- Continuation must be resumed exactly once on every path.
- Discarding the continuation without resuming is not allowd.
for-await-in loop
A for-await-in
loop iterates over an asynchronous sequence looks like:
swift
import Foundation
let handle = FileHandle.standardInput
for try await line in handle.bytes.lines {
print(line)
}
async-let
To call an asynchronous function and let it run in parallel with code around it, write async
in front of let
when you define a constant, and then write await
each time you use the constant.
swift
async let firstPhoto = downloadPhoto(named: photoNames[0])
async let secondPhoto = downloadPhoto(named: photoNames[1])
async let thirdPhoto = downloadPhoto(named: photoNames[2])
let photos = await [firstPhoto, secondPhoto, thirdPhoto]
show(photos)
async-let
indicates that you don’t need the result until later in your code. This creates work that can be carried out in parallel.async-let
allow other code to run while they’re suspended.await
indicates that execution will pause, if needed, until all asynchronous functions have returned.
task
A task
is a unit of work that can be run asynchronously as part of your program. A task itself does only one thing at a time, but when you create multiple tasks, Swift can schedule them to run simultaneously.
The async-let
syntax described in the previous section implicitly creates a child task.
structured
Tasks are arranged in a hierarchy. Each task in a given task group has the same parent task, and each task can have child tasks.
- In a parent task, you can’t forget to wait for its child tasks to complete.
- When setting a higher priority on a child task, the parent task’s priority is automatically escalated.
- When a parent task is canceled, each of its child tasks is also automatically canceled.
- Task-local values propagate to child tasks efficiently and automatically.
swift
let photos = await withTaskGroup(of: Optional<Data>.self) { group in
let photoNames = await listPhotos(inGallery: "Summer Vacation")
for name in photoNames {
let added = group.addTaskUnlessCancelled {
guard !Task.isCancelled else { return nil }
return await downloadPhoto(named: name)
}
guard added else { break }
}
var results: [Data] = []
for await photo in group {
if let photo { results.append(photo) }
}
return results
}
unstructured
swift
let newPhoto = // ... some photo data ...
let handle = Task {
return await add(newPhoto, toGalleryNamed: "Spring Adventures")
}
let result = await handle.value
actor
Actors allow only one task to access their mutable state at a time.
You introduce an actor with the actor
keyword, followed by its definition in a pair of braces.
swift
actor TemperatureLogger {
let label: String
var measurements: [Int]
private(set) var max: Int
init(label: String, measurement: Int) {
self.label = label
self.measurements = [measurement]
self.max = measurement
}
}
When you access a property or method of an actor, you use await
to mark the potential suspension point. For example:
swift
let logger = TemperatureLogger(label: "Outdoors", measurement: 25)
print(await logger.max)
// Prints "25"
If code from another task is already interacting with the logger, this code suspends while it waits to access the property.
Code that’s part of the actor doesn’t write await when accessing the actor’s properties.
swift
extension TemperatureLogger {
func update(with measurement: Int) {
measurements.append(measurement)
if measurement > max {
max = measurement
}
}
}
If code outside the actor tries to access those properties directly, like accessing a structure or class’s properties, you’ll get a compile-time error. For example:
swift
print(logger.max) // Error
Swift guarantees that only code running on an actor can access that actor’s local state. This guarantee is known as actor isolation
.
- Code in between possible suspension points runs sequentially, without the possibility of interruption from other concurrent code.
- Code that interacts with an actor’s local state runs only on that actor.
- An actor runs only one piece of code at a time.
Sendable
A sendable
type is a type that can be shared from one concurrency domain to another.
For example, it can be passed as an argument when calling an actor method or be returned as the result of a task.
A class that contains mutable properties and doesn’t serialize access to those properties can produce unpredictable and incorrect results when you pass instances of that class between different tasks.
You mark a type as being sendable by declaring conformance to the Sendable
protocol.
In general, there are three ways for a type to be sendable:
- The type is a value type, and its mutable state is made up of other sendable data.
- The type doesn’t have any mutable state, and its immutable state is made up of other sendable data.
- The type has code that ensures the safety of its mutable state, like a class that’s marked
@MainActor
or a class that serializes access to its properties on a particular thread or queue.
Some types are always sendable, like structures that have only sendable properties and enumerations that have only sendable associated values.
A implicitly sendable type
swift
struct TemperatureReading {
var measurement: Int
}
A explicitly sendable type
swift
struct TemperatureReading: Sendable {
var measurement: Int
}
To explicitly mark a type as not being sendable
swift
struct FileDescriptor {
let rawValue: CInt
}
@available(*, unavailable)
extension FileDescriptor: Sendable { }
In the code above, the FileDescriptor is a structure that meets the criteria to be implicitly sendable.
However, the extension makes its conformance to Sendable unavailable, preventing the type from being sendable.
References
- The Swift Programming Language - Concurrency
- WWDC21 Meet async/await in Swift
- WWDC21 Explore structured concurrency in Swift
- WWDC21 Protect mutable state with Swift actors
- WWDC21 Swift concurrency: Behind the scenes
- WWDC21 Meet AsyncSequence
- WWDC22 Eliminate data races using Swift Concurrency
- WWDC22 Visualize and optimize Swift concurrency
- WWDC22 Efficiency awaits: Background tasks in SwiftUI
- WWDC24 Migrate your app to Swift 6
- Updating an app to use strict concurrency
- Swift 6 Migration Guide
- Swift 并发初步
- Swift 结构化并发
- Swift 6 适配的一些体会以及对现状的小吐槽