6

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.

3
  • The only wrong decision is defaulting ConfigureAwait to true. But since you complaining, how would you design it better -- face a goal: your solution has to be back-compatible, it should provide asynchronous calls (because performance matters). Commented Aug 17, 2019 at 6:54
  • @greenoldman, you make a good point about backwards compatibility, thanks.
    – spirc
    Commented Aug 18, 2019 at 2:14
  • My question has produced some really good thoughtful answers, thanks everyone.
    – spirc
    Commented Aug 18, 2019 at 2:15

3 Answers 3

14

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.

Oh, but that is not wordy at all. You are not writting something like this:

client.GetAsync("http://www.nzherald.co.nz/").Then
(
    response => response.Content.ReadAsStringAsync().Then
    (
        pageContents =>
        {
            Console.WriteLine(pageContents);
            Console.ReadLine();
        }
    )
);

And an API like the one above (probably a wrapper around ContinueWith) it won't handle exceptions properly, you would probably need an Error 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 a Task, it does not means you have to await it. If you want the Task to do something else with it (for example place it an array for Task.WaitAll or Task.WhenAll), you do not put the await keyword. Therefore, the await keyword is not redundant.


is creation of threads for async code going to impact performance?

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 the ThreadPool, thus there won't be a lot of new Threads created.

That it will use the ThreadPool is not true for every API. Most will follow the following pattern:

  1. The library creates a TaskCompletionSource.

  2. The library sets a means to receive a notification. Callback, timer, message, whatever...

  3. The library sets code to react to the notification that will call SetResult, or SetException on the TaskCompletionSource as appropriate for the notification received.

  4. The library does the actual call to the external system.

  5. 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 the Task is scheduled (that continuation is what happens after your await).

How can I throttle the rate of calls if I'm calling GetAsync in a loop?

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 Tasks concurrently (not being awaited), I would suggest to use a SemaphoreSlim. I would use it in conjunction with Task.WaitAny or Task.WhenAny.

Besides that, you probably will find await Task.Delay(milliseconds) useful. It will use the pattern I described above, around a timer, no Thread will be blocked waiting (unlike Thread.Sleep).


has the c# team gone in the right direction creating this async paradigm in the language?

I believe async/await is generally good. I agree that there is some overhead (creating Task 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 often async, and if you want to await an async method, you have to make your method async.

So, yeah, async propagating is annoying. The solutions is that the entry point will be impure (and async) it will deal with external systems (impure imperative shell) and call into your not async code (pure functional core), which returns to the shell for more interoperability. That way you do not have to make everything async, 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.

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.

Is your complain that await is longer to type than go? 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 do TaskCompletionSource.SetResult and then system can schedule the continuation (your code after await). No Thread, and you didn't have to worry about it. And yes, that is the same pattern I described above.


I find it hard to reason about what is happening behind the scenes of the compiler

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-routines iterators with yield return.

For an async method, each await means that the code afterwards will be a continuation the Task being awaited.

Please note that the Thread calling will not be waiting. Instead, the async method schedules the first Task, sets its continuation, and returns a Task 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 scheduled Tasks. Sometimes the continuation will run on the same Thread that called it, sometimes it will run on a different one. It is up to the TaskScheduler. The default TaskScheduler will use the ThreadPool. And, yes, you can write a custom TaskScheduler.


Perhaps the interview Mads Torgersen: Inside C# Async can help you understand further. Strongly recommended.

4
  • "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" - No, listening on sockets is truly asynchronous. Commented Aug 17, 2019 at 17:54
  • @SebastianRedl I found recv, the docs says "f no incoming data is available at the socket, the recv call blocks and waits for data to arrive according to the blocking rules defined for WSARecv with the MSG_PARTIAL flag not set unless the socket is nonblocking." and about MSG_PARTIAL I find "Be aware that the MSG_PARTIAL flag bit is not supported by all protocols."
    – Theraot
    Commented Aug 17, 2019 at 18:14
  • @SebastianRedl about .NET, looking with Just Decompile, I see HttpClientHandler.SendAsync uses Task.Run. About .NET Core, I find it uses a WinHttpHandler that uses TaskFactory.StartNew. Could they not do it without spinning a thread?
    – Theraot
    Commented Aug 17, 2019 at 18:20
  • The socket APIs have async methods, and those use Windows overlapped I/O, not the old WSAPI primitives. In .Net Core anyway. Commented Aug 17, 2019 at 18:30
4

If you only ever deal with webservers and other highly I/O bound applications, sure you'll see a lot of async stuff and not a lot without. However, there are still plenty of people writing software for desktops and mobile devices that aren't IO bound where async is not as beneficial, and not used as often.

In any case, many libraries use it because async/await is a useful tool that solves a common problem for a lot of people. Someone might have asked the same question when people started using LINQ, or Generics, or using C# at all instead of C++. It's basically the same reason we aren't writing everything in assembly.

1

Lets say you have 100 incoming calls to your webserver. All 100 starts to exeute a SQL query. 99% of the call is spent waiting for SQL to return a result.

Wouldnt it be better to return the thread to the pool and serve another request while SQL is working?

You can also benefit in UI code by using tasks. Prior to Tasks tools like Unity 3D and Caliburn Micro used Coroutines to execute async code in a synchronous manner. THis is required because you cant manipulate UI components on other threads than the main thread. Also if you wouldnt do it async the UI would stop responding. In winforms you had to marshall this stuff yourself.

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.