Async/Await: is it possible to start a Task on @MainActor synchronously?

Hi @gwendal.roue! Is this what you're looking for?

await MainActor.run {
  // this code executes on the main actor.
  print("Hello, main actor!")
}
1 Like

Thanks @grynspan! I was using await Task { @MainActor in ... }, and I have to learn the difference between the two variants — oh yes, MainActor.run accepts a sync function.

Yet this won't do it yet, because your suggestion can only be called from an asynchronous context (inside an async function). In order to launch it from a synchronous context (such as in a UIKit or SwiftUI callback, in my specific case), some async Task must be created. And there lies the behavior I'd like to avoid: even if the task is created from the main thread, it won't "start" immediately.


The more I think about it, the more I start to understand that my request is quite unlikely to be ever fulfilled:

// Ideally, the "parallel stuff" only starts as soon as
// the iterator yields execution:
Task { @MainActor in
  for try await value in myAsyncSequence {
    ...
  }
}
// <- here all values that were produced before
// the first suspension point are already handled.

We can imagine that with enough information, the compiler could embed the beginning of the task right into the caller. Yet this will never happen unless the sequence and its iterator are inlinable, and there exists some explicit annotation in the source code that opts in for this scheduling optimization.

Yet I deeply regret that this precise scheduling is almost certainly out of reach, considering this use case has a long history of conscious and explicit support in existing async frameworks (I quoted LibDispatch, Combine and RxSwift before). Really all I can hope for is that a designer of Swift concurrency happens to read this thread, chimes in, and expresses something about the topic - a regret, a hope, a workaround, an acknowledgment.

If there was a way to query “current actor / executor” then what you want is:

  • if on actor “main”
  • run this now
  • else
  • run this later (“main actor”.run {})

That capability could appear along with custom executors I suppose.

Thank you @ktoso. Maybe custom executors will help defining precise interactions with @MainActor, yes. I hope this topic will come back in the design discussions!

We should document the behaviour of MainActor.run() when called from the main actor. :slight_smile:

Maybe MainActor will change its behavior, and start running synchronously when it can, yes.

But if I understand @ktoso well, at the time custom executors become public, we'd become able to write, outside of the stdlib, in our own application or library code, a custom global actor that does what we need:

@MainActor // (sic)
func someSynchronousUIKitOrSwiftUICallback() {
  Task { @UIActor in // (sic) a custom global actor
    for await value in someAsyncSequence {
      // handle value
    }
  }
  // <- Here values that were produced synchronously
  // are already handled, and UIKit/SwiftUI has been
  // correctly instructed.
}

This custom UIActor would play exactly the same role as those existing tools:

They exist for a well identified reason: they help developers program asynchronously within the constraints of frameworks like UIKit or SwiftUI.

In UIKit or SwiftUI, a missed opportunity from one of their synchronous callbacks usually means a degraded experience for the app user or the developer, from a blank frame on screen, to a need to explicitly code against double-taps.

Combine and RxSwift exist independently of UIKit or SwiftUI, and yet they provide the sharp and necessary tools for a seamless integration. I'm very eager to see Swift concurrency fill the gap with custom executors.

1 Like

I’m not saying anything like any magic treatment of submitting a task. Submitting a task means just that, you’ve enqueued it, at some point it’ll run.

I literarily mean that what wrote out up there in the bullet points as code that anyone could write, if there was this “get actor” and equality check on them:

extension MainActor { 
  static func runNowIfPossibleOrLater(_ body: <<same annotations as Task>> () -> ()) {
    if <<is current actor main actor>> { 
      body() 
    } else { 
      MainActor.run { body() }
    }
  }
}

I’ve not thought this through but this sounds like that you’re asking for — is that right? Tho again, all this has to be discussed and debated a bit more during Swift Evolution. I personally think it’d be reasonable to allow such “get actor” and based on this, we could implement such, and other interesting APIs.

No it is not, because I discuss about tasks started from a sync function. In a very specific fashion: this sync function is a UIKit or SwiftUI callback that does not behave as desired as soon as a thread hop happens (because that's how they work).

Since MainActor.run is declared as an async function, the aforementioned sync function has to start a task, and it is already too late if starting a task always performs a thread hop.

at some point it’ll run

The goal is to turn this "at some point" to "right now because I instructed the program to do so".

For a description of one undesired consequence of this mandatory thread hop, see the above comment.

If thread hopping remains unavoidable in the transition from a sync context to an async context, then Swift concurrency will remain unsuitable for some UIKit/SwiftUI interactions. That's not that bad, since it's not as if we missed alternative tooling. But that's quite a missed opportunity.

It is intentional that Swift Structured Concurrency does not define a mechanism allowing a synchronous function to invoke work on an actor and block until it is complete.

As I recall (and I may be recalling incorrectly) such an operation would require some scheduling mechanism that could block the calling thread on the actor's execution context. That mechanism might be a semaphore but isn't necessarily, depending on whether the actor is bound to a specific thread/queue or not.

For the main actor on Apple's platforms (i.e. where UIKit lives), the main actor is bound to the main thread/queue, and a semaphore is needed to gain the desired effect. Using a semaphore in this fashion is an anti-pattern on Apple's platforms because it leads to priority inversions, deadlocks, and other unpleasant side effects.

It is intentional that Swift Structured Concurrency does not define a mechanism allowing a synchronous function to invoke work on an actor and block until it is complete.

Absolutely nobody is asking for that, and all the awful consequences you rightfully enumerate.

In the previous sample code, it is not written that one waits for Asynchronously produced values. No. It has always been about Synchronously produced values that are currently thrown in with the bath water because all tasks induce a thread hop in the transition from a sync function to an asynchronous context.

Maybe one has to experience the pain, one has to understand why there exists so many solutions to this problem in other async frameworks, in order to see what I'm talking about.

This can be a fun game: please try to feed a SwiftUI view from an async sequence, with two constraints:

  1. The iteration of the async sequence must start from that view (so that the view is standalone).
  2. The first rendering is performed with the first sequence element. In other words, we avoid an initial "blank" rendering, as the view is waiting for the first element of the sequence. (This avoids undesired rendering glitches)

This can be done with LibDispatch, with Combine, with RxSwift, nearly all async frameworks out there, when one takes care of building a sequence that produces its initial value synchronously. But this can not be done, as far as I know, with Swift concurrency and async sequences. I regret it, and if I'm the only one discussing it in these forums, I'm certainly not alone regretting it.

3 Likes

Perhaps I'm not understanding the problem correctly? It seems like you want the Standard Library to supply something like @ktoso's runNowIfPossibleOrLater() function so that SwiftUI can avoid presenting a blank view before the data becomes available?

I don't think that such a function is the right approach when working with SwiftUI, but there's nothing stopping you from using runNowIfPossibleOrLater() as Konrad wrote it.

The "right" approach is probably to have SwiftUI work seamlessly with Structured Concurrency. My understanding is that View.task(id:_:) is the right way to attach asynchronous work to a view:

@State var favoriteFood: String = "Fetching..."

var body: some View {
  VStack {
    Text(favoriteFood)
      .task(id: favoriteFood) {
        favoriteFood = await getFavoriteFood()
      }
    Button("Set Food") {
      Task {
        await setFavoriteFood(...)
      }
    }
}

Yes there is a misunderstanding - surely because I can't explain myself correctly.

When a view starts a task, as in your above example, the view currently MUST display a transient state, even if the task completes very quickly (and could complete synchronously if the runtime would allow it).

In this transient state lies many bad things:

  • Visible flashes of missing content. QA reports: "When the user profile appears, the user avatar picture does not show up immediately". Developer thinks: "but the avatar is cached: we should be able to display it immediately! All right, let's ask my coworker to replace this async function with a callback-based api, or a Combine publisher, so that I can fix this ticket.")
  • Undesired animations QA reports: "List contents should not animate in the initial appearance of the screen". Developer thinks: "I could handle make a special case of the initial appearance, but the real problem is that I can't immediately setup my view with its initial content, so that animations only trigger for future updates. All right, let's ask my coworker to replace this async sequence with a callback-based api, or a Combine publisher, so that I can fix this ticket."
  • Poor-quality SwiftUI previews. Developer 1 reports "Xcode preview is empty, even though the data should be readily available. When I hit the run button the content appears, though, but this is slow and ruins the experience". Developer 2 thinks: "I'm fed up with those async tasks. The language is not production-ready yet and we switched to async/await much too early. What a pity @groue was not listened to with more attention at the time he was reporting all this pain, with a luxury of details, of the Swift forums".
  • Etc
12 Likes

What about this:

public extension Task where Failure == Never {

  @discardableResult
  static func onMainActor(priority: TaskPriority? = nil,
                          @_implicitSelfCapture operation: @escaping @MainActor () async  -> Success) -> Task where Failure == Never {
    .init(priority: priority) {
      await operation()
    }
  }
}

Hi Guy. This extension does not address the initial issue, which is to start a @MainActor task in a synchronous way from the main thread:

// Prints (1 2 3 4) or (1 2 4 3), but never (1 4 2 3)
print("1")
Task { @MainActor in
   print("2")
   await ...
   print("3")
}
print("4")

See the whole above thread for various explanations why this behavior can be desirable.

Just wanted to chime in to say that I've also wanted the same ability @gwendal.roue describes.

There are a lot of cases in my UI code (especially when initializing a new screen) where I'd like to call a function that is technically async but is very, very quick in practice. A good example is cache retrieval or accessing an actor that isolates an in-process data structure.

As @gwendal.roue says, this currently requires launching a task and creating an intermediate state in the UI to indicate that we're waiting for results (a progress wheel, a template profile photo, etc) even though the results will arrive almost immediately. The user sees a flicker of the stub interface before their data appears (which makes the app look unpolished and slow).

It would be great to have a way to do what Gwendal is asking for.

1 Like

Here I go again but it really sounds like you could just use a lock. Protecting simple data structures is not worth the cost of going async (both in terms of performance cost and also the cost of spreading async/await everywhere).

I'd like to add that per my own research, the reason this is impossible is because Swift Concurrency requires that all concurrency threads make forward progress, and running async code from a synchronous context implies blocking the synchronous context until the asynchronous context (which could be on the same thread or could involve awaiting other tasks/threads) completes.
Trying to skirt around the issue by introducing locks is possible, but breaks the guarantees of Swift Concurrency, thereby opening yourself up to the possibility of deadlocking the entire Swift Concurrency framework (eg. if your main-thread synchronous code starts waiting for asynchronous code off-thread that starts waiting for the now-locked main actor).

I would actually also love to see someone from the Swift Concurrency team chime in to explain clearly how we are expected to interop Swift Concurrency-based paradigms with code operating on synchronous paradigms. (cc @Douglas_Gregor perhaps?)

Actors are fantastic, but once you lock away your data in actors, any usage of this data bleeds out as async requirements into the entire code base. Very quickly, you'll find yourself in a scenario where UIKit-based code which assumes things like "finish your setup before willFinishLaunchingWithOptions or viewWillAppear completes", and trying to satisfy those requirements with Tasks appears to be nigh on impossible. With UIView.beginAnimations and UIView.commitAnimations deprecated, how do I guarantee that async @MainActor code gets to fully complete within the animation context, seeing as UIView.animate is not-async? Try as I may, I can find no good guidance from Apple or the Swift team on how we can bring these two competing requirements together.

There used to be @asyncHandler, but this got nixed, presumably in favor of just doing Task { @MainActor in ... }, but who knows.

There are discussions here about using @MainActor Tasks from these contexts, akin to DispatchQueue.async, but AFAIK they do not seem to solve the core issue, which is that doing so destroys the timing guarantees/stack context that these overrides rely on. (I'm also curious why detached tasks are recommended here rather than regular tasks which are expected to inherit the active actor, assumed to be the @MainActor?).

2 Likes

I can't shake the thought that perhaps ideally any @MainActor function should be allowed to have await keywords directly in its body despite not being itself marked as async - with the assumption that doing so would simply run the operation on the main actor and suspend the main actor for other actors as-required.

    @MainActor
    override func viewDidLoad() {
        super.viewDidLoad()

        self.textField.text = await self.model.name
    }

This would make it so much easier to integrate asynchronous functions into existing synchronous paradigms; where there are common requirements that the synchronous function be suspended until data lookups complete in order to populate the view correctly before rendering.

I'd love to learn what, if any, issues exist that prevent us from adding this to the language.

I am also rewriting some UIKit code to use async/await instead of callbacks, and I am running into this problem. Again, this is not about waiting for asynchronous results, it's about allowing the initial synchronous part of a Task to run synchronously up until the first suspension point.

Basically, to illustrate, this is well-defined:

print("1")
await printTwo()
print("3")

func test() async {
    print("2")
    await something()
    whatever()
}

It will print 1, 2 and then 3. However, if we are in a synchronous context, we need to add a Task:

print("1")
Task { await printTwo() }
print("3")

func test() async {
    print("2")
    await something()
    whatever()
}

This can print either 1, 2, 3 or 1, 3, 2, depending on scheduling. It would be extremely nice if both of them were defined to always print 1, 2, 3.

To illustrate why this is so useful in UIKit:

@objc func buttonPressed() {
    imageView.image = nil
    Task { imageView.image = try await loadImage() }
}

func loadImage() async -> UIImage {
    if let image = cache.getImage() {
        return image
    } else {
        return try await loadImageFromNetwork()
    }
}

What we want here is that if the image is in fact available in the fast synchronous cache, imageView.image should be set to nil and then synchronously set to a new image. What actually happens is that imageView.image is set to nil, then the loading task is enqueued, and buttonPressed() exits, and on a later run loop, imageView.image is set to the cached image. This is visible to the user as a quick flicker of the image.

5 Likes

I'd usually avoid reviving an old thread but I wanted to share a solution here given that I'm probably not the only one who found this page while searching the web for this problem.

As several people pointed out, you can't always run an async task synchronously because it may suspend, during which time blocking the synchronous thread would violate the forward progress invariant. However it IS legal within the rules of Concurrency to start a task synchronously; that is, to execute the first Job immediately, assuming you're already on the correct executor. The only hurdle is that by default, Task.init uses the global executor to enqueue new Jobs, which always causes the job to run on the next tick (NB: not 100% sure of the terminology here so if someone more knowledgeable finds that I mixed up a few terms please correct me.) Eventually Swift will probably have a kosher solution for solving this in the form of Task Executors:

Luckily the Swift runtime already includes a tool to override the global enqueue implementation: https://github.com/apple/swift/blob/98e65d015979c7b5a58a6ecf2d8598a6f7c85794/stdlib/public/Concurrency/GlobalExecutor.cpp#L107

EDIT: In the meanwhile, as it so happens, there's already SPI that does exactly this for us, though the usual warnings around private API usage apply.

With some tinkering, you can build something that utilizes this: see Run tasks synchronously on the Main Actor: https://forums.swift.org/t/async-await-is-it-possible-to-start-a-task-on-mainactor-synchronously/52862/23 · GitHub.

And accordingly, the original problem can be solved:

func calledOnTheMainThread() /* non async */ {
  let task = Task.startOnMainActor {
    print("started")
    try? await Task.sleep(for: .seconds(5))
    print("ended")
  }
  // "started" is printed synchronously!
}
6 Likes