How does swift annotate that DispatchQueue.main.async runs on @MainActor but DispatchQueue.background.async doesn't

I haven't found any definitive documentation around this - so please point me at that if it's available!

Somehow, the compiler 'knows' that

        DispatchQueue.background.async {
            //this does not run on the main-actor
        }

        DispatchQueue.main.async {
            //this does run on the main-actor
        }

I can't see any clues in the headers as to how the two cases are annotated differently. They both just show as DispatchQueue.

I have a similar case in my own API where the user can set the queue for a notification to callback.
I'd like to mark that the block runs on @MainActor if the queue is .main - but I can't see how to do that.

e.g.

func register(on queue:DispatchQueue,callback:<if queue is .main @MainActor> ()->Void)

1 Like

What is DispatchQueue.background and what exactly do you mean by "compiler 'knows'"?

sorry - I'm so used to using my own extensions!

public extension DispatchQueue {
    static var background:DispatchQueue {
        get {
            return DispatchQueue.global(qos: .background)
        }
    }

By 'knows', I mean that it emits warnings if the block runs code that requires @MainActor access, and has been run by .background

I have the following flags turned on:
OTHER_SWIFT_FLAGS = -Xfrontend -warn-concurrency -Xfrontend -enable-actor-data-race-checks

the header gives me the following for async:

public func async(group: DispatchGroup? = nil , qos: DispatchQoS = .unspecified, flags: DispatchWorkItemFlags = , execute work: @escaping @convention (block) () -> Void)

but when used with .main, it is as if the method is:

public func async(group: DispatchGroup? = nil , qos: DispatchQoS = .unspecified, flags: DispatchWorkItemFlags = , execute work: @escaping @convention @MainActor (block) () -> Void)

There's indeed some magic inside. It's not very deep/thorough though, probably some simple heuristic in there:

func foo() {
    DispatchQueue.main.async {
        _ = UIApplication.shared.applicationState // ok
    }
    let mainQueue = DispatchQueue.main
    mainQueue.async {
        _ = UIApplication.shared.applicationState // error
        // Class property 'shared' isolated to global actor 'MainActor' can not be referenced from a non-isolated synchronous context
    }
}

Edit:
Or even this:

    enum DispatchQueue { // let's cheat
        static let main = Foundation.DispatchQueue.global()
    }
    DispatchQueue.main.async { // not real main
        _ = UIApplication.shared.applicationState // ok 😀
    }

so you think 'special case compiler magic' rather than 'using the @MainActor' annotation (or similar) in a way that any API designer can use?

Looks so. I'd prefer some annotation system like @convention(mainActor) to make the check thorough but apparently it's much harder to do. You can still rely on runtime checks though.

You can never rely on runtime checks ;)