Greg,

On 28 March 2015 at 15:17, Greg Keogh <[email protected]> wrote:
> Folks, I have an existing library with lots of traditional non-async methods
> in it, and I want to provide async versions of the old methods. Would you
> consider this to be a simple and trustworthy way of getting this done?
>
> public Thing GetThing(int key)
> {
>   // This is the existing method
>   return ...
> }
>
> public Task<Thing> GetThingAsync(int key)
> {
>   return Task.Run(() =>
>   {
>      return GetThing(key);
>   }
> }
>
> So I just make matching pairs where the old methods are just wrapped in
> Task.Run(...). It seems too easy. This is Framework 4.5, and I have a vague
> recall that Task.Run doesn't work this easily in 4.0 and it's a bit more
> verbose.

On 12 April 2015 at 07:30, Tristan Reeves <[email protected]> wrote:
> Also to obtain any *actual* benefit from an async counterpart the
> implementation more than likely has to change. For example, a sync method
> might use the synchronous File API, whereas an async counterpart can gain
> benefit by using the async File API.

Sorry for resurrecting an old thread, but I recently did some async
work and I thought of this question.

You are right. Wrapping synchronous methods in Task.Run is "too easy".
The problem is I/O.

One important thing I learned about async/await is that it works best
when all the I/O in your stack is asynchronous, as Tristan mentioned.
If you perform synchronous, blocking I/O in the delegate passed to
Task.Run, a pool thread will likely end up blocked, waiting on I/O.
This is a bad thing.

In highly concurrent workloads, blocked pool threads can lead to
scalability issues and eventually resource starvation of the thread
pool (see Jeffrey Richter's book, CLR via C#, chapter 28 "I/O-Bound
Asynchronous Operations"). I have also observed the runtime limiting
the rate at which it enlarges the pool.

Basically, one should never wait in a pool thread. Unfortunately this
means you probably shouldn't implement GetThingAsync with GetThing.
And unfortunately this also means the asynchronous pattern "infects"
all methods "down" the call stack. That is, all the methods
transitively called by GetThingAsync should use asynchronous I/O and
never wait. See the section "Async All the Way" in this[1] MSDN
magazine article.

Corollary: You should also favour lock-free data structures and
continuations over blocking synchronisation primitives when
synchronising tasks running on pool threads. This means avoiding the
"lock" keyword, Mutexes, AutoResetEvents and other WaitHandles as
these can all put a pool thread to sleep.

[1] https://msdn.microsoft.com/en-us/magazine/jj991977.aspx

--
Thomas Koster

Reply via email to