Deepak Sharma
Deepak Sharma

Reputation: 6581

Swift isolated(any Actor)? keyword usage

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

Answers (2)

Rob
Rob

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 but stopUpdates() 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

Sweeper
Sweeper

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

Related Questions