MainActor.assumeIsolated without halting program

Is there a way to use MainActor.assumeIsolated in certain places without halting the program if the thread is incorrect? I'd like to enable strict concurrency and this escape hatch is critical for a legacy codebase, but I'd rather send a log in prod than crash. I don't want to disable dynamic isolation runtime checking everywhere, however, just in select places in code.

1 Like

MainActor.assumeIsolated works by using unsafeBitCast to cast away the @MainActor on its closure: swift/stdlib/public/Concurrency/MainActor.swift at 380f319371dc9d50a98b2da5fba5affce3dcaf28 · swiftlang/swift · GitHub

You can implement the same thing yourself, and use something like Thread.isMainThread to perform a similar check for the main actor.

Just be aware that if the test fails, you'll be running @MainActor code off the main actor, which is likely to crash anyway. So I'm not sure that this is all that useful in general.

4 Likes

Generally we try not to offer such unsafe version of it.

Note that you cannot implement the check "correctly" because dispatch doesn't offer a non-crashing API either.

You can check for the main thread, but the main queue is NOT necessarily the main thread. As Keith said, yes it'll be unsafe if you run such code and you're likely to get other mysterious crashes somewhere...

Having that said, I understand especially libraries and SDKs may want to give users a "softer" migration path, so we'd like to explore if we can provide warnings. But... no such API exists in Dispatch to reliably implement this, and it's been an ongoing topic "if" there could be such SPI for Swift Concurrency, but there isn't one today.

1 Like

Can't you use DispatchQueue.setSpecific() to check for the queue and not the thread? In what way is it not reliable? Quite curious, because I just saw a case where a test that used this failed (once, four years ago), which I was going to chalk up to memory corruption or something.

1 Like

I’m not sure I understand exactly what you’re proposing, but my experience is that checking this stuff is hard. Consider this:

import Foundation

func main() {
    let q = DispatchQueue(label: "not-main")
    q.sync {
        dispatchPrecondition(condition: .onQueue(q))
        print(Thread.isMainThread)  // prints "true"
    }
}

main()

So we’re using the main thread to run on a secondary queue. And you can get the exact opposite too:

import Foundation

func main() {
    let source = DispatchSource.makeTimerSource(queue: .main)
    source.setEventHandler() {
        dispatchPrecondition(condition: .onQueue(.main))
        print(Thread.isMainThread)  // prints "false"
        exit(0)
    }
    source.schedule(deadline: .now() + 0.1)
    source.activate()
    dispatchMain()
}

main()

Fun times!

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

4 Likes

I can't say for sure if this is the same as what @benpious proposed, but my idea was to consider a workaround for the lack of a checking API in Dispatch implemented in executors:

Depending on the type of executor, we define a locally stored object which is basically just a std::stack<executor_unique_id_t>:

  • For DispatchQueue-backed executors, this object is stored via dispatch_queue_set_specific.

  • For Thread-backed executors, it's stored in the TLS.

Every time an executor enqueues a job, it adds a prologue with stack->push(executor_id) and an epilogue with stack->pop().

Then, the check function is implemented as a test to see if the stack object exists in the current context and if stack->top() matches the expected ID.

Quinn: this code in RxSwift is what I'm referring to.

They're using it in some of their testing APIs to do a runtime version of marking them @MainActor.

My understanding was that you basically never want to check if you're on the main thread in GCD, and you always want to check the queue instead. I kind of assumed that even when the main thread wasn't 1:1 with the main queue that roughly the same notion of Isolation from Swift Concurrency was going on.