Task isolation
Task isolation ensures that data across tasks is not shared in a manner that can introduce data races
Task
A task performs a specific job sequentially from start to finish
Tasks are asynchronous, and their work can be suspended any number of times at
awaitoperationsA task is self-contained - has its own resources and can operate by itself, independently of any other task
Communications between tasks
done by passing objects across tasks (a task passes an object by returning a value at the end of its body)
no problem if the shared/transferred data is value type
can (potentially) cause data races if the data is reference type
Sendable
Swift helps us telling us when it’s safe to share our data across tasks via the Sendable protocol:
Sendabledescibes types that can cross an isolation domain (like tasks), without making data racesdata races checks happen while building by the Swift compiler
For tasks, the actual
Sendableconstraint comes from their definition:Tasks return type must conform toSendableYou should use
Sendableconstraints where you have generic parameters whose values will be passed across different isolation domainsSendableconformances can be inferred by the Swift compiler for non-public types (but you can addSendableconformance explicitly)Classes (reference types) can conform to
Sendableonly under very narrow circumstancese.g., when a
finalclass only has immutable storage
for reference types that do their own internal synchronization (e.g., via locks), you can use
@unchecked Sendable
class ConcurrentCache<Key: Hashable & Sendable, Value: Sendable>: @unchecked Sendable {
var lock: NSLock
var storage: [Key: Value]
// ...
}Actor isolation
Actors provide a way to isolate state that can be accessed by different tasks, in a coordinated manner that eliminates data racesan
Actoris self-contained - has its own resources and can operate by itself, independently of any other Actorin order to execute code (or read values) defined in an actor, you need to use a task
only one task can execute on an actor at a time
entering into an actor is a potential suspension point, as there might be already another task running on it, and even other tasks waiting to enter into that specific actor
the same rules for communications across tasks are true for communication between tasks and actors and between actors
said in other words, actors rely on
Sendable, too
Actors are reference types, but isolate all of their properties and code to prevent concurrent accessall
Actortypes are implicitlySendable
all Actor instance definitions (properties and functions) are isolated
a child/sub task inherits all attributes of the parent task, therefore, if a task is generated directly by an actor function, said task inherits actor isolation from its context (thus will be able to access the actor properties and call other functions without
awaiting on themthe same is not true for detached tasks, which do not inherit traits from that task’s originating context
Actor properties and functions marked as
nonisolatedare considered to be outside the actor
@MainActor
represents main thread
use it when you need to update UI in your app
use the
@MainActorattribute to indicate that the code must run on the main actor:
@MainActor func updateView() { … }
Task { @MainActor in
// update UI here
}the Swift compiler will guarantee that main-actor-isolated code will only be executed on the main thread
@MainActorcan also be applied to types, in which case the instances of those types will be isolated to the main actorproperties will be only accessible while on the main actor
methods are isolated to the main actor, unless marked
nonisolated
@MainActor
class ChickenValley: Sendable {
var flock: [Chicken]
var food: [Pineapple]
func advanceTime() {
for chicken in flock {
chicken.eat(from: &food)
}
}
}Atomicity
state can change across
awaitscallsif you’re not careful, you can end up with a high-level data race where the program is in an unexpected state, even though the data is not corrupted
when writing your actor, think in terms of synchronous, transactional operations that can be interleaved in any way
keep async actor operations simple
Ordering
Swift Concurrency provides tools for ordering operations
actors do no guaranteed FIFO processing
actors execute the highest-priority work first
eliminates priority inversions
diffent than serial Dispatch queues, which execute in a strictly FIFO order
Tools for ordering:
TasksAsyncStreams deliver elements in order:
for await event in eventStream {
await process(event)
}