Revealed on: January 10, 2025
While you activate strict concurrency checking otherwise you begin utilizing the Swift 6 language mode, there can be conditions the place you run into an error that appears just a little bit like the next:
Foremost actor-isolated property can’t be referenced from a Sendable closure
What this error tells us is that we’re attempting to make use of one thing that we’re solely supposed to make use of on or from the principle actor within a closure that is presupposed to run just about anyplace. In order that could possibly be on the principle actor or it could possibly be some place else.
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()
}
}
After all, this instance may be very contrived. You would not truly write code like this, however it isn’t unlikely that you’d wish to use a primary 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, isn’t tremendous simple as a result of the repair will depend upon how a lot management we’ve over this sendable closure.
Fixing the error whenever you personal all of the code
If we fully personal this code, we might truly change the perform that takes the closure to turn into an asynchronous perform that may truly await entry to the depend property. Here is what that may appear like:
func useCount() {
runClosure {
await print(depend)
}
}
func runClosure(_ closure: @Sendable @escaping () async -> Void) {
Job {
await closure()
}
}
By making the closure asynchronous, we are able to now await our entry to depend, which is a legitimate option to work together with a primary actor remoted property from a special isolation context. Nevertheless, this won’t be the answer that you simply’re on the lookout for. You won’t need this closure to be async, for instance. In that case, should you personal the codebase, you could possibly @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 principle actor, we’re free to run it and entry some other primary actor remoted state within the closure that is handed to runClosure
. At this level depend
is primary actor remoted as a consequence of its containing sort being primary actor remoted, runClosure
itself is primary actor remoted as a consequence of its unclosing sort being primary actor remoted, and the closure itself is now additionally primary actor remoted as a result of we added an express annotation to it.
After all this solely works whenever you need this closure to run on the principle actor and should you absolutely management the code.
If you don’t need the closure to run on the principle actor and also you personal the code, the earlier resolution would give you the results you want.
Now let’s check out what this appears like should you do not personal the perform that takes this sendable closure. In different phrases, we’re not allowed to switch the runClosure
perform, however we nonetheless have to make this mission compile.
Fixing the error with out modifying the receiving perform
Once we’re solely allowed to make modifications to the code that we personal, which on this case could be the useCount
perform, issues get just a little bit trickier. One method could possibly be to kick off an asynchronous job within the closure and it will work with depend
there. Here is what this appears like:
func useCount() {
runClosure {
Job {
await print(depend)
}
}
}
Whereas this works, it does introduce concurrency right into a system the place you won’t wish 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 record in order that we entry the captured worth fairly than the principle 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, fairly than attempting to learn it from within our sendable closure. For read-only entry, it is a strong resolution that can work properly for you. Nevertheless, we might complicate this just a little bit and attempt to mutate depend which poses a brand new drawback since we’re solely allowed to mutate depend from within the principle actor:
func useCount() {
runClosure {
// Foremost actor-isolated property 'depend' can't be mutated from a Sendable closure
depend += 1
}
}
We’re now working into the next error:
Foremost actor-isolated property ‘depend’ can’t be mutated from a Sendable closure
I’ve devoted submit about working work on the principle actor the place I discover a number of methods to resolve this particular error.
Out of the three options proposed in that submit, the one one that may work for us is the next:
Use MainActor.run or an unstructured job to mutate the worth from the principle actor
Since our closure is not async already, we won’t use MainActor.run
as a result of that is an async perform that we would need to await.
Just like how you’ll use DispatchQueue.primary.async
in outdated code, in your new code you should use Job { @MainActor in }
to run work on the principle actor:
func useCount() {
runClosure {
Job { @MainActor in
depend += 1
}
}
}
The truth that we’re pressured to introduce a synchronicity right here isn’t one thing that I like quite a bit. Nevertheless, 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’ll be able to 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 outdoors of the actor. The identical applies whenever you isolate one thing to the principle actor as a result of whenever you isolate one thing to the principle actor it primarily turns into a part of the principle actor’s isolation context, and we’ve to asynchronously work together with primary actor remoted state from outdoors of the principle actor.
I hope this submit gave you some insights into how one can repair errors associated to capturing primary actor remoted state in a sendable closure. When you’re working into eventualities the place not one of the options proven listed below are related I would love should you might share them with me.