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.
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:
TaskCreationOptions.RunContinuationsAsynchronously to the TCS constructor, if "thread theft" would be a huge concern (otherwise, the await steals the thread of whatever calls .TrySetResult)Task[<T>] instances without the additional allocation of a TaskCompletionSource<T>, but they're more advancedValueTask[<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 scenarioIf you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With