Calling escaping closure from actor context: Why don't I need `await` here?

Suppose I am using some, pre-Swift 5.5, API that looks like this:

func whoKnowsWhatQueueThisWillExecuteOn(closure: @escaping () -> ())

Now, let's suppose I set up an actor like this:

actor Foo {
    var bar: String = "bar"
    func setBar(_ bar: String) { self.bar = bar }

    func baz() {
        whoKnowsWhatQueueThisWillExecuteOn {
            // also compiles without the `Task`
            Task {
                self.setBar("baz") // why don't I need 'await' here?
            }
        }
    }
}

This compiles fine, but I don't quite understand why. Shouldn't I need an await here? Since the closure is escaping, there's no way for the compiler to know what context it will be called from, what queue it will be called on, or even when it will be called.

Am I missing something?

3 Likes

Bump. I'm still curious about this...

1 Like

Hey I remember reading an article the other day about actor isolation and the reason why you don't need an await inside of another function declaration. The article was by Antoine van der Lee and its a pretty good read. Always trust that guy.

Actors in Swift: how to use and prevent data races - SwiftLee.

My research on it is that since the function is created in isolation, it doesn't need an await since that function is isolated by default compared to a class function which is marked as nonisolated by default.

Also from my experience and experimenting with actors and closures, I found that unless the function needs to be a closure like anything having to do with for example AVFoundation. Any API that apple hasn't made async await compatible functions compared to their completion handler counterparts are required to have that closure. I have dealt with weird bugs and resuming continuations in async await but I have been mixing Dispatchqueues and Tasks which I have found to be a head ache.

To hopefully answer your question or provide a solution, I would have four functions in there where you have
var bar: String = "bar"
func setBar(_ bar: String) { self.bar = bar }
func baz() {
let barResultThatIsSet = whateverYouWantToCallThisFuncName()
setBar(barResultThatIsSet)
}
//the end result function
func whoKnowsWhatQueueThisWillExecuteOn(closure: @escaping (T) -> (T))
//where you have to use this closure because there aren't any async functions made for it yet
func whateverYouWantToCallThisFuncName() -> T {
let result = whoKnowsWhatQueueThisWillExecuteOn { t in
return t
}
return result
}
//Pretty cool with no awaits there, I haven't tested this out in the compiler but it should look similar to this, as well as you let the system be in charge of context switching> I know with an escaping closure as well as tasks, They will be put off the main thread, so I am sure you know that any UI updates should be on the main thread, just a friendly reminder on that.

Hope this helps out, I am really excited about actors, They have a long way to go but make your code way less messy.

Do you have a link? My curiosity about this has not abated.

Hey does this help at all? Sorry i am just starting to using these forums and reply. Doing research is fun and I always like to help folks out if I can.

import Foundation

actor Foo<T: Sendable> {
    
    ///The init value
    var bar: T
    
    ///public init
    init(bar: T) where T: Sendable {
        self.bar = bar
    }
    
    ///base func to call the callback result and get the value out of it while keeping the init value as
    func baz() where T: Sendable {
        
        let barResultThatIsSet: T = whateverYouWantToCallThisFuncName(with: self.bar)
        setBar(barResultThatIsSet)
        
    }
    
    ///this gaurantees that the bar will be set in isolation
    ///since `setbar(_ bar: T)` is isolated by default
    func setBar(_ bar: T) where T: Sendable { self.bar = bar }
    
    ///Do the magic function, call whatever you want in here
    ///Also T can be any T, you just have to declare what it is,
    ///The only caveat to this is that T must also conform to
    ///sendable so unfortunately NSObject or any big class
    ///object might not be able to conform
    ///Value types are fine though, Enums are offlimits
    func whoKnowsWhatQueueThisWillExecuteOn(with valueToPass: T,
                                            closure: @escaping (T) -> (T)) -> T where T: Sendable {
        ///Do what ever mutating thing inside of here and
        ///the you can assign the closure to which ever
        ///value you want and do your magic
        let current = valueToPass
        ///return the closure once it is complete
        return closure(current)
    }
    
    ///Do whatever other magic you want to do in here and it
    ///should be a pretty good base Network Model controller
    ///or utilities
    func whateverYouWantToCallThisFuncName(with valueToPass: T) -> T {
        let result = whoKnowsWhatQueueThisWillExecuteOn(with: valueToPass) { t in
            ///Mutate t and do what you will with it
            ///T is yours
            return t
        }
        return result
    }
}

I haven't tested this out but it was fun writing this in a polymorphic way.
Hopefully this could be a starting point in helping you find what you are
looking for, happy holidays to ya

Hmm... On reading your post, I started wondering whether Sendable was it. However, if I use an arbitrary, non-Sendable class, it still compiles.

I did find one wrinkle that is currently leading me toward thinking that this is a compiler bug. This actually does raise a compiler error:

class SomeClass {}

actor Foo {
    var bar = SomeClass()
    func setBar(_ bar: SomeClass) { self.bar = bar }

    func baz() {
        DispatchQueue.main.async {
            // also compiles without the `Task`
            Task {
                self.setBar(.init()) // error: Expression is 'async' but is not marked with 'await'
            }
        }
    }
}

However, this seemingly functionally identical code compiles with no complaints:

func whoKnowsWhatQueueThisWillExecuteOn(closure: @escaping () -> ()) {
    DispatchQueue.main.async { closure() }
}

class SomeClass {}

actor Foo {
    var bar = SomeClass()
    func setBar(_ bar: SomeClass) { self.bar = bar }

    func baz() {
        whoKnowsWhatQueueThisWillExecuteOn {
            // also compiles without the `Task`
            Task {
                self.setBar(.init()) // A-OK, I guess
            }
        }
    }
}

Am I off-base? This has to be unintended behavior, right?

At a quick glance (so don’t take this as 100% sure) I think is because Task.init.
Task.init inherits its current context (which includes the actor) so the compiler knows where the code in the closure is supposed to run.
Try Task.detached and see of that changes things.

It does indeed:

func wellTechnicallyIGuessIKnowWhatQueueThisWillExecuteOn(closure: @escaping () -> ()) {
    DispatchQueue.main.async { closure() }
}

class SomeClass {}

actor Foo {
    var bar = SomeClass()
    func setBar(_ bar: SomeClass) { self.bar = bar }

    func baz() {
        wellTechnicallyIGuessIKnowWhatQueueThisWillExecuteOn {
            Task.detached {
                self.setBar(.init()) // error: Expression is 'async' but is not marked with 'await'
            }
        }
    }
}

I'm still confused, though, as to why the Task.init version gets the compile error when I call DispatchQueue.main.async directly instead of going through the mystery box function. What would make the mystery box function safer than that?

Wait, I'm pretty sure Task.init can't be the entire culprit, since this still compiles even without the task:

func wellTechnicallyIGuessIKnowWhatQueueThisWillExecuteOn(closure: @escaping () -> ()) {
    DispatchQueue.main.async { closure() }
}

class SomeClass {}

actor Foo {
    var bar = SomeClass()
    func setBar(_ bar: SomeClass) { self.bar = bar }

    func baz() {
        wellTechnicallyIGuessIKnowWhatQueueThisWillExecuteOn {
            self.setBar(.init()) // why does this compile???
        }
    }
}

:confused:

2 Likes

I am not trying to sound insulting or trying to get someone to follow or evangelize upon mutable state and threads but from what I know of actors is that you want to keep the actor as isolated as possible so what I have learned and what works best for me and my company is to save all of the work and code being done inside of the actor singleton as much as possible. Still again if you do require work to be done outside of the actor or initializing the actor itself inside of a global helper func is wild but I will see what errors I get from it and try it out and see what is going on here. I still know there is some "magic" being done by the actor from my code base and I am trying to hunt down those bugs myself after my implementation. Also if i could confirm some stuff that you would like me to try out as well.

  1. is did you want the callback to be a global func?

  2. what if anything would with the class like a class that doesn't conform to the sendable protocol?

  3. if that question was way too confusing, is your class conforming to the sendable protocol and if its not then I could see why the compiler would be all fussy.

  4. Is your code set in stone, like can you not change it due to company or framework reasons?

Ill try my best, sorry for the late reply, holidays have been busy.

func wellTechnicallyIGuessIKnowWhatQueueThisWillExecuteOn(closure: @escaping () -> ()) {
DispatchQueue.main.async { closure() }
}

class SomeClass {}

actor Foo {
var bar = SomeClass()
func setBar(_ bar: SomeClass) { self.bar = bar }

func baz() {
    wellTechnicallyIGuessIKnowWhatQueueThisWillExecuteOn {
        Task.detached {
            await self.setBar(.init()) // error: Expression is 'async' but is not marked with 'await'
        }
    }
}

}

Just mark it with an await I guess haha, just make sure that the behavior is what you want like look at the threading in the debugging window and see if the task.detach is acting weird or not

This isn't from any particular project of mine. This is me playing around with a bunch of weird situations to try to improve my understanding of actors and what all of their little unwritten rules are, and running into one case in which the fact that it's working is confusing me, since it seems like it shouldn't.

is did you want the callback to be a global func?

In this example, the function taking the callback is from some external library where we don't really know what it's doing internally.

what if anything would with the class like a class that doesn't conform to the sendable protocol?

if that question was way too confusing, is your class conforming to the sendable protocol and if its not then I could see why the compiler would be all fussy.

The compiler's being all un-fussy, which is what's making me scratch my head. :wink:

Is your code set in stone, like can you not change it due to company or framework reasons?

Purely academic curiosity at this point.

1 Like

This code compiles? Seems like a bug if so. The escaping closure isn’t in an async context and should not be able to access the actor afaik.

It sure does, at least on my system. :man_shrugging: I agree that it seems like it shouldn't.

Maybe I should just repost this on the bug tracker and let the team figure out whether it's a bug or not.

I went ahead and created a bug on the tracker:

https://bugs.swift.org/browse/SR-15745

1 Like

I stumbled across this recently. I had to add nonisolated to safely escape the actor isolated method.

actor Adder {

  var total: Int = 0

  func add(_ val: Int) {
      total += val
  }

  // 😱 escapes `add(_:)` bypassing actor isolation
  func getAdderUnsafe() -> (Int) -> Void {
      return add(_:)
  }

  // 🙃 escapes `add(_:) async` keeping isolation
  nonisolated func getAdder() -> (Int) async -> Void {
      { await self.add($0) }
  }
}

This is concerning, considering how it violates the premises and rules of actors.

This is concerning, considering how it violates the premises and rules of actors.

I agree, although so far my report has been largely ignored. If you guys could all vote for the issue I created ([SR-15745] Actor methods able to be called from escaping closure without `await` - Swift), it might help.

1 Like