Passing data around across isolation domains means you need the types to conform to Sendable.
You need to pass some non-Sendable arguments into a function in a different isolation domain.
func myAsyncFunction(_ nonSendable: NonSendable) async {
}
let nonSendable = NonSendable()
// this produces a warning
await myAsyncFunction(nonSendable)
Assumption: the definition is under your control.
func myAsyncFunction(_ nonSendable: @Sendable () -> NonSendable) async {
}
await myAsyncFunction({ NonSendable() })
You need to isolate things differently depending on usage.
func takesClosure(_ block: () -> Void) {
}
takesClosure {
accessMainActorOnlyThing()
}
We've seen this one before when working with protocols.
func takesClosure(_ block: () -> Void) {
}
takesClosure {
MainActor.assumeIsolated {
accessMainActorOnlyThing()
}
}
If you find yourself doing this a lot or you are just not into the nesting, you can make wrapper.
func takesMainActorClosure(_ block: @MainActor () -> Void) {
takesClosure {
MainActor.assumeIsolated {
block()
}
}
}
takesMainActorClosure {
accessMainActorOnlyThing()
}
func takesClosure(isolatedTo actor: isolated any Actor, block: () -> Void) {
}
There are situations where you need to manage a whole bunch of global state all together. In a case like that, a custom global actor can be useful.
@globalActor
public actor CustomGlobalActor {
public static let shared = CustomGlobalActor()
// I wanted to do something like MainActor.assumeIsolated, but it turns out every global actor has to implement that manually. This is because
// it isn't possible to express a global actor assumeIsolated generically. So I just copied the sigature from MainActor.
public static func assumeIsolated<T>(_ operation: @CustomGlobalActor () throws -> T, file: StaticString = #fileID, line: UInt = #line) rethrows -> T {
// verify that we really are in the right isolation domain
Self.shared.assertIsolated()
// use some tricky casting to remove the global actor so we can execute the clsoure
return try withoutActuallyEscaping(operation) { fn in
try unsafeBitCast(fn, to: (() throws -> T).self)()
}
}
}
Non-Sendable
types can participate in concurrency. But, because self
cannot cross isolation domains, it's easy to accidentally make the type unusable from an isolated context.
class NonSendableType {
func asyncFunction() async {
}
}
@MainActor
class MyMainActorClass {
// this value is isolated to the MainActor
let value = NonSendableType()
func useType() async {
// here value is being transferred from the MainActor to a non-isolated
// context. That's not allowed.
// ERROR: Sending 'self.value' risks causing data races
await value.asyncFunction()
}
}
class NonSendableType {
func asyncFunction(isolation: isolated (any Actor)? = #isolation) async {
}
}
@MainActor
class MyMainActorClass {
// this value is isolated to the MainActor
let value = NonSendableType()
func useType() async {
// the compiler now knows that isolation does not change for
// this call, which makes it possible.
await value.asyncFunction()
}
}