What's being unsafely sent here?

I'm working on converting a legacy codebase to modern concurrency. One of the helper methods in the project is runOnMainThreadAsync, which runs its argument immediately if on the main thread and asynchronously otherwise. The initial implementation is something like this:

func runOnMainThreadAsync(work: @escaping () -> Void) {
  if Thread.isMainThread {
    work()
  } else {
    DispatchQueue.main.async(execute: work)
  }
}

Most of the time this is called, it's accessing some API that can only be used from the main thread, i.e. @MainActor functions. As such I need to annotate this callback as @MainActor. However, I'm unable to find a spelling that doesn't diagnose about the else branch. Here's what I've come up with so far:

func runOnMainThreadAsync(work: @escaping @MainActor () -> Void) {
  if Thread.isMainThread {
    MainActor.assumeIsolated(work)
  } else {
    Task { @MainActor in
      work()
    }
  }
}

This gives me

Task or actor isolated value cannot be sent

pointing at the { on the line Task { @MainActor in. What's being illegally sent here?

  • The task isn't being sent, as it's not being returned, but even then tasks are sendable
  • The work is being sent to the actor it's isolated to so there's nothing unsafe here, right?

I suspect this is an overzealous or misleading diagnosis, but I'm running out of ideas. Is it just not possible to safely schedule an @escaping @MainActor closure from a nonisolated method?

Try adding @Sendable to the closure definition. I think there's bug in the Swift 5 mode where @MainActor doesn't properly infer Sendable (even with complete checking), but in Swift 6 mode it does.

1 Like

Yep, @Sendable did it, thanks! (With that annotation I can even leave it as DispatchQueue.main.async(execute: work), which is mildly surprising, but whatever)

Because then it matches the signature required by the execute parameter.

But execute isn't @MainActor, only @Sendable, right? Adding the @MainActor annotation would be dependent on the value of self, which I thought wasn't expressible under Swift's current type system.

The compiler is hard coded to recognize that DispatchQueue.main requires @MainActor (the implementation of which is hilarious). Unfortunately this means your own API wrapping the main queue lose the special capability, which is very annoying.

1 Like

It's not a bug, it's just behind a different upcoming feature. You need to enable -enable-upcoming-feature GlobalActorIsolatedTypesUsability. The reason it's not enabled under -strict-concurrency=complete is because it would be a source break for code that had already adopted complete concurrency checking. The same is true for InferSendableFromCaptures and DisableOutwardActorInference.

5 Likes

Is that an experimental or upcoming feature? It doesn't appear to be toggleable in Xcode.

It would be nice if the diagnostics could hint toward that fact (though that verges on having to compile under both modes or something).

2 Likes

It's a proper upcoming feature implemented in Swift 6.0. Thanks for pointing out that there's a missing toggle in Xcode build settings. It can still be enabled via "Other Swift Flags"

Yes, agreed.

1 Like

So should we use -enable-upcoming-feature with it instead of -enable-experimental-feature?

1 Like

Yes, you should use -enable-upcoming-feature. (sorry, I fixed the typo in my post above)

4 Likes