project-ideas icon indicating copy to clipboard operation
project-ideas copied to clipboard

async / await in dmd!

Open zoujiaqing opened this issue 5 years ago • 20 comments

We need it! C# / Rust / Python / TypeScript and more language is supported.

zoujiaqing avatar Feb 22 '20 23:02 zoujiaqing

This needs a proper description: What exactly do we need, and why ? What would it require in terms of druntime support, etc...

Geod24 avatar Feb 23 '20 10:02 Geod24

Making a simple request

// No await
string fetch()
{
    asyncFetch("https://jsonplaceholder.typicode.com/todos", void(Response response)
            {
                string data = response.body();
            });
    // How to return data?
}

// Using await
string fetch()
{
    auto response = await asyncFetch("https://jsonplaceholder.typicode.com/todos");
    return response.body();
}

TypeScript Example: https://www.carlrippon.com/fetch-with-async-await-and-typescript/

Makeing a simple database query

// No await
database.asyncQuery("SELECT customer_id, amount, account_name FROM payment", void(Result result) {
    // result is here ..
});

// Using await
auto result = await database.asyncQuery("SELECT customer_id, amount, account_name FROM payment");

zoujiaqing avatar Mar 02 '20 14:03 zoujiaqing

await in TypeScript example

not use await:

const start = callback => {
    setTimeout(() => {
        callback('Hello');
        setTimeout(() => {
            callback('And Welcome');
            setTimeout(() => {
                callback('To Async Await Using TypeScript');
            }, 1000);
        }, 1000);
    }, 1000);
};

start(text => console.log(text));

use await:

const startAsync = async callback => {
    await wait(1000);
    callback('Hello');
    await wait(1000);
    callback('And Welcome');
    await wait(1000);
    callback('To Async Await Using TypeScript');
};

startAsync(text => console.log(text));

zoujiaqing avatar Mar 02 '20 14:03 zoujiaqing

async/await is stackless coroutines with red-blue problem. It's a crutch for runtimes without stackfull coroutines support.

D supports stackfull coroutines (fibers). Its don't have red-blue problem and don't need the special syntax to switch between tasks.

See:

nin-jin avatar Jun 14 '20 12:06 nin-jin

@nin-jin then how about https://github.com/seeseemelk/dawait which is built on top of Fiber...especially the await implementation?

import std.stdio;

int calculateTheAnswer() {
	import core.thread : Thread;
	Thread.sleep(5.seconds);
	return 42;
}

void doTask() {
	writeln("Calculating the answer to life, the universe, and everything...");
	int answer = await(calculateTheAnswer());
	writeln("The answer is: ", answer);
}

void main() {
	startScheduler({
		doTask();
	});
}

aberba avatar Aug 09 '20 23:08 aberba

alias async = spawnLinked;
alias await = yield;
startScheduler( void delegate() callback ){
    scheduler = new FiberScheduler;
    scheduler.start( callback );
}

:-D

nin-jin avatar Aug 09 '20 23:08 nin-jin

So stackless coroutines is slower than synchronous code. See the JS results: https://t.co/YSELe37bGX?amp=1

image

nin-jin avatar Aug 09 '20 23:08 nin-jin

I also wanted async/await, so I wrote this toy library a while ago.

https://github.com/lempiji/libfuture

I used Fiber and it looks like Rust.

What I found out when I built it is that I don't have a mechanism to handle waiting well, and I can't make it as usable as Task in C#.

If there is a better way to do this I would like to know too.

(This is a machine translation and I'm not very good at English. Sorry if I can't reply.)

lempiji avatar Aug 10 '20 13:08 lempiji

So stackless coroutines is slower than synchronous code. See the JS results: https://t.co/YSELe37bGX?amp=1

Sorry, benchmarks like that do not make sense. Coroutines are not made for millions of invocations per second. There is quite some allocation and synchronization overhead, so it is no surprise that this performs worse. Nearly every feature can be used in a wrong way, leading to bad performance. Coroutines are made for IO, e.g. fetch data from the web and get async result when the answer is there. Read a file and await data. Read stuff from a database. In these scenarios, the little synchronization overhead is neglectable. It can even improve efficiency, because you do not have threads spin waiting or sleeping/polling for results. You just request data, and you get a callback when it is there. No busy waiting, wasting CPU cycles. No more frozen UIs.

Having coroutines makes writing async code a whole lot easier and cleaner. It was the introduction of coroutines that really kicked off async IO usage in the supporting programming languages. More and more languages are supporting it. It would be a very good addition in the D language as well.

lukasf avatar Nov 29 '22 10:11 lukasf

Coroutines are stackfull and stackless. The first ones are already in the D-runtime in. Here we are discussing the second. Like a virus, they lead to the cascading transformation of a bunch of ordinary functions into "asynchronous" ones, each call of which gives significant overhead both in memory and in the processor.

nin-jin avatar Nov 29 '22 15:11 nin-jin

The perf problem can be solved by having the function be perfectly regular function under the hood, and have the runtime lazily duplicate the stack on demand when something actually waits.Not sure how suitable it is for D, but this is a common way of doing it (for instance, this is how HHVM does it).

deadalnix avatar Nov 29 '22 15:11 deadalnix

The perf problem can be solved by having the function be perfectly regular function under the hood, and have the runtime lazily duplicate the stack on demand when something actually waits .Not sure how suitable it is for D, but this is a common way of doing it (for instance, this is how HHVM does it).

This cannot be a regular function, since it does not return the value itself, but the awaitable/promise/future/task, which stores subscribers and the result that provides each await.

nin-jin avatar Nov 29 '22 16:11 nin-jin

I think you might want to let the HHVM guys know, then. Clearly, you have a lot to teach them.

deadalnix avatar Nov 29 '22 16:11 deadalnix

I think you might want to let the HHVM guys know, then. Clearly, you have a lot to teach them.

A short look at HHVM shows that any async method has a return type of awaitable<T> plus the "async" keyword. These are clearly not normal method but special async method, most probably very similar to how any other async/await capable language does. You can have normal methods in HHVM without awaitable and async keyword (and they will sure be faster when calling them a million times without doing any work in them). No matter how the async state handling is done internally, this always has some overhead for special handling, that is not neccessary for synchronous methods. And that is absolutely fine, because you will never use async methods in high performance areas (actual number crunching vs. waiting for slow data from web/storage/database).

lukasf avatar Nov 29 '22 17:11 lukasf

Coroutines are stackfull and stackless. The first ones are already in the D-runtime in. Here we are discussing the second. Like a virus, they lead to the cascading transformation of a bunch of ordinary functions into "asynchronous" ones, each call of which gives significant overhead both in memory and in the processor.

You only need async methods on high level application APIs and IO APIs. There, the overhead is totally irrelevant. If you wait a second for data from the web to arrive, or you save data to your hard disk, it simply does not matter at all, if you need a few more bytes or spend some ns fraction for synchronization. And as I said, you will even save some CPU cycles because your thread won't be busy waiting for data (but sure, you spend some additional cycles on synchronization).

Saying that async is evil is like telling me that classes are evil, just because you can create an artificial scenario where classes perform much worse than working with raw pointers on binary data. Sure you can prove that they are slower due to virtual function tables and stuff like that. But this does not mean that classes are a bad. It just means that in a few very performance critical areas you should not use them, at least if you need to squeeze out the very last bit of performance. So for a number crunching lib, you should not use classes, and you should not use async/await. But for the high level application code, both are absolutely fine and will not cause any noticeable performance impact, because the number of invocations per seconds are super low (you only save that file once, you download that data only once - not a million times a second).

As always, you need to know your tools and use them wisely. And I think async/await is a fantastic tool for writing non-blocking high-level application code. Unless you do something seriously wrong, the performance impact won't even be measurable, at least not noticeable. The benefits of non-blocking behavior, much simpler, much cleaner code, greatly simplified error handling are worth a lot as well. Performance is not the only factor when doing technology decisions. Readability and maintainability of code is important as well.

Transitioning existing, synchronous code to async is indeed not fun. But if you create new applications from scratch, this is not an issue, since you can think async first.

lukasf avatar Nov 29 '22 18:11 lukasf

A short look at HHVM shows that any async method has a return type of awaitable<T> plus the "async" keyword.

A longer look would have informed you that compilers, and especially optimizers, do all kind of things behind your back, which allow to present a convenient model of the world to the user while actually being fast.

deadalnix avatar Nov 29 '22 18:11 deadalnix

A longer look would have informed you that compilers, and especially optimizers, do all kind of things behind your back, which allow to present a convenient model of the world to the user while actually being fast.

You said that their async function are just regular, normal functions. That is not true, that's all I wanted to point out. They might use clever tricks to reduce the overhead. But still, these are not regular functions, and they will come at a cost. Actually, I don't even care how good their performance is, so I don't want to argue here. To me this is just one more framework that has picked up the concept of async/await. Good to see. I really hope that more will follow, because it is a great concept. There are good reasons why it is implemented in more and more tech stacks. Heck, even C++ has it, and they are sure not the kind of guys who introduce a new concept, just because it is the latest shit and trending on twitter.

lukasf avatar Nov 29 '22 19:11 lukasf

What the computer run is a plain regular function with a plain regular return value 99% of the time (gross underestimate).

deadalnix avatar Nov 29 '22 19:11 deadalnix

Okay this discussion does not seem to make sense. A return value of type int is obviously not the same as a return value of type awaitable<int>, where you need to use await and let the framework do its magic to get the actual result. This will be my last post regarding HHVM since this seems to lead nowhere.

lukasf avatar Nov 29 '22 19:11 lukasf

Ok.

struct S  { int i; }

S foo() { return S(42); }

S is not an int, right? But what does the codegen looks lie?

example.S example.foo():
        mov     eax, 42
        ret

See? the compiler doesn't care S is not an int, it's going to strip out all of this and generate a function that returns an int anyways.

Same goes for awaitable. The only place where you need to do something special is when you await on several functions at once, as you need a marker in the stack there, so if one actually stalls, you can memcopy the stack away in a buffer and switch to execute the next one.

deadalnix avatar Nov 29 '22 20:11 deadalnix