Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implement async "yielding" the proper way

Async method runs sync on caller context/thread until its execution path runs into an I/O or similar task which has some waiting involved and then, instead of waiting, it returns to original caller, resuming its continuation later. The question is, what is the preferred way of implementing that "wait" method. How do the File/Network/etc async methods do it?

Lets assume I have a method which will have some waiting involved which is not covered by current IOs out of the box. I do not want to block calling thread and I do not want to force my caller to do a Task.Run() to offload me, I want a clean async/await pattern so that my callers can seamlessly integrate my library and I can run on its context until such time I need to yield. Lets for the sake of argument assume that I want to make a new IO lib which is not covered and I need a way to make all the glue that keeps async together.

Do I Task.Yield and continue? Do I have to do my own Task.Run/Task.Wait, etc? Both seem like more of the same abstractions (which brings the question how does Yield yield). I am curious, because there is a lot of talk about how async/await continuation works for the consumer and how all IO libs come already prepped, but there is very little about how the actual "breaking" point works and how process makers should implement it. How does the code at the end of a sync path actually release control and how the method operates at that point and after.

like image 803
mmix Avatar asked May 03 '26 20:05

mmix


1 Answers

If you're the bottom of the async pile, with no inbuilt async downstream calls to defer to, then: it falls to you. The simple way to do this is to allocate a TaskCompletionSource<T> (TCS) for some T, hook up the async work (that isn't Task<T> based) in whatever way you need to, stick the TCS somewhere you can get at it later, and hand back the .Task from the TCS, to the caller. When the async work completes - possibly via some kind of callback, or whatever is suitable for that API; fetch the TCS from where-ever you stuffed it, and signal completion there, via TrySetResult etc.

There are various things to consider, though:

  • in many cases, you may want to ensure that you pass TaskCreationOptions.RunContinuationsAsynchronously to the TCS constructor, if "thread theft" would be a huge concern (otherwise, the await steals the thread of whatever calls .TrySetResult)
  • there are ways of creating and managing Task[<T>] instances without the additional allocation of a TaskCompletionSource<T>, but they're more advanced
  • or at the extreme end, if this is high throughput: ValueTask[<T>] has a token-based API (via IValueTaskSource[<T>]) that allows the same object model to be used many times (as different ValueTask[<T>] values), to avoid any additional allocations - again, this is an advanced scenario
like image 52
Marc Gravell Avatar answered May 05 '26 11:05

Marc Gravell



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!