Printed on: January 10, 2025
If 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 somewhat bit like the next:
Principal actor-isolated property cannot 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 primary actor inside a closure that is speculated to run just about wherever. In order that could possibly be on the primary actor or it could possibly be some other place.
The next code is an instance of code that we may 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 may be very contrived. You would not really write code like this, however it’s not unlikely that you’d wish to use a predominant 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, is just not tremendous simple as a result of the repair will rely on how a lot management we have now over this sendable closure.
Fixing the error while you personal all of the code
If we fully personal this code, we may really change the perform that takes the closure to develop into an asynchronous perform that may really await entry to the depend property. Here is what that will appear like:
func useCount() {
runClosure {
await print(depend)
}
}
func runClosure(_ closure: @Sendable @escaping () async -> Void) {
Process {
await closure()
}
}
By making the closure asynchronous, we will now await our entry to depend, which is a legitimate method to work together with a predominant actor remoted property from a unique isolation context. Nevertheless, 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, should you personal the codebase, you might @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 some other predominant actor remoted state inside the closure that is handed to runClosure
. At this level depend
is predominant actor remoted resulting from its containing sort being predominant actor remoted, runClosure
itself is predominant actor remoted resulting from its unclosing sort being predominant actor remoted, and the closure itself is now additionally predominant actor remoted as a result of we added an specific annotation to it.
In fact this solely works while you need this closure to run on the primary actor and should you absolutely management the code.
If you do not need the closure to run on the primary actor and also you personal the code, the earlier answer 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 must make this venture compile.
Fixing the error with out modifying the receiving perform
After we’re solely allowed to make adjustments to the code that we personal, which on this case can be the useCount
perform, issues get somewhat bit trickier. One strategy could possibly be to kick off an asynchronous job inside the closure and it will work with depend
there. Here is what this appears like:
func useCount() {
runClosure {
Process {
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 may really do is seize depend
within the closure’s seize record 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 attempting to learn it from inside our sendable closure. For read-only entry, this can be a stable answer that may work properly for you. Nevertheless, we may complicate this somewhat bit and attempt to mutate depend which poses a brand new drawback since we’re solely allowed to mutate depend from inside the primary actor:
func useCount() {
runClosure {
// Principal actor-isolated property 'depend' cannot be mutated from a Sendable closure
depend += 1
}
}
We’re now operating into the next error:
Principal actor-isolated property ‘depend’ cannot 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 unravel this particular error.
Out of the three options proposed in that put up, the one one that will work for us is the next:
Use MainActor.run or an unstructured job to mutate the worth from the primary 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 might need to await.
Much like how you’ll use DispatchQueue.predominant.async
in previous code, in your new code you should use Process { @MainActor in }
to run work on the primary actor:
func useCount() {
runClosure {
Process { @MainActor in
depend += 1
}
}
}
The truth that we’re compelled to introduce a synchronicity right here is just not 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 all the time must have its state and capabilities awaited while you entry it from exterior of the actor. The identical applies while you isolate one thing to the primary actor as a result of while you isolate one thing to the primary actor it basically turns into a part of the primary actor’s isolation context, and we have now to asynchronously work together with predominant 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 predominant actor remoted state in a sendable closure. In the event you’re operating into eventualities the place not one of the options proven listed below are related I would love should you may share them with me.