When using `DispatchQueue.main` as a parameter/variable it's impossible to satisfy the `@MainActor` requirements

When using DispatchQueue.main as a parameter/variable it's impossible to satisfy the @MainActor requirements.

See the following code:

final class MyClass {
    init() {
        DispatchQueue.main.async {
            self.someMainFunction() // legal
        }

        let queue = DispatchQueue.main
        queue.async {
            assert(Thread.isMainThread) // true
            self.someMainFunction() // Call to main actor-isolated instance method 'someMainFunction()' in a synchronous nonisolated context
        }

        queue.async { @MainActor in // Converting function value of type '@MainActor () -> ()' to '@convention(block) () -> Void' loses global actor 'MainActor'
            assert(Thread.isMainThread) // true
            self.someMainFunction()
        }

        // or another example

        doSomethingAsync(queue: .main) { [weak self] in
            assert(Thread.isMainThread) // true
            self?.someMainFunction() // Call to main actor-isolated instance method 'someMainFunction()' in a synchronous nonisolated context
        }
    }

    func doSomethingAsync(queue: DispatchQueue, something: @escaping () -> Void) {
        queue.async {
            something()
        }
    }

    @MainActor
    func someMainFunction() {
    }
}

Any idea for workarounds?

Also asked on SO, swift - When using `DispatchQueue.main` as a parameter/variable it's impossible to satisfy the `@MainActor` requirements - Stack Overflow

And another similar question (unanswered): How does swift annotate that DispatchQueue.main.async runs on @MainActor but DispatchQueue.background.async doesn't - #7 by ConfusedVorlon

1 Like

Checking the last question you linked to, this does feel like a (probably temporary) oversight in however the compiler is determining whether or not DispatchQueue.sync() or DispatchQueue.async() will fire on the main queue.

That aside though, could you share why you're using DispatchQueue directly instead of @MainActor to ensure code runs on the main thread? That will probably determine what workarounds are viable or not for you.

This is just a simplified example of the issue. The main use case we have is passing the DispatchQueue as a parameter to a function, and using it inside that function.

So while we know that doSomething(on: DispatchQueue.main) { something } will be executed on the main, it's not possible to satisfy that requirement with @MainActor.

This causes several thousand warnings in our project where this format is used for a pre-async implementation of Futures.

@clausjoergensen You might be able to do something like this:

typealias DispatchQueueAsync = (
  DispatchGroup?,
  DispatchQoS,
  DispatchWorkItemFlags,
  @escaping @convention(block) () -> Void
) -> Void

typealias DispatchQueueMainAsync = (
  DispatchGroup?,
  DispatchQoS,
  DispatchWorkItemFlags,
  @escaping @convention(block) @MainActor() -> Void
) -> Void

extension DispatchQueue {
  func async( // possibly different name
    group: DispatchGroup? = nil,
    qos: DispatchQoS = .unspecified,
    flags: DispatchWorkItemFlags = [],
    execute work: @escaping @convention(block) @MainActor() -> Void
  ) {
    // could check if main thread here
    unsafeBitCast(
      self.async as DispatchQueueAsync,
      to: DispatchQueueMainAsync.self
    )(group, qos, flags, work)
  }
}

Edit: Keeping in mind it doesn't ensure the work is performed on the main thread. ie:

DispatchQueue.global(qos: .background).async { @MainActor in
  print(Thread.isMainThread) // false
}

Seems a bit over the top as far as workarounds go. What we'll probably end up doing is create specific "onMain" methods to use while we transition to async/await (once we drop iOS12 of course, since the Concurrency library causes iOS12 apps to crash)

It just seems like the DispatchQueue.main.async and @MainActor scenarios here just really wasn't thought through by the Swift team when they introduced actors.

I'm surprised it didn't break Combine as well (or maybe it did and we just haven't found out yet)

It doesn't seem like this is solvable in general since the compiler wouldn't always be able to see into the body of functions like doSomethingAsync(queue:something:) to verify the relationship between the queue and the work that is being run. (Or in the other direction, the compiler wouldn't always be able to see the source of an arbitrary DispatchQueue to verify whether it really is the main queue.)

I feel like what we would want here is some sort of library function akin to withoutActuallyEscaping, something like assumingAlreadyOnMainActor(do:) which would be available from non-@MainActor contexts, but would accept and immediately execute an @MainActor closure. Of course, it would be a programming error if this function were executed not on the main actor at runtime.

I vaguely remember something like this coming up during the discussion of the concurrency proposals, but maybe it was just suggested as a future direction?

2 Likes

What we'll probably end up doing is create specific "onMain" methods to use

Oh cool :slight_smile: For some reason I was thinking there was a requirement for queue.async { @MainActor in ... }.