Concurrency and @escaping closures

Currently, there's a hole in the Swift Concurrency story regarding escaping closures. For example:

// This function does not document whether the completion
// closure will be invoked on the main thread or on some
// other thread.
func runSomethingLater(_ completion: @escaping ()->Void) {
    DispatchQueue.global().async {
        completion()
    }
}

@MainActor
func mainThreadThing() {
    assert(Thread.isMainThread)
}

func main() {
    runSomethingLater {
        // The compiler doesn't know whether this
        // closure will run on the main thread or
        // a background thread. It allows this call
        // without an `await`, even though we're 
        // actually on a background thread here. This
        // will trigger the 'assert' in mainThreadThing()
        mainThreadThing()
    }
}

This means that annotating a function with @MainActor is not currently sufficient to ensure that it actually runs on the main thread. This is the current state of things. No warning is given for this code, even with -warn-concurrency enabled.

I know there are proposals for @Sendable closures and Senable checking., but I don't think I have a complete understanding of what that will entail. Is there a plan that will cause the code above to issue either a warning or an error at some point in the future?

2 Likes

My understanding is that Sendable checking will enforce that you either await mainThreadThing() or mark the closure argument to runSomethingLater as @MainActor in.

The Swift 5.6 beta I have installed correctly diagnoses this:

error: call to main actor-isolated global function 'mainThreadThing()' in a synchronous nonisolated context
        mainThreadThing()
        ^
note: calls to global function 'mainThreadThing()' from outside of its actor context are implicitly asynchronous
func mainThreadThing() {
     ^

Doug

2 Likes

Okay, that's good to know. I investigated more, and found some more interesting information. As posted above, the code does indeed produce an error. However, if func main() is itself tagged with @MainActor, then the warning goes away.

Here's the actual code from my sample project:

@MainActor
class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.

        callCompletion {
            mainThreadThing()
        }
    }
}

func callCompletion(_ completion: @escaping ()->Void) {
    DispatchQueue.global().async {
        completion()
    }
}

@MainActor
func mainThreadThing() {
    assert(Thread.isMainThread)
}

Because viewDidLoad is implicitly @MainActor, the error seems to have gone away, and yet the assertion still fails. Is the compiler assuming that the completion handler is also being implicitly annotated with @MainActor?

Here, completion needs to be Sendable because it is captured in a closure that will be executed concurrently. However, your callCompletion function doesn't use any concurrency (per the definition in SE-0337), so Swift 5 doesn't warn. Under Swift 6, you get:

t7.swift:19:9: error: capture of 'completion' with non-sendable type '() -> Void' in a `@Sendable` closure
        completion()
        ^
t7.swift:19:9: note: a function type must be marked '@Sendable' to conform to 'Sendable'
        completion()
        ^

At present, we don't get a corresponding warning for this code under -warn-concurrency. I consider that a bug that we should fix.

Doug

8 Likes