[Concurrency] Asynchronous functions

What happens if someone does it the other way? I could write a synchronous function in an actor, and then provide an asynchronous version that can be called from outside the actor. If the asynchronous function attempts to call the synchronous one as its implementation, it’ll likely call itself instead if the have the same name.

Also, if you are in an asynchronous context, you could still prefer the synchronous version to avoid adding a suspension point that would allow other tasks to change the state mid-way in your function. (I realize it would be better to only block tasks for this actor without blocking the whole thread, had we a way to do than.)

We should consider making await imply throws. This would declutter the call site. Async operations are often throwing:

  • loading web resources: definitely throwing
  • file IO: usually throwing
  • costly operations: can go either way
  • UI actions: the only case I can think of right now that cannot be throwing

This was discussed in previous threads and the response was pretty positive. What are the benefits of proposed syntax?

1 Like

Does it mean that, without considering actors, writing something like :

func onRefreshButtonClick() {
self.data = await jsonAPI.fetchData()
tableView.reloadData()
}

from a viewcontroller running on the main thread is a very bad idea , because by default we have no guarantee that the fetchData() api will return on the main thread ?

This is nothing new per say, and a closure callback would have the same drawback. However the word "await" in itself probably implies that the function code will actually "resume" where it was (thread included).

I may have misunderstood the proposal, and i'm clearly not knowledgeable of implementation details, but wouldn't it be safer to have await always returns to the original thread by default ? (I understood that this feature is meant to work together with actors, which makes things more explicit, but i believe the feature should work nicely on its own)

1 Like

To “get” how this all will work forget for a moment that threads exist :slightly_smiling_face:

What code that runs like what you said would look like is this:

@MainActor // or UIActor tho I lean to saying “Main” but maybe we’d alias “UI” too?
func onRefreshButtonClick() async { // must be async to await on anything
    self.data = await jsonAPI.fetchData()
    tableView.reloadData()
}

As such, this function must respect actor rules — the actor may only execute on its executor; The executor happens to be the “main” or “ui” one, which happens to be the “main/ui” thread. But semantically all we care about is “actor code always executes on the right executor” and some actors have special executors.

So to answer your question — execution (tableView.reloadData()) will resume on the right actor/executor/(internally: thread). and there’s no need make any extra steps to make it do the right thing :+1:

Hope this helps,

// edit 1: fixed typos

5 Likes

I stumbled upon some interesting case about overloading:

actor class X {
    func foo() {
        // Do some work
    }

    // Async wrapper
    func foo() async {
        foo() // Error: call is not marked with 'await'
    }
}

You cannot wrap sync foo inside an async function of the same name (to allow access from outside of actor, mentioned in another thread) because the compiler (current toolchain) is thinking that I intend to use the async function, which would cause infinite-loop. Maybe we can make sure that it doesn't do that but that's a very sharp edge.


What's the semantic for await-ing on actor-isolated async method?

@MainActor
func foo() async {
  let a = await task1()
  let b = await task2()
}

Does it need to jump back to MainActor between a and b? What happens if both task1 and task2 are actor-isolated?

2 Likes

The rule is simple: Each actor must execute on its executor, this is how the model achieves thread safety (and sanity). There if the caller, task1 and task2 are on different actors, there would be hops back and forth, yes — that’s semantically what was asked for by such code. If they’re all the same actor, there’s no hops.

These hops can sometimes be optimized away, under special circumstances, but semantically think about it the way as phrased above.

This is kind of the wrong thread for it... let’s keep actors chat to the actors thread please if we’d like to continue this thread.

Does this mean await /async isn't meant to be used outside actors ?

async functions can absolutely be used outside of actors. However, the assumption is that actors are what impose correctness requirements on where code must be running, so if a function isn’t an actor function, there must not be any such requirements (until the function returns, at any rate).

6 Likes

Is it possible to call an async function without await-ing if am not interested in the returned value (or don’t care about the proper continuation flow ) - i.e. kind of a oneway semantics?

We intentionally do not want to make that very easy because it undermines structured concurrency, but you can create a detached task to do it, and the as-yet-unpitched asyncHandler feature is meant to support certain use-patterns that typically favor this.

2 Likes

In the current design all async functions have to be in an async context. Is there a way to design an scape hatch to block results of awaiting async function in a way allowing synchronous function call async functions like in dotNet’s Task.Result [1]

Similar to what we do with throwing functions with try?; could we have await? that wraps async functions results into a Result?

[1]

Aha, I didn't understand that at all. I agree with you that this is a lot more predictable, and has advantages for cases where you want to introduce async versions of existing methods (but see @Lantua's comment above)

Ok, I agree that this is a useful thing to be able to do. Have you considered tightly scoping this? You could have an attribute (similar but different to @ _disfavoredOverload) that enables the otherwise-forbidden overload for just this one case with effectively the same semantics you describe, but more narrowly specified.

Got it, I still think subtyping is important, but I agree that the closure-expression case isn't the important example.

Oh wow, didn't know that. I agree with you -- adding an Async suffix (like C# does) seems like a much more reasonable disambiguation here.

Thanks Doug!

-Chris

I have to say that I'm really excited about this feature overall and think that the API for composing async functions looks great. Even in the full proposal, I don't see anything about how async functions interact with other times of asynchronous code, though.

How can an async function be made that interacts with closure-based APIs or Future based APIs like SwiftNIO? In other languages with async / await, at least ones that I'm familiar with, they all have some kind of Future type that is returned from async functions, which has a clear way of being created from a closure or other future / promise types, but I'm having trouble imagining how it would look when the async nature is part of the function signature but not the return type.

I'm particularly interested in how async / await works with NIO's EventLoopFuture, because server side swift currently suffers from the same pyramid of death problems noted in the proposal.

Hi Marcus,

This is covered in the full proposal on structured concurrency, the last section in Structured concurrency: Low-level code and integrating with legacy APis with UnsafeContinuation, please have a look.

Please refer to this thread on NIO adoption of the concurrency features: Future of Swift-NIO in light of Concurrency Roadmap

Long story short: there are a few "steps" in adoption and yes this proposal should solve the "pyramid of doom" issues you allude to in server-side frameworks.

Future values which are free to "pass around" undermine structured-concurrency and thus are a bit discouraged by this design. Please have a look at the structured concurrency design to get a feeling for this.

"Futures" still exist though -- they are called Task.Handle but should be used rarely because they miss out on some cool automatic context/priority/deadline propagation as well as bounded concurrency features.

5 Likes

Asynchronous programs that need to do intense computation should generally run it in a separate context.

It would be helpful to see an example of how to implement this. Do you mean to suggest using a detached task? I think having guidelines about when those are appropriate would be useful. I'm especially concerned about scenarios where they should be used and people don't use them because they are less convenient.

When that’s not feasible, there will be library facilities to artificially suspend and allow other operations to be interleaved.

In most cases, wouldn't a well-behaved long-running computation periodically check for cancellation? I imagine a convenient way to implement both cancellation checking and suspension would be something like func throwIfCancelled() async throws. Calling this function periodically would both introduce a suspension point and abort if the result is no longer required.

Further, it is necessary if we choose implicit task cancellation at suspension points (something I think is a good idea).

The compiler is working as the proposal states it should. The shortest trick if you want to implement your asynchronous function in terms of a synchronous one with the same signature---which I'm honestly a bit dubious about in the first place---is to wrap the call in a (non-async) closure, e.g.,

{ foo() }()

We could probably come up with a clearer way.

Doug

This exists and is called checkCancellation(), please refer to the structured concurrency proposal: https://github.com/DougGregor/swift-evolution/blob/structured-concurrency/proposals/nnnn-structured-concurrency.md#cancellation

Rationale: This order restriction is arbitrary, but it's not harmful, and it eliminates the potential for stylistic debates.

I'm not sure about this for two reasons. Firstly, I believe it's unprecedented in the language to require that a semantically unordered list be placed in an arbitrary order. These concerns are currently, and probably rightfully, left to a linter to enforce. e.g. There's no enforced order for attributes like these:

class C {
  @discardableResult @inlinable @objc func f() -> Int { 0 }
  @objc @inlinable @discardableResult func g() -> Int { 0 }
  // … etc
}

Secondly, there's no proposed general principle here, just “the order is async throws”, which makes it unclear if/how to apply it to other parts of the language (like the above example), hard to remember, and hard to extend in future. What do you do if there's a new word that can go in this position, like a new keyword or a user-defined token (e.g. I think this is where a general effects system would live)? Would the rule be “alphabetical order”? Does this apply to both the definition and the call site? What if the call site words alphabetise in a different order than the definition site words?

I would prefer that these semantically unordered lists of attributes/keywords remain unordered for simplicity.

3 Likes

Protocol compositions are an example of where the language already requires an order purely for style reasons. Protocols can go in any order, but if it includes a class, the class must go first. Class & Protocol is allowed, but Protocol & Class is not.

8 Likes

Good point. A counterpoint is that you can only inherit from a single class but can conform to multiple protocols, so it might be unordered but there are two clear groups of things to distinguish. You don't have to specify all the protocols in any particular order there, which is perhaps more comparable.

3 Likes
Terms of Service

Privacy Policy

Cookie Policy