Reputation: 6581
The sample code below demonstrates two implementations of class Pipeline
(one is Actor
and other is a class). It is being compiled under Swift 6 settings.
actor Pipeline {
let updater = Updater()
func startUpdates() {
updater.startUpdates()
}
func stopUpdates() async {
await updater.stopUpdates()
await updater.stopUpdates2() //<--Build fails with error
//Sending 'self'-isolated 'self.updater' to nonisolated instance method 'stopUpdates2()' risks causing data races between nonisolated and 'self'-isolated uses
}
}
final class AnotherPipeline {
let updater = Updater()
func startUpdates() {
updater.startUpdates()
}
func stopUpdates() async {
await updater.stopUpdates()
await updater.stopUpdates2()
}
}
final class Updater {
func startUpdates() {
print("started updates")
}
func stopUpdates2() async {
print("stopped updates 2")
}
func stopUpdates(_ isolation: isolated (any Actor)? = #isolation) async {
print("stopped updates")
}
}
As we can see, the build fails when calling stopUpdates2()
from the Actor but stopUpdates()
causes no problems. Is the isolated(any)
keyword just suppressing the errors at compile time in stopUpdates()
and fail at runtime when called on the wrong context, or there is more to it?
Sending 'self'-isolated 'self.updater' to nonisolated instance method 'stopUpdates2()' risks causing data races between nonisolated and 'self'-isolated uses
Finally, what exactly is this syntax, I can't find any documentation on the same.
func stopUpdates(_ isolation: isolated (any Actor)? = #isolation)
Upvotes: 2
Views: 207
Reputation: 438112
You asked:
what exactly is this syntax, I can't find any documentation on the same.
func stopUpdates(_ isolation: isolated (any Actor)? = #isolation)
SE-0420 introduces us to the #isolation
parameter, implemented in Swift 6. If invoked from an actor-isolated context, it will be isolated to that actor. If invoked from a non-isolated context, then it will not be isolated (and as a non-isolated async
function, it will run on the generic executor as outlined in SE-0338).
The #isolation
default argument discussion of SE-0420 outlines the difference between invoking this from an isolated context (your actor
example) and when invoking it from a non-isolated context (your class
example).
Earlier you asked:
As we can see, the build fails when calling
stopUpdates2()
from the Actor butstopUpdates()
causes no problems.
Yes, in your actor
example, stopUpdates
is actor-isolated (by virtue of being isolating to the caller’s #isolation
), but stopUpdates2
is not. The error is protecting you against the latter, namely the potential race between stopUpdates2
work on the generic executor and other subsequent access from the self-isolated context (which is free to do other things while it awaits).
Upvotes: 1
Reputation: 273540
The isolated
parameter modifier is documented in SE-0313. By adding an isolated
parameter to an otherwise nonisolated function, you make that function isolated to the actor instance that is passed as the isolated
parameter.
To use an example from the SE proposal,
actor BankAccount {
let accountNumber: Int
var balance: Double
init(accountNumber: Int, initialDeposit: Double) {
self.accountNumber = accountNumber
self.balance = initialDeposit
}
}
func deposit(amount: Double, to account: isolated BankAccount) {
assert(amount >= 0)
account.balance = account.balance + amount
}
deposit
is isolated to account
, so it can synchronously access account.balance
.
isolated (any Actor)?
is just like the above example, but any actor instance can be passed to the parameter.
isolation: isolated (any Actor)? = #isolation
is a parameter that has a default value of #isolation
. #isolation
is a macro that expands to whatever actor the current context is isolated to (or nil
if the current context is nonisolated).
Macros used as default values of parameters expand on the caller's side. That means, when you call stopUpdates()
, you are actually calling stopUpdates(#isolation)
. #isolation
expands to whatever the current actor is. In other words, you are saying that stopUpdates
should be isolated to the current actor.
Without the isolation
parameter, stopUpdates2
is always nonisolated. A nonisolated async method is always run on the cooperative thread pool, not isolated to any actor. By doing await updater.stopUpdates2()
, you are sending the non-sendable updator
from a context isolated to self
(the Pipeline
actor), to a nonisolated context.
stopUpdates
allows itself to be run in the same isolation context as before, so you are not sending the non-sendable updator
to anywhere.
Upvotes: 2