Suggestion - Runtime warnings in debug builds if @MainActor methods/vars are called from background threads

Marking a method or variable with @MainActor doesn't guarantee that it will be called on the main thread - but that's what people are probably expecting, and this can lead to errors.

It's pretty easy to get caught out with a call to a @MainActor method on a background thread - particularly if you're using ObjectiveC or frameworks/libraries with asynchronous callbacks (like CoreBluetooth).

I posted some examples of how this can happen:
https://blog.hobbyistsoftware.com/2022/11/five-ways-to-break-swift-concurrency/

If XCode automatically gave purple runtime warnings whenever this happened - then people would at least be prodded to investigate.

Obviously this would only be for debug builds, and perhaps disable-able with a build flag.
I have no idea how much overhead it would add to check the thread on every call.

Thoughts?

6 Likes

In your examples, you show that Obj-C method dispatch can be used to invoke a @MainActor function from another thread. I think that's likely unintentional - in other words, a bug. Have you filed a Github issue or Apple feedback report about it?

It's not a diagnostics issue; it's a memory safety issue.

As for solutions, maybe it's possible for the Obj-C versions of these functions to be secretly be some kind of wrapper which checks the thread?

I don't send feedback reports any more.

https://blog.hobbyistsoftware.com/2021/08/fool-me-once-reporting-bugs-to-apple/

Is the -Xfrontend -enable-actor-data-race-checks command-line option what you're looking for? This enables runtime warnings (or fatal errors if you set the appropriate env variable) for using an actor-confined thing from outside that actor. I assume it will become the default in Swift 6.

1 Like

I don't see "optimise for Mac" in a new project created with Xcode 14:

FWIW, the app works correctly for me when running iOS target on macOS. Testing on macOS 12.6

Without filing the bug there's always an excuse: "you didn't file the bug → we didn't know about it → so it is not fixed". If you do file the bug at least the ball is on the apple side.

Is the -Xfrontend -enable-actor-data-race-checks command-line option what you're looking for?

yup - that's exactly it. Thanks @tgoyne

It seems odd that this isn't the default already given that it only generates warnings by default...

Bugs related to the Swift project, including concurrency, can be tracked on GitHub. You can consider filing an issue there.

I'm not really convinced that these are bugs here. Just that they demonstrate limitations in Swift Concurrency.

What I'd really like to see is clear documentation on what @MainActor does - and what it does not guarantee.

It appears that there is a documented guarantee—you quote it—which is not met: that would be a bug. We should meet guarantees that we document or we shouldn’t document them.

First-party documentation that says “The compiler guarantees…” is very strong language, and if the guarantee isn’t upheld, there is little wiggle room here: something is wrong and it’s not you.

2 Likes

Right—if it’s infeasible to enforce these guarantees robustly when going through the Objective-C runtime then that should be made clear.

I’m more worried about the example where supplying a key path literal to map is apparently enough to defeat the main actor guarantee.

Oh wow, I totally missed that one. Yeah, that seems like a very straightforward bug that definitely shouldn’t compile when concurrency checking is enabled. Looks like we’re skipping actor isolation checks when we convert a key path literal to its corresponding function.

It appears that there is a documented guarantee—you quote it—which is not met: that would be a bug. We should meet guarantees that we document or we shouldn’t document them.

First-party documentation that says “The compiler guarantees…” is very strong language, and if the guarantee isn’t upheld, there is little wiggle room here: something is wrong and it’s not you.

Is that documented?
I haven't found it in official documentation.
(I haven't found any official documentation of what @MainActor does...)

'The Swift Programming Language' only mentions @MainActor once in passing whilst discussing Sendable.
According to swift.org, that's the 'comprehensive guide and formal reference'

I thought it was your blog which quotes this line from Apple:

By adding the new @MainActor annotation to Photos, the compiler will guarantee that the properties and methods on Photos are only ever accessed from the main actor.

The quote is an example of the kind of description I see in less formal contexts.
In this case, it was a quote from an Apple rep in a WWDC talk.

I wouldn't call that a documented guarantee, and it was only intended as an example of what people describe/expect.

There is of course lots like that in tutorials/forums/etc - as well as in many other Apple talks.

For example in 'Eliminate data races using Swift Concurrency', there is a stronger quote:

Isolation to the main actor is expressed with the MainActor attribute.

This attribute can be applied to a function or closure to indicate that the code must run on the main actor.

Then, we say that this code is isolated to the main actor.

The Swift compiler will guarantee that main-actor-isolated code will only be executed on the main thread, using the same mechanism that ensures mutually exclusive access to other actors.

but again - I don't take WWDC talks as 'documentation'

The documentation you’re looking for here probably falls out of the proposal for actors (SE-0306) and global actors (SE-0316). I’m not sure you’ll find quite such a pithy quote , but SE-0316 does say this:

Note that the data in this view controller, as well as the method that performs the update of this data, is isolated to the @MainActor . That ensures that UI updates for this view controller only occur on the main thread, and any attempts to do otherwise will result in a compiler error.

And the proposals are broadly clear that, at least within Swift, actor isolation isn’t really ‘optional’—it should be that anything isolated to an actor (global or otherwise) cannot be synchronously accessed from outside that isolation domain.

One of the tests for the actor data race warning is also something that I'd expect to be a compile error as it's very straightforwardly doing something which violates actor isolation. The other one which uses an unsafeBitCast is reasonable though.

I figured I should stop complaining about the lack of documentation, and have a go myself at writing the @MainActor rules...

I couldn't find a bug report for this specific problem, so I went ahead and filed one: Using key path literals as functions defeats @MainActor checks ¡ Issue #63189 ¡ apple/swift ¡ GitHub

I also agree that the other @MainActor loopholes @ConfusedVorlon identified are important to either close or document very explicitly. @ConfusedVorlon Thank you for your articles about these problems!

5 Likes

I found another 'loophole' today.

Binding captures getter/setter callbacks.
The compiler considers these as in the context where they are created (so you can call @MainActor in your setter if the binding is created in @MainActor)

But the actual callback runs merrily on whatever thread it is called from