Revealed on: January 10, 2025
If you activate strict concurrency checking otherwise you begin utilizing the Swift 6 language mode, there shall be conditions the place you run into an error that appears a bit of bit like the next:
Major actor-isolated property can’t be referenced from a Sendable closure
What this error tells us is that we’re making an attempt to make use of one thing that we’re solely supposed to make use of on or from the primary actor within a closure that is presupposed to run just about wherever. In order that may very well be on the primary actor or it may very well be elsewhere.
The next code is an instance of code that we might have that outcomes on this error:
@MainActor
class ErrorExample {
var depend = 0
func useCount() {
runClosure {
print(depend)
}
}
func runClosure(_ closure: @Sendable () -> Void) {
closure()
}
}
In fact, this instance could be very contrived. You would not truly write code like this, however it isn’t unlikely that you’d need to use a fundamental actor remoted property in a closure that’s sendable inside of a bigger system. So, what can we do to repair this drawback?
The reply, sadly, will not be tremendous easy as a result of the repair will rely on how a lot management we now have over this sendable closure.
Fixing the error whenever you personal all of the code
If we utterly personal this code, we might truly change the operate that takes the closure to grow to be an asynchronous operate that may truly await entry to the depend property. Here is what that might seem like:
func useCount() {
runClosure {
await print(depend)
}
}
func runClosure(_ closure: @Sendable @escaping () async -> Void) {
Job {
await closure()
}
}
By making the closure asynchronous, we will now await our entry to depend, which is a sound method to work together with a fundamental actor remoted property from a unique isolation context. Nonetheless, this won’t be the answer that you just’re on the lookout for. You won’t need this closure to be async, for instance. In that case, if you happen to personal the codebase, you may @MainActor annotate the closure. Here is what that appears like:
@MainActor
class ErrorExample {
var depend = 0
func useCount() {
runClosure {
print(depend)
}
}
func runClosure(_ closure: @Sendable @MainActor () -> Void) {
closure()
}
}
As a result of the closure is now each @Sendable and remoted to the primary actor, we’re free to run it and entry every other fundamental actor remoted state within the closure that is handed to runClosure. At this level depend is fundamental actor remoted resulting from its containing sort being fundamental actor remoted, runClosure itself is fundamental actor remoted resulting from its unclosing sort being fundamental actor remoted, and the closure itself is now additionally fundamental actor remoted as a result of we added an specific annotation to it.
In fact this solely works whenever you need this closure to run on the primary actor and if you happen to totally management the code.
If you don’t need the closure to run on the primary actor and also you personal the code, the earlier answer would be just right for you.
Now let’s check out what this seems like if you happen to do not personal the operate that takes this sendable closure. In different phrases, we’re not allowed to switch the runClosure operate, however we nonetheless have to make this challenge compile.
Fixing the error with out modifying the receiving operate
Once we’re solely allowed to make adjustments to the code that we personal, which on this case can be the useCount operate, issues get a bit of bit trickier. One method may very well be to kick off an asynchronous activity within the closure and it will work with depend there. Here is what this seems like:
func useCount() {
runClosure {
Job {
await print(depend)
}
}
}
Whereas this works, it does introduce concurrency right into a system the place you won’t need to have any concurrency. On this case, we’re solely studying the depend property, so what we might truly do is seize depend within the closure’s seize checklist in order that we entry the captured worth moderately than the primary actor remoted worth. Here’s what that appears like.
func useCount() {
runClosure { [count] in
print(depend)
}
}
This works as a result of we’re capturing the worth of depend when the closure is created, moderately than making an attempt to learn it from within our sendable closure. For read-only entry, it is a strong answer that may work nicely for you. Nonetheless, we might complicate this a bit of bit and attempt to mutate depend which poses a brand new drawback since we’re solely allowed to mutate depend from within the primary actor:
func useCount() {
runClosure {
// Major actor-isolated property 'depend' can't be mutated from a Sendable closure
depend += 1
}
}
We’re now operating into the next error:
Major actor-isolated property ‘depend’ can’t be mutated from a Sendable closure
I’ve devoted put up about operating work on the primary actor the place I discover a number of methods to resolve this particular error.
Out of the three options proposed in that put up, the one one that might work for us is the next:
Use MainActor.run or an unstructured activity to mutate the worth from the primary actor
Since our closure is not async already, we will not use MainActor.run as a result of that is an async operate that we might should await.
Much like how you’d use DispatchQueue.fundamental.async in outdated code, in your new code you need to use Job { @MainActor in } to run work on the primary actor:
func useCount() {
runClosure {
Job { @MainActor in
depend += 1
}
}
}
The truth that we’re compelled to introduce a synchronicity right here will not be one thing that I like loads. Nonetheless, it’s an impact of utilizing actors in Swift concurrency. When you begin introducing actors into your codebase, you additionally introduce a synchronicity as a result of you may synchronously work together with actors from a number of isolation contexts. An actor at all times must have its state and features awaited whenever you entry it from exterior of the actor. The identical applies whenever you isolate one thing to the primary actor as a result of whenever you isolate one thing to the primary actor it basically turns into a part of the primary actor’s isolation context, and we now have to asynchronously work together with fundamental actor remoted state from exterior of the primary actor.
I hope this put up gave you some insights into how one can repair errors associated to capturing fundamental actor remoted state in a sendable closure. In case you’re operating into eventualities the place not one of the options proven listed here are related I might love if you happen to might share them with me.
