Swift Concurrency Roadmap

One of the primary benefits of async/await when writing and reading code is to avoid that pyramid of doom, with several nested completion handlers. And if you want some of the tasks to execute in parallel, it gets even more complex, with having to use something like DispatchGroup. There's a lot of cognitive overhead.

You are correct in that you can accomplish what async/await gives us using existing mechanisms. What you appear to be missing is all the advantages this specific solution brings.

4 Likes

Well, yes. That's why I asked how is this different from cps.

APIs like dispatch hide much of complexity already. And if it is about making new types in foundation vs making new language feature bloated with keywords made specifically for it, then I strongly in favor of former option, since the overhead is actually lower, because no new things have to be learned, just old ones have to be mastered.


Yeah, I might not have asked these questions, if the proposal had a description of how its solution's intrinsics function.


I mostly concerned that something unnecessary is put on the surface. Not only it's one more burden to think around, but it's gonna influence semantics of all future frameworks. I have no idea why asyncs are better than a couple of new dispatch apis and new patterns.
No clear idea about how it should 'just click', because there are already 5 new keywords. And it looks like they don't stop coming; I think it's very far from being simple. The whole idea is made in image of actor model, but it's not the actor model, simply because it doesn't protect from deadlocks.

Also, JS is poor, it shouldn't be a source from which ideas are to be taken.

1 Like

How does the proposed actor model not protect against deadlocks?

Asyncs on actors can return values. You make a code that uses two actors, and A waits data from B, and B awaits data from A. Boom! Deadlock.

No. In the currently-pitched model, if A calls into B, and B calls into A, the second call in A can proceed while the first is suspended, because tasks can be interleaved on the same executor in the async model. The first call can maintain invariants because its call to B is an explicit suspension point, and invariants must hold at potential suspension points. This has been discussed at length.

2 Likes

That's why Python, C#, and Rust are also mentioned, which have async/await built on similar principles.

I only emphasized JavaScript as something in which I have certainty of how it's implemented down to the lowest level. The fact that JavaScript has something, doesn't mean that all ideas are bad. Are you against loops, conditionals, properties, and classes because JavaScript also has them?

Where?

Just against async.

The collection of proposals which describe Swift's concurrency features are about concurrency, not parallelism. Is your issue with the idea of concurrency, or do you simply not feel that concurrency is a desirable language feature? Can you elaborate on your position?

Javascript is in many ways not a great language, but if you're talking about JS in historically single-threaded execution environements (e.g. Node) then it's a brilliant example of exactly how async/await helps.

Imagine you've got a tiny API, and for whatever reason, it can only run on a single thread. That API has to handle lots of requests, but part of it's work involves calling another API that is really, really slow...

func returnFoo(for bar: Bar) -> Foo {
	let result = reallySlowHttpApi.fetch(bar) // #1
	
	return result
}

Lets assume the call to #1 takes 10 seconds. With code as it is, your API can only handle one request at a time, and given each request takes 10 seconds, that means you can handle 0.1 requests per second. Pretty bad. So you rewrite it with async/await...

func returnFooAsync(for bar: Bar) async -> Foo {
	let result = await reallySlowHttpApi.fetchAsync(bar) // #1
	
	return result
}

#1 still takes 10 seconds, but because execution is suspended at the await, your single thread is immediately able to start servicing other requests. An individual request will still take 10 seconds, but you can suddenly handle perhaps thousands of requests concurrently, and your throughput is improved enormously.

How would you do this without async/await?

4 Likes

I use something like this.

import Foundation

let fn: () -> String = {
    var result: String? = nil
    let invocation = { result = "I am done!" }
    let heavy_work = Thread { invocation() }
    heavy_work.start()
    // do anything you need. Task is being performed meanwhile
    // kepp doing something else here

    // at this point you have to await result, because you need it
    // so wait, or dont wait at all if the task is ready.
    // this is identical to async btw
    while !heavy_work.isFinished {
        print("Busy waiting ...")
    }
    return result!
}
print(fn())

The big advantage of my approach is that you can write all the sugar yourself!
It also is much simpler than a bunch of offered keywords from the proposal. And it works already!

Busy waiting is usually not a great idea when working with constrained environment. That's why we moved onto something else like event handler, but event handlers is very restrictive in terms of syntactic freedom. That's partly why Swift is trying to move away from event handler onto async.

4 Likes

Your busy while loop is either going to chew up CPU/battery, or not be at all responsive, and it's also blocking. It's also not single threaded.

I don't see any advantage to it at all, over the async version, even if it met the constraints I gave :man_shrugging:

4 Likes

You can put there actual computation while that task on the current thread is doing other work without blocking, you realize that right? It shouldn't be a noop


Are you talking about embedded, or what?


No, it is equivalent to await from the proposal

It doesn't block anything. This technique is called fork-join parallelism, look it up.

One advantage you don't see is that there is no separation of sync and async domains, while the semantic of concurrent behaviour is present. Which could be important, if only you knew what it means.

And that means what?

This is exactly what we're trying to avoid with async/await. In this example, fn() is blocked, which means that the thread executing fn() is also blocked. You are again conflating concurrency and parallelism.

4 Likes

iOS. Busy-waiting on the main thread will get your app killed if you don't severely limit the wait time.

2 Likes

Unless you poll for the result at intervals, instead of in a tight loop, you will be blocking the thread which is waiting for the result. And in your example, where would you do this polling? What happens when you reach a point where you must have the result before continuing? In today's Swift, you'd use a callback. Obviating the need for callbacks is one of the goals of async/await.

1 Like

It is not literally wasting cycles! A thread can continue to do whatever it pleases, but at some point, it has to wait for resources that were delegated to a pool. It is a fork-join model and it same as await from the proposal.

At a certain point, you must wait to retrieve the result from the pool. It is how it is done. The proposal doesn't provide a better alternative.

Is this better now?

let fn: () -> () -> String = {
    var result: String? = nil
    let invocation = { result = "I am done!" }
    let heavy_work = Thread { invocation() }
    heavy_work.start()
    // do anything you need. Task is being performed meanwhile
    // kepp doing something else here

    return {
        // at this point you have to await result, because you need it
        // so wait, or dont wait at all if the task is ready.
        // this is identical to async btw
        while !heavy_work.isFinished {
            print("Busy waiting ...")
        }
        return result!
    }
}
let run_as_async = fn ()
//keep doing your stuff
//...
// now it is time to end it by any means
print(run_as_async())