Dunfey · Hotel WWDC as data, est. 1983
Front desk everything
Years
Topics

2021 Swift

WWDC21 · 29 min · Swift

Protect mutable state with Swift actors

Data races occur when two separate threads concurrently access the same mutable state. They are trivial to construct, but are notoriously hard to debug. Discover how you can stop these data races in their tracks with Swift actors, which help synchronize access to data in your code. Discover how actors work and how to share values between them. Learn about how actor isolation affects protocol conformances. And finally, meet the main actor, a new way of ensuring that your code always runs on the main thread when needed. To get the most out of this session, we recommend first watching “Meet async/await in Swift.”

Watch at developer.apple.com ↗

Transcript all transcripts

Code shown on screen · 19 snippets

Data races make concurrency hard swift · at 0:42 ↗
class Counter {
    var value = 0

    func increment() -> Int {
        value = value + 1
        return value
    }
}

let counter = Counter()

Task.detached {
    print(counter.increment()) // data race
}

Task.detached {
    print(counter.increment()) // data race
}
Value semantics help eliminate data races swift · at 2:20 ↗
var array1 = [1, 2]
var array2 = array1

array1.append(3)
array2.append(4)

print(array1)        // [1, 2, 3]
print(array2)        // [1, 2, 4]
Sometimes shared mutable state is required swift · at 2:59 ↗
struct Counter {
    var value = 0

    mutating func increment() -> Int {
        value = value + 1
        return value
    }
}

let counter = Counter()

Task.detached {
    var counter = counter
    print(counter.increment()) // always prints 1
}

Task.detached {
    var counter = counter
    print(counter.increment()) // always prints 1
}
Actor isolation prevents unsynchronized access swift · at 5:23 ↗
actor Counter {
    var value = 0

    func increment() -> Int {
        value = value + 1
        return value
    }
}

let counter = Counter()

Task.detached {
    print(await counter.increment())
}

Task.detached {
    print(await counter.increment())
}
Synchronous interation within an actor swift · at 7:51 ↗
extension Counter {
    func resetSlowly(to newValue: Int) {
        value = 0
        for _ in 0..<newValue {
            increment()
        }
        assert(value == newValue)
    }
}
Check your assumptions after an await: The sad cat swift · at 9:02 ↗
actor ImageDownloader {
    private var cache: [URL: Image] = [:]

    func image(from url: URL) async throws -> Image? {
        if let cached = cache[url] {
            return cached
        }

        let image = try await downloadImage(from: url)

        // Potential bug: `cache` may have changed.
        cache[url] = image
        return image
    }
}
Check your assumptions after an await: One solution swift · at 11:50 ↗
actor ImageDownloader {
    private var cache: [URL: Image] = [:]

    func image(from url: URL) async throws -> Image? {
        if let cached = cache[url] {
            return cached
        }

        let image = try await downloadImage(from: url)

        // Replace the image only if it is still missing from the cache.
        cache[url] = cache[url, default: image]
        return cache[url]
    }
}
Check your assumptions after an await: A better solution swift · at 11:59 ↗
actor ImageDownloader {

    private enum CacheEntry {
        case inProgress(Task<Image, Error>)
        case ready(Image)
    }

    private var cache: [URL: CacheEntry] = [:]

    func image(from url: URL) async throws -> Image? {
        if let cached = cache[url] {
            switch cached {
            case .ready(let image):
                return image
            case .inProgress(let task):
                return try await task.value
            }
        }

        let task = Task {
            try await downloadImage(from: url)
        }

        cache[url] = .inProgress(task)

        do {
            let image = try await task.value
            cache[url] = .ready(image)
            return image
        } catch {
            cache[url] = nil
            throw error
        }
    }
}
Protocol conformance: Static declarations are outside the actor swift · at 13:30 ↗
actor LibraryAccount {
    let idNumber: Int
    var booksOnLoan: [Book] = []
}

extension LibraryAccount: Equatable {
    static func ==(lhs: LibraryAccount, rhs: LibraryAccount) -> Bool {
        lhs.idNumber == rhs.idNumber
    }
}
Protocol conformance: Non-isolated declarations are outside the actor swift · at 14:15 ↗
actor LibraryAccount {
    let idNumber: Int
    var booksOnLoan: [Book] = []
}

extension LibraryAccount: Hashable {
    nonisolated func hash(into hasher: inout Hasher) {
        hasher.combine(idNumber)
    }
}
Closures can be isolated to the actor swift · at 15:32 ↗
extension LibraryAccount {
    func readSome(_ book: Book) -> Int { ... }
    
    func read() -> Int {
        booksOnLoan.reduce(0) { book in
            readSome(book)
        }
    }
}
Closures executed in a detached task are not isolated to the actor swift · at 16:29 ↗
extension LibraryAccount {
    func readSome(_ book: Book) -> Int { ... }
    func read() -> Int { ... }
    
    func readLater() {
        Task.detached {
            await read()
        }
    }
}
Passing data into and out of actors: structs swift · at 17:15 ↗
actor LibraryAccount {
    let idNumber: Int
    var booksOnLoan: [Book] = []
    func selectRandomBook() -> Book? { ... }
}

struct Book {
    var title: String
    var authors: [Author]
}

func visit(_ account: LibraryAccount) async {
    guard var book = await account.selectRandomBook() else {
        return
    }
    book.title = "\(book.title)!!!" // OK: modifying a local copy
}
Passing data into and out of actors: classes swift · at 17:39 ↗
actor LibraryAccount {
    let idNumber: Int
    var booksOnLoan: [Book] = []
    func selectRandomBook() -> Book? { ... }
}

class Book {
    var title: String
    var authors: [Author]
}

func visit(_ account: LibraryAccount) async {
    guard var book = await account.selectRandomBook() else {
        return
    }
    book.title = "\(book.title)!!!" // Not OK: potential data race
}
Check Sendable by adding a conformance swift · at 20:08 ↗
struct Book: Sendable {
    var title: String
    var authors: [Author]
}
Propagate Sendable by adding a conditional conformance swift · at 20:43 ↗
struct Pair<T, U> {
    var first: T
    var second: U
}

extension Pair: Sendable where T: Sendable, U: Sendable {
}
Interacting with the main thread: Using a DispatchQueue swift · at 24:19 ↗
func checkedOut(_ booksOnLoan: [Book]) {
    booksView.checkedOutBooks = booksOnLoan
}

// Dispatching to the main queue is your responsibility.
DispatchQueue.main.async {
    checkedOut(booksOnLoan)
}
Interacting with the main thread: The main actor swift · at 25:01 ↗
@MainActor func checkedOut(_ booksOnLoan: [Book]) {
    booksView.checkedOutBooks = booksOnLoan
}

// Swift ensures that this code is always run on the main thread.
await checkedOut(booksOnLoan)
Main actor types swift · at 26:21 ↗
@MainActor class MyViewController: UIViewController {
    func onPress(...) { ... } // implicitly @MainActor

    nonisolated func fetchLatestAndDisplay() async { ... } 
}

Resources