2022 EssentialsSwift
WWDC22 · 29 min · Essentials / Swift
Eliminate data races using Swift Concurrency
Join us as we explore one of the core concepts in Swift concurrency: isolation of tasks and actors. We’ll take you through Swift’s approach to eliminating data races and its effect on app architecture. We’ll also discuss the importance of atomicity in your code, share the nuances of Sendable checking to maintain isolation, and revisit assumptions about ordering work in a concurrent system.
Watch at developer.apple.com ↗Code shown on screen · 30 snippets
Tasks
Task.detached {
let fish = await catchFish()
let dinner = await cook(fish)
await eat(dinner)
} What is the pineapple?
enum Ripeness {
case hard
case perfect
case mushy(daysPast: Int)
}
struct Pineapple {
var weight: Double
var ripeness: Ripeness
mutating func ripen() async { … }
mutating func slice() -> Int { … }
} Adding chickens
final class Chicken {
let name: String
var currentHunger: HungerLevel
func feed() { … }
func play() { … }
func produce() -> Egg { … }
} Sendable protocol
protocol Sendable { } Use conformance to specify which types are Sendable
struct Pineapple: Sendable { … } //conforms to Sendable because its a value type
class Chicken: Sendable { } // cannot conform to Sendable because its an unsynchronized reference type. Check Sendable across task boundaries
// will get an error because Chicken is not Sendable
let petAdoption = Task {
let chickens = await hatchNewFlock()
return chickens.randomElement()!
}
let pet = await petAdoption.value The Sendable constraint is from the Task struct
struct Task<Success: Sendable, Failure: Error> {
var value: Success {
get async throws { … }
}
} Sendable checking for enums and structs
enum Ripeness: Sendable {
case hard
case perfect
case mushy(daysPast: Int)
}
struct Pineapple: Sendable {
var weight: Double
var ripeness: Ripeness
} Sendable checking for enums and structs with collections
//contains an array of Sendable types, therefore is Sendable
struct Crate: Sendable {
var pineapples: [Pineapple]
} Sendable checking for enums and structs with non-Sendable collections
//stored property 'flock' of 'Sendable'-conforming struct 'Coop' has non-sendable type '[Chicken]'
struct Coop: Sendable {
var flock: [Chicken]
} Sendable checking in classes
//Can be Sendable if a final class has immutable storage
final class Chicken: Sendable {
let name: String
var currentHunger: HungerLevel //'currentHunger' is mutable, therefore Chicken cannot be Sendable
} Reference types that do their own internal synchronization
//@unchecked can be used, but be careful!
class ConcurrentCache<Key: Hashable & Sendable, Value: Sendable>: @unchecked Sendable {
var lock: NSLock
var storage: [Key: Value]
} Sendable checking during task creation
let lily = Chicken(name: "Lily")
Task.detached { in
lily.feed()
} Sendable function types
struct Task<Success: Sendable, Failure: Error> {
static func detached(
priority: TaskPriority? = nil,
operation: @escaping () async throws -> Success
) -> Task<Success, Failure>
} Actors
actor Island {
var flock: [Chicken]
var food: [Pineapple]
func advanceTime()
} Only one boat can visit an island at a time
func nextRound(islands: [Island]) async {
for island in islands {
await island.advanceTime()
}
} Non-Sendable data cannot be shared between a task and actor
//Both examples cannot be shared
await myIsland.addToFlock(myChicken)
myChicken = await myIsland.adoptPet() What code is actor-isolated?
actor Island {
var flock: [Chicken]
var food: [Pineapple]
func advanceTime() {
let totalSlices = food.indices.reduce(0) { (total, nextIndex) in
total + food[nextIndex].slice()
}
Task {
flock.map(Chicken.produce)
}
Task.detached {
let ripePineapples = await food.filter { $0.ripeness == .perfect }
print("There are \(ripePineapples.count) ripe pineapples on the island")
}
}
} Nonisolated code
extension Island {
nonisolated func meetTheFlock() async {
let flockNames = await flock.map { $0.name }
print("Meet our fabulous flock: \(flockNames)")
}
} Non-isolated synchronous code
func greet(_ friend: Chicken) { }
extension Island {
func greetOne() {
if let friend = flock.randomElement() {
greet(friend)
}
}
} Non-isolated asynchronous code
func greet(_ friend: Chicken) { }
func greetAny(flock: [Chicken]) async {
if let friend = flock.randomElement() {
greet(friend)
}
} Isolating functions to the main actor
func updateView() { … }
Task { in
// …
view.selectedChicken = lily
}
nonisolated func computeAndUpdate() async {
computeNewValues()
await updateView()
} @MainActor types
class ChickenValley: Sendable {
var flock: [Chicken]
var food: [Pineapple]
func advanceTime() {
for chicken in flock {
chicken.eat(from: &food)
}
}
} Non-transactional code
func deposit(pineapples: [Pineapple], onto island: Island) async {
var food = await island.food
food += pineapples
await island.food = food
} Pirates!
await island.food.takeAll() Modify `deposit` function to be synchronous
extension Island {
func deposit(pineapples: [Pineapple]) {
var food = self.food
food += pineapples
self.food = food
}
} AsyncStreams deliver elements in order
for await event in eventStream {
await process(event)
} Minimal strict concurrency checking
import FarmAnimals
struct Coop: Sendable {
var flock: [Chicken]
} Targeted strict concurrency checking
import FarmAnimals
func visit(coop: Coop) async {
guard let favorite = coop.flock.randomElement() else {
return
}
Task {
favorite.play()
}
} Complete strict concurrency checking
import FarmAnimals
func doWork(_ body: @escaping () -> Void) {
DispatchQueue.global().async {
body()
}
}
func visit(friend: Chicken) {
doWork {
friend.play()
}
} Resources
Related sessions
-
24 min -
25 min -
38 min -
25 min -
23 min -
14 min -
34 min -
29 min -
28 min -
1h 1m -
39 min