-
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.”Ressources
- SE-0316: Global actors
- SE-0313: Improved control over actor isolation
- SE-0306: Actors
- SE-0302: Sendable and @Sendable closures
- The Swift Programming Language: Concurrency
Vidéos connexes
WWDC22
- Eliminate data races using Swift Concurrency
- Meet distributed actors in Swift
- Use Xcode for server-side development
- Visualize and optimize Swift concurrency
WWDC21
-
Rechercher dans cette vidéo…
-
-
0:42 - Data races make concurrency hard
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 } -
2:20 - Value semantics help eliminate data races
var array1 = [1, 2] var array2 = array1 array1.append(3) array2.append(4) print(array1) // [1, 2, 3] print(array2) // [1, 2, 4] -
2:59 - Sometimes shared mutable state is required
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 } -
5:23 - Actor isolation prevents unsynchronized access
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()) } -
7:51 - Synchronous interation within an actor
extension Counter { func resetSlowly(to newValue: Int) { value = 0 for _ in 0..<newValue { increment() } assert(value == newValue) } } -
9:02 - Check your assumptions after an await: The sad cat
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 } } -
11:50 - Check your assumptions after an await: One solution
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] } } -
11:59 - Check your assumptions after an await: A better solution
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 } } } -
13:30 - Protocol conformance: Static declarations are outside the actor
actor LibraryAccount { let idNumber: Int var booksOnLoan: [Book] = [] } extension LibraryAccount: Equatable { static func ==(lhs: LibraryAccount, rhs: LibraryAccount) -> Bool { lhs.idNumber == rhs.idNumber } } -
14:15 - Protocol conformance: Non-isolated declarations are outside the actor
actor LibraryAccount { let idNumber: Int var booksOnLoan: [Book] = [] } extension LibraryAccount: Hashable { nonisolated func hash(into hasher: inout Hasher) { hasher.combine(idNumber) } } -
15:32 - Closures can be isolated to the actor
extension LibraryAccount { func readSome(_ book: Book) -> Int { ... } func read() -> Int { booksOnLoan.reduce(0) { book in readSome(book) } } } -
16:29 - Closures executed in a detached task are not isolated to the actor
extension LibraryAccount { func readSome(_ book: Book) -> Int { ... } func read() -> Int { ... } func readLater() { Task.detached { await read() } } } -
17:15 - Passing data into and out of actors: structs
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 } -
17:39 - Passing data into and out of actors: classes
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 } -
20:08 - Check Sendable by adding a conformance
struct Book: Sendable { var title: String var authors: [Author] } -
20:43 - Propagate Sendable by adding a conditional conformance
struct Pair<T, U> { var first: T var second: U } extension Pair: Sendable where T: Sendable, U: Sendable { } -
24:19 - Interacting with the main thread: Using a DispatchQueue
func checkedOut(_ booksOnLoan: [Book]) { booksView.checkedOutBooks = booksOnLoan } // Dispatching to the main queue is your responsibility. DispatchQueue.main.async { checkedOut(booksOnLoan) } -
25:01 - Interacting with the main thread: The main actor
@MainActor func checkedOut(_ booksOnLoan: [Book]) { booksView.checkedOutBooks = booksOnLoan } // Swift ensures that this code is always run on the main thread. await checkedOut(booksOnLoan) -
26:21 - Main actor types
@MainActor class MyViewController: UIViewController { func onPress(...) { ... } // implicitly @MainActor nonisolated func fetchLatestAndDisplay() async { ... } }
-