On 8 December 2015 at 11:12, Greg Keogh <[email protected]> wrote:
> Here's a really late reply to a really old thread re making a library async
> by simply wrapping "plain" calls with Task.Run. Yes, Richter describes
> nicely how you shouldn't block in worker threads doing I/O as you "pollute
> the pool".
>
> The trouble is, sometimes you don't know what a library is doing down the
> call stack. I dredged out my old (now defunct) library and realised it's
> calling library methods which in turn make web calls to cloud providers
> (Rackspace, Azure, etc). I just happen to know that's happening further
> down, but what if I didn't?! There is danger that you may write ugly or
> inefficient software because you don't know what's happening all down the
> line. I suppose this is always a danger when calling "black box" libraries,
> unless they're well documented or carefully written. Even now I'm not sure
> if my old code like this pollutes the pool:
>
> return Task.Run(() =>
> {
>  return GetSomething(args); // This makes a REST call at the lowest level
> }
>
> Someone might be blocking down the stack, I can't tell.

That's life, in .NET anyway.

Asynchronous patterns leak upwards to callers. Even if a method uses
asynchronous methods internally, if that method is not itself
asynchronous, then either:

* it still blocks somewhere (perhaps on Task.Wait/Task.Result), or
* it starts an asynchronous task but never uses the result, which is
  kind of silly from a reliability point of view, like
  "catch (Exception) { return; }".

This means that if a method cannot possibly do its job without I/O, and
no asynchronous version is provided, then it's a pretty safe bet that it
blocks somewhere. Time to file a bug report.

Basically, you just have to guess and hope for the best. Don't forget
that logging is I/O too.

Note that none of this is likely to matter to desktop apps that only use
async/await for a few background jobs, as you will not likely explode
your thread pool in this scenario. It matters more to services that
handle high volumes of network requests. For example, if request
handlers block on database queries, a spike in requests can lead to
stalls as the thread pool suddenly needs to grow substantially.

Through "perfect play" with async/await, your process will not use more
OS threads than you have physical cores (modulo hyperthreading, and not
counting administrative threads like GC), irrespective of the number of
I/O operations the process is waiting for.

If you think about it, I/O is naturally asynchronous and the OS is
already working in an event/callback way. The simple, synchronous APIs
we know from C and their analogues in the .NET Framework are
artificial.

Ideally, the CLR should implement lightweight threads, a new I/O
scheduler to schedule them, and a new System.IO that uses *only*
asynchronous calls internally. With these three things, concurrency on
the scale of Erlang/Go/Haskell will become possible in .NET. We already
have the async/await syntax to take full advantage of this.
Unfortunately, the Framework appears to rely heavily on thread-local
storage (security principal, current culture, ASP.NET context, etc).
Thread-local storage is generally incompatible with lightweight threads,
so I doubt this will happen any time soon. (You already need to be very
careful about thread-local storage when using System.Threading.Tasks.)

--
Thomas Koster

Reply via email to