Jump to page: 1 2
Thread overview
[Semi-OT] Fibers vs. Async / Await
May 11, 2022
René Zwanenburg
May 11, 2022
rikki cattermole
May 11, 2022
Ali Çehreli
May 12, 2022
rikki cattermole
May 12, 2022
René Zwanenburg
May 12, 2022
rikki cattermole
May 12, 2022
bauss
May 12, 2022
René Zwanenburg
May 13, 2022
bauss
May 12, 2022
Sebastiaan Koppe
May 12, 2022
IGotD-
May 12, 2022
rikki cattermole
May 12, 2022
René Zwanenburg
May 12, 2022
rikki cattermole
May 11, 2022

The suggestion of adding async / await to the language comes up with some frequency, with fibers being seen as a lackluster alternative.

Now, I feel like I must be missing something obvious, as fibers seem like the better option in almost every aspect to me. I'll compare them to async / await in .Net since that's the implementation I'm most familiar with.

The issues I have with a/a:

  1. When a function wants to do something asynchronous every function up the call stack needs to be in on it. This is especially problematic with generic code and higher-order functions. Even with D's powerful metaprogramming facilities I don't see a way to handle this nicely. As an example: the whole IEnumerable / IAsyncEnumerable situation in .Net is a manifestation of this problem.

  2. It messes up your callstack. This leads to problems with:

  • Stack traces
  • Debugging
  • Memory dumps
  • Sampling profilers
  • And probably more that I haven't run into yet.

Now, some tooling can kinda sorta reconstruct a usable stack, but all the efforts I've seen so far are still harder to use than when you have a proper stack.

  1. I don't like the Task<T> and await noise everywhere. It clutters up the code.

  2. It's too easy to make a mistake and forget an await somewhere. Yes, an IDE will likely give a warning. No, I still don't like it ;)

  3. It can put too much pressure on the GC. This won't be a problem when doing an HTTP request or something like that. It is a problem when there's an allocation for reading every single value in a DB query result.

All the above problems don't exist when using fibers. The only downside of fibers I'm aware of is that you have a full stack allocated for every fiber. This can cost a lot of (virtual) memory, but on the other hand it makes for very effective pooling. And if you're in a situation where this really is a problem you can tweak the stack size. Seems like a small price to pay to me.

Am I missing something? Thank you for your time.

May 12, 2022
On 12/05/2022 1:27 AM, René Zwanenburg wrote:
> All the above problems don't exist when using fibers. The only downside of fibers I'm aware of is that you have a full stack allocated for every fiber. This can cost a lot of (virtual) memory, but on the other hand it makes for very effective pooling. And if you're in a situation where this really is a problem you can tweak the stack size. Seems like a small price to pay to me.

Don't forget the extra cost of having to scan those stacks regardless of those fibers state.

The context state required for a task will be a lot smaller (and hence cheaper) than what a single fiber stack will cost to scan.
May 11, 2022
On 5/11/22 06:49, rikki cattermole wrote:

> The context state required for a task will be a lot smaller (and hence
> cheaper) than what a single fiber stack will cost to scan.

While were on it, what are the blockers for stackless fibers for D?

Ali

May 12, 2022
On 12/05/2022 8:15 AM, Ali Çehreli wrote:
> On 5/11/22 06:49, rikki cattermole wrote:
> 
>  > The context state required for a task will be a lot smaller (and hence
>  > cheaper) than what a single fiber stack will cost to scan.
> 
> While were on it, what are the blockers for stackless fibers for D?
> 
> Ali
> 

I don't know.

Whatever solution we come up with, it must support yielding in say a database driver, while an ORM returns say a Future and have the event loop automatically complete it.
May 12, 2022

On Wednesday, 11 May 2022 at 13:27:48 UTC, René Zwanenburg wrote:

>
  1. I don't like the Task<T> and await noise everywhere. It clutters up the code.

You don't have to have a "Task" type that you declare, it could simply be T and then the function is just marked async and T automatically becomes Task!T.

The reason why C# doesn't do it that way is because they envisioned the ability to add your own Task types etc. but that has never been implemented or anything, so it's really just verbose.

>
  1. It's too easy to make a mistake and forget an await somewhere. Yes, an IDE will likely give a warning. No, I still don't like it ;)

It's easy to fix, don't allow implicit conversions between Task!T and T and thus whenever you attempt to use the result you're forced to await it because otherwise you'd get an error from the compiler due to mismatching types.

Like the below would definitely fail:

async int getNumberAsync();

...

auto number = getNumberAsync();

int otherNumber = 10 * number; // number cannot be used here due to it not being implicitly convertible to int. The fix would be to await getNumberAsync().
>
  1. It can put too much pressure on the GC. This won't be a problem when doing an HTTP request or something like that. It is a problem when there's an allocation for reading every single value in a DB query result.

It doesn't really put anymore pressure on the GC. The compiler converts your code to a state machine and that hardly adds any overhead. It makes your functions somewhat linear, that's it. It doesn't require many more allocations, at least not more than fibers.

May 12, 2022

On Wednesday, 11 May 2022 at 13:27:48 UTC, René Zwanenburg wrote:

>

Am I missing something? Thank you for your time.

Don't forget that fibers aren't supported on all platforms.

They are definitely interesting but I don't think they should be the basis of everything.

May 12, 2022

On Thursday, 12 May 2022 at 08:49:18 UTC, Sebastiaan Koppe wrote:

>

Don't forget that fibers aren't supported on all platforms.

They are definitely interesting but I don't think they should be the basis of everything.

That was one of my observations when I looked in the druntime source code. The D runtime has basically implemented all of the scheduling itself, including all context switching. This means that it has to be implemented for all architectures, and variants of all architectures which is a lot. Many platforms like Windows has its own fiber API and I was surprised that druntime didn't use that one.

I'm not saying that it is wrong to do the entire implementation in druntime but it is a lot of work and only the most common CPU architectures will be supported. There is also the risk that it becomes outdated as CPU vendors add stuff to their CPUs.

I think since the D project has such limited resources, it should go for as generic solutions and reuse existing APIs. Async/Await is becoming accepted as a programming model and I think that D should put its effort to support that.

May 12, 2022
On 12/05/2022 9:16 PM, IGotD- wrote:
> I'm not saying that it is wrong to do the entire implementation in druntime but it is a lot of work and only the most common CPU architectures will be supported. There is also the risk that it becomes outdated as CPU vendors add stuff to their CPUs.
> 
> I think since the D project has such limited resources, it should go for as generic solutions and reuse existing APIs. Async/Await is becoming accepted as a programming model and I think that D should put its effort to support that.

The cost to maintain our fiber implementation is very minimal.

Prior to the refactor 3 years ago, a whole pile of the context switch assembly was last touched like 8-13 years ago.

https://github.com/dlang/druntime/blame/3ead62a9bf4e0f866af10fdd3bc4edeb87237305/src/core/thread.d#L3754

ABI's are generally stable for this stuff. If they weren't a lot of userland would break and there would be no way to fix things.
May 12, 2022
On Wednesday, 11 May 2022 at 13:49:28 UTC, rikki cattermole wrote:
> Don't forget the extra cost of having to scan those stacks regardless of those fibers state.
>
> The context state required for a task will be a lot smaller (and hence cheaper) than what a single fiber stack will cost to scan.

That's a good point. As an improvement, could we mark the unused part of the fiber's stack as NO_SCAN when it yields? And ofc undo it when the fiber switches back in. Or would that be too heavy an operation?
May 12, 2022

On Thursday, 12 May 2022 at 06:04:06 UTC, bauss wrote:

>

You don't have to have a "Task" type that you declare, it could simply be T and then the function is just marked async and T automatically becomes Task!T.

That would help a little. Doesn't get rid of the await though.

>

It's easy to fix, don't allow implicit conversions between Task!T and T and thus whenever you attempt to use the result you're forced to await it because otherwise you'd get an error from the compiler due to mismatching types.

Right. .Net doesn't do implicit conversion either, I was thinking of functions that are void / just Task, like writing to a database. Assuming you use exceptions to report problems.

>

It doesn't really put anymore pressure on the GC. The compiler converts your code to a state machine and that hardly adds any overhead. It makes your functions somewhat linear, that's it. It doesn't require many more allocations, at least not more than fibers.

Are you sure about this? The state machine needs to be stored somewhere. I'd think we would need something similar to a delegate: a fixed-size structure that can be passed around, with a pointer to a variable sized context / state machine living on the heap.

« First   ‹ Prev
1 2