I'm expressing my frustration here somewhat, but why do many new libraries only have asynchronous APIs? For example I'm creating a small utility to fetch a web page and parse some data from it. However, making use of HttpClient from the System.Net.Http namespace in .net core requires a lot of async and await boilerplate code, from this web page for example:
async static Task Main(string[] args)
{
}
Then the contents of Main:
HttpClient client = new HttpClient();
var response = await client.GetAsync("http://www.nzherald.co.nz/");
var pageContents = await response.Content.ReadAsStringAsync();
Console.WriteLine(pageContents);
Console.ReadLine();
I feel that c# has become a very wordy language and I'm not happy to have to code in the async style like this. I have been writing c# programs since .net 1.0, and I find it hard to reason about what is happening behind the scenes of the compiler: is creation of threads for async code going to impact performance? How can I throttle the rate of calls if I'm calling GetAsync in a loop?
My Question is: has the c# team gone in the right direction creating this async paradigm in the language? I feel that go's approach of creating a thread by just saying go myfunction()
is less long-winded, but I know it has it's performance penalties due to the green-threads used.
Look at this question for example, it has the same type of concerns about async, and the single answer isn't very helpful.
Best Answer
Oh, but that is not wordy at all. You are not writting something like this:
And an API like the one above (probably a wrapper around
ContinueWith
) it won't handle exceptions properly, you would probably need anError
method with a callback to get the exceptions.No, C# is not wordy.
Addendum: perhaps it is worth mentioning that if an
async
method returns aTask
, it does not means you have toawait
it. If you want theTask
to do something else with it (for example place it an array forTask.WaitAll
orTask.WhenAll
), you do not put theawait
keyword. Therefore, theawait
keyword is not redundant.No. Although some API do actually need to block on a
Thread
– and I think that is the case for listening on a socket, at least on Windows – it will use theThreadPool
, thus there won't be a lot of newThread
s created.That it will use the
ThreadPool
is not true for every API. Most will follow the following pattern:The library creates a
TaskCompletionSource
.The library sets a means to receive a notification. Callback, timer, message, whatever...
The library sets code to react to the notification that will call
SetResult
, orSetException
on theTaskCompletionSource
as appropriate for the notification received.The library does the actual call to the external system.
The library returns
TaskCompletionSource.Task
.The call from step 4 goes to the external system. The external system responds, the response is received on the notification mechanism from step 2, then it sets the
Task
completed, and afterwards the continuation of theTask
is scheduled (that continuation is what happens after yourawait
).If you are awaiting it, then your code will not continue until it completes. Alternatively, if we are talking about having a limited number of
Task
s concurrently (not being awaited), I would suggest to use aSemaphoreSlim
. I would use it in conjunction withTask.WaitAny
orTask.WhenAny
.Besides that, you probably will find
await Task.Delay(milliseconds)
useful. It will use the pattern I described above, around a timer, noThread
will be blocked waiting (unlikeThread.Sleep
).I believe
async/await
is generally good. I agree that there is some overhead (creatingTask
objects, et. al.). However, C# is doing better than most languages.I understand the blue-red argument...
... on that, I would argue that if you want to follow the idea of a pure core and an impure shell, you will find that pure and impure and
async
and "sync" are similar. The impure shell would deal with external systems. If you call an impure method from your method, your method is impure. Similarly, interacting with external systems is oftenasync
, and if you want toawait
anasync
method, you have to make your methodasync
.So, yeah,
async
propagating is annoying. The solutions is that the entry point will be impure (andasync
) it will deal with external systems (impure imperative shell) and call into your notasync
code (pure functional core), which returns to the shell for more interoperability. That way you do not have to make everythingasync
, and you do not have to make your whole code impure. If you are isolating external systems (for ease of exchanging them and ease of testing), you are probably doing this already.Is your complain that
await
is longer to type thango
? that explains "wordy".Asynchronous does not mean multi-threaded. For instance, we can read for the hard disk without having a
Thread
waiting. You initialize the buffer where you will read, tell the operating system to tell the driver to tell the hard disk to write to that buffer (via DMA) and trigger an event. Then in the event we doTaskCompletionSource.SetResult
and then system can schedule the continuation (your code afterawait
). NoThread
, and you didn't have to worry about it. And yes, that is the same pattern I described above.The compiler is rewriting your code as continuations. It is a state machine. This is not the first time the compiler rewrites your code as a state machine, that would have been
co-routinesiterators withyield return
.For an
async
method, eachawait
means that the code afterwards will be a continuation theTask
being awaited.Please note that the
Thread
calling will not be waiting. Instead, the async method schedules the firstTask
, sets its continuation, and returns aTask
that will completed when all the continuations created for the method completes.Actually, it is a bit more complicated because there is error handling, and it works with
try-catch-finally
. The compiler does the rewriting.That probably means that the calling
Thread
will eventually be free to do work, if that is the case it could be used to run scheduledTask
s. Sometimes the continuation will run on the sameThread
that called it, sometimes it will run on a different one. It is up to theTaskScheduler
. The defaultTaskScheduler
will use theThreadPool
. And, yes, you can write a customTaskScheduler
.Perhaps the interview Mads Torgersen: Inside C# Async can help you understand further. Strongly recommended.