Printed on: January 10, 2025
Whenever 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:
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 inside a closure that is presupposed to run just about anyplace. In order that might be on the primary actor or it might be someplace 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 could be very contrived. You would not really write code like this, however it isn’t unlikely that you’d need to use a principal actor remoted property in a closure that’s sendable inside of a bigger system. So, what can we do to repair this downside?
The reply, sadly, is just not tremendous easy 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 utterly personal this code, we might really change the perform that takes the closure to change into an asynchronous perform that may really await entry to the depend property. Here is what that may seem like:
func useCount() {
runClosure {
await print(depend)
}
}
func runClosure(_ closure: @Sendable @escaping () async -> Void) {
Activity {
await closure()
}
}
By making the closure asynchronous, we are able to now await our entry to depend, which is a legitimate method to work together with a principal actor remoted property from a distinct isolation context. Nonetheless, this may not be the answer that you just’re searching for. You may not need this closure to be async, for instance. In that case, in case you personal the codebase, you can @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 principal actor remoted state inside the closure that is handed to runClosure. At this level depend is principal actor remoted as a consequence of its containing sort being principal actor remoted, runClosure itself is principal actor remoted as a consequence of its unclosing sort being principal actor remoted, and the closure itself is now additionally principal 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 primary actor and in case you 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 to be like in case 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 undertaking 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 just a little bit trickier. One method might be to kick off an asynchronous activity inside the closure and it will work with depend there. Here is what this seems to be like:
func useCount() {
runClosure {
Activity {
await print(depend)
}
}
}
Whereas this works, it does introduce concurrency right into a system the place you may not need to have any concurrency. On this case, we’re solely studying the depend property, so what we might really do is seize depend within the closure’s seize record in order that we entry the captured worth relatively 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, relatively than making an attempt to learn it from inside our sendable closure. For read-only entry, this can be a strong answer that may work effectively for you. Nonetheless, we might complicate this just a little bit and attempt to mutate depend which poses a brand new downside since we’re solely allowed to mutate depend from inside the primary actor:
func useCount() {
runClosure {
// Major actor-isolated property 'depend' can't be mutated from a Sendable closure
depend += 1
}
}
We’re now working into the next error:
Major actor-isolated property ‘depend’ can’t be mutated from a Sendable closure
I’ve devoted submit about working 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 submit, the one one that may 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 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.principal.async in previous code, in your new code you need to use Activity { @MainActor in } to run work on the primary actor:
func useCount() {
runClosure {
Activity { @MainActor in
depend += 1
}
}
}
The truth that we’re compelled to introduce a synchronicity right here is just not 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 capabilities awaited whenever you entry it from outdoors 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’ve to asynchronously work together with principal actor remoted state from outdoors of the primary actor.
I hope this submit gave you some insights into how one can repair errors associated to capturing principal 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 in case you might share them with me.
