3

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.

13
  • You might want to read There is no thread and realise it's async all the way down. Commented Oct 8, 2021 at 9:36
  • Does this answer your question? How do I implement an async I/O bound operation from scratch? Commented Oct 8, 2021 at 9:40
  • Does this answer your question? Write your own async method Commented Oct 8, 2021 at 9:45
  • Would be interesting to know what is that waiting not covered by current IO out of the box. But in general I think you'll have to just use native OS api anyway. Commented Oct 8, 2021 at 9:45
  • On Windows I think they use IOCP for IO bound work (I think). I also think other asynchronous operations use task schedulers backed by the thread pool to efficiently schedule multiple tasks on a single thread. Commented Oct 8, 2021 at 9:47

1 Answer 1

6

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
Sign up to request clarification or add additional context in comments.

3 Comments

Cool, this is actually what I have been looking for. Thanks for pointing me in the right direction, now I have to see how all this fares on Linux, but other than hardware specific P/I this looks like a solution... I'll as here if I run into trouble :D
Btw, you said this is the simple way? What would the complicated way be (I presume with creating and managing Task instances you mentioned)? Can you point me towards some reading material?
@mmix there are a few APIs you can use; AsyncMethodBuilder<T> can do this for you, although how that happens depends on the framework version; or there's some related tools in the PooledAwait library on nuget - but if allocations "matters* (often it doesn't), then moving to ValueTask[<T>] with IValueTaskSource[<T>] might be a more practical direction

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.