DEV Community 👩‍💻👨‍💻

Cover image for How C# asynchronous programming is different than what you think
Yaser Al-Najjar
Yaser Al-Najjar

Posted on • Updated on

How C# asynchronous programming is different than what you think

Why even asynchronous programming?

Non-blocking code... period!

That's the whole goal of async code, you wanna write an app that doesn't hang on the user's face so that he won't feel things are stuck!

Sample for blocking ui:

blocking
src:https://www.mithunvp.com/building-responsive-ui-using-async-await-csharp/

The same app when it's doing its job the async way

non-blocking
src:https://www.mithunvp.com/building-responsive-ui-using-async-await-csharp/

This goes on any platform and any device, whether phone, web, or even TV apps.

So what did C# bring exactly?

The problem with async programming is that it's been always hard as you know, in C/C++ it's really hard to get it done right (and testing is even harder).

On the other hand C# brought a simple way (when Anders Hejlsberg introduced async keyword) to do that that in a much less error-prone way.

Sample code for summing numbers asynchronously:

// Code sample from: https://www.dotnetperls.com/async
using System;
using System.Threading.Tasks;

class Program
{
    static void Main()
    {
        // Call async method 10 times.
        for (int i = 0; i < 10; i++)
        {
            Run2Methods(i);
        }
        // The calls are all asynchronous, so they can end at any time.
        Console.ReadLine();
    }

    static async void Run2Methods(int count)
    {
        // Run a Task that calls a method, then calls another method with ContinueWith.
        int result = await Task.Run(() => GetSum(count))
            .ContinueWith(task => MultiplyNegative1(task));
        Console.WriteLine("Run2Methods result: " + result);
    }

    static int GetSum(int count)
    {
        // This method is called first, and returns an int.
        int sum = 0;
        for (int z = 0; z < count; z++)
        {
            sum += (int)Math.Pow(z, 2);
        }
        return sum;
    }

    static int MultiplyNegative1(Task<int> task)
    {
        // This method is called second, and returns a negative int.
        return task.Result * -1;
    }
}
Enter fullscreen mode Exit fullscreen mode

You see how simple they made it, just add async await everywhere and it's gonna be magically asynchronous.

Nothing comes perfect at first

Of course, C# dudes didn't have this simple idea of async programming upfront; it's been actually developed tons of times, you can see the legacy patterns here:

https://docs.microsoft.com/en-us/dotnet/standard/async

The way other programming langs are doing it

I believe that most langs implementations are poor in doing async programming.

I mentioned the phrase "langs implementations" cuz most languages actually support async programming, but it gets tricky when implemented.

Java, Python, and even Javascript are supporting simple syntax as a "language".

Example in python

async def get_json(client, url):  
    async with client.get(url) as response:
        assert response.status == 200
        return await response.read()
Enter fullscreen mode Exit fullscreen mode

The async and await keywords have been added after C# as mentioned in PEP492

The Java way

When I was doing Android programming, threading was the normal way to do it:

// src: https://android.jlelse.eu/8-ways-to-do-asynchronous-processing-in-android-and-counting-f634dc6fae4e
new Thread(new Runnable(){
  public void run() {
    // do something here
  }
}).start();
Enter fullscreen mode Exit fullscreen mode

But now there is CompletableFuture and AsyncTask, and Kotlin emphasize on coroutines (correct me if I'm wrong).

Other langs

Go actually follows a similar model to coroutine, but in its own Goroutines.

Javascript has the exact same syntax as C#.

As for C++, wiki mentions:

In C++, await (named co_await in C++) has been officially merged into C++20 draft

The crux of the matter

This is the reason I'm writing this post, the original question goes here:

why do you think its async support is better in C# than other programming languages?

I'm gonna tell you how it's better in one word: the adoption.

Yes, you heard that right...

Whether it's a simple HTTPClient:

// src: https://stackoverflow.com/a/31102831/4565520
public Task<string> TestDownloadTask()
{
    using (HttpClient client = new HttpClient())
    {
        client.BaseAddress = new Uri(@"https://api.nasa.gov/planetary/apod");
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

        // You don't need await here
        return client.GetStringAsync("?concept_tags=True&api_key=DEMO_KEY");
    }
}
Enter fullscreen mode Exit fullscreen mode

A Controller in your webapp:

[HttpGet("{id}")]
public async Task<IActionResult> GetDepartment(int id){
    var department = await departmentRepository.GetByIdAsync(id);
    return Ok(department);
}
Enter fullscreen mode Exit fullscreen mode

Or even an ORM that talks to a database (EntityFramework with linq-to-sql):

return await db.students.Where(s=>s.Name.StartsWith("A")).ToListAsync();
Enter fullscreen mode Exit fullscreen mode

I've heard once famous C# author, Scott Allen, saying:

I hope we will see a day where we won't write async/await, cuz everything are just asynchronous... but it's just a hope!

Merely, cuz C# adopts async await in almost every class especially with .NET Core.

So, you will end up with mostly high performant app on every platform... if done right 😉

Know your tools well

Guess what, the async/await pattern is NOT a solution for all your asynchronous problems.

And, it's not black and white, you should understand how async/await works and also understand the costs of it.

You can achieve non-blockingness in various ways, and an event driven architecture (with a message queue fire and forget style) might help you more than relying on just adding async/await all over your code.

What do you think

Is C# doing it right? or are there much better alternatives to get a non-blocking code?

Top comments (17)

Collapse
rhymes profile image
rhymes

Hi Yaser, thanks for the explanation! My reply is going to be long and will take a few detours, so brace yourself :D

Async programming as programming model has been around for quite a while. Think about how I/O works with interrupt handlers in operating systems, or that most desktop UI frameworks (for example the age old Win32 for example with PostMessage) have an event queue. Think about epoll in Linux, kqueue on BSD and MacOS and Windows's I/O completion ports.

Async as model can be implemented with different techniques.
Recently, as you explained, a lot of languages are adopting these two "magical" keywords: async and await but the underlying concepts definitely predates the "shortcut" keywords.

A quick trip down memory lane: I remember reading about the reactor pattern in the context of Twisted, a Python framework (arguably one of the most famous at its peak in the realm of high level languages) where everything is async. It was built to be multi platform on top various I/O sys calls, it introduced people to the concept of deferred (basically the same thing as a promise or a future), it allowed to branch out using threads if needed (but with the same interface, hence the abstraction mentioned earlier) and it even supported GUI event loops so that you could integrate with other toolkits. It had widespread usage for a while: Apple famously used it for the calendar server, people built reservation systems and file servers and many other things. It came with support for many, many protocols. Rubyists a few years later launched EventMachine which clearly was inspired by the work of Twisted and other frameworks like it (I think Boost.ASIO for C++ at least).

Why these frameworks didn't take over everything for ever and ever? I don't know the exact reasons but I can make a few guesses. I used Twisted a lot back in the day and the first complain I had (most of people unfamiliar with it did) was that it forced everything to be async. You know very well, even for modern implementation of this model, that you can't magically intermix blocking stuff so once you go async, you more or less have to go async all the way. There was also way less information back then (I remember reading Twisted's own source code to figure out how to use some parts of it or literally write to a friend of mine who understood it better than I did and also was a contributor to the project). Twisted and EventMachine are the embodiment of the concept of a framework: they call your code and your code has to be ready to be called and to not block the main event loop. A lot of existing code wasn't async, web servers were either multithreaded or multiprocess (!!) and even though they had the usual drawbacks of those concurrency models, people valued their own developer experience over the idea of having to rewrite stuff I guess, even though Twisted performs very well. I'm sure there are other reasons but this didn't probably help adoption, which is probably why the two most famous web frameworks in Python were not built around an async model.

What does it mean not to block? It means that it needs to yield back control (yield is also an ever present keyword in Python's cooperative multitasking but that's another story) to the caller pretty quick because the framework has to pass the token between all the participants. If one takes the slot and decides to go on a rampage without giving back the control, everything blocks and suddenly no one can do anything, hence why all the I/O libraries have to be rewritten using the async model when you integrate async. You can't one one that yields back control and one that doesn't. This is both an advantage and a disadvantage and people have been debating on this since the beginning. This is Twisted's implementation of async, and Node's as well as far as I know. Though we can't say Node suffers by adoption problems, it clearly worked because for a combination of factors (inherent merits, timing, marketing and having chosen a language millions of people were already familiar with). So well, you never know, Node clearly showed there's a market for a "single threaded event loop" type of ecosystem.

Nginx must have indirectly helped changing people's perception of async in web servers. No more "difficult to debug" threads, no more multiprocessing that doesn't scale with heavy load anyway.

So, coming back to today. Python and JavaScript added async/await to their core, other languages have some sort of async support or are about to and C# seems to be well equipped too.

As I said, a drawback (at least in my opinion) of the async programming model is its all or nothing contract. Think about JavaScript, the second you put "async" on a function, every caller has to be declared async. So it becomes a literal tree structure of async invocations, isn't this the same thing as a framework repackaged with a nicer interface? You are marking your unit of works with a special keyword that the platform knows how to treat differently.

To be clear: I think it's really nice that the developer experience now is better: no more rewriting the whole code to fit a third party framework, no more wrapping results in promises/futures in most cases, no more calling the reactor manually or yielding results.

Going back your article:

You see how simple they made it, just add async await everywhere and it's gonna be magically asynchronous.

That magic scares me sometimes ;)

I believe that most langs implementations are poor in doing async programming
I'm gonna tell you how it's better in one word: the adoption.

I don't know if I agree with the C# developer you quoted because I like options. I like the option of choosing to program in an async model or not. I'm not even sure it's the best model, heck, I'm not even sure a best model exists. That's why I like options :D

Correct me if I'm mistaken because I only had a quick look at the .NET async patterns you linked but it seems like three things pop up: async is implemented on top of a thread pools, the interface is really well thought of and .NET designers have gone all in. So yeah, it's widely adopted but not by chance, it's because people have put a lot of thought in this and planned it well.

Again, I understood correctly, the fact that it is implemented transparently on top of threads means that it can potentially use multiple cores (though it's not guaranteed), which seems a smart move since practically all the CPUs are multicore now.

So yeah, .NET async model is very interesting :-)

Sorry for the super long reply but it's a topic I'm clearly interested in and I like to poke holes into. I'm quite convinced that most if not all concurrency models suck, probably because any type of concurrent execution is hard.

ps. I wouldn't use the expression "C# dudes" because it probably doesn't represent the full spectrum of people working on C# and .NET ;-)

Collapse
vekzdran profile image
Vedran Mandić

A very detailed comment, thank you for the overview of other languages!

There is definitely huge effort behind the language and framework design as you state, and it resulted in such a concise and succinct interface. And yes the thread pool is included in the whole problematic, and the key is not to exhaust it with writing bad async/await.

So, how I see it, async/await is about performing non blocking actions by the help of Tasks (a wrapper on top of Thread) and returning those who would block to the pool to free it for more processing and doing all of that elegantly in code so the reader and reviewer can mentally map the process as the code is read/compiled. And that is about it. If you need parallelism then favor Parallel and other libraries. What I found most in my short experience as a C# developer is that people do not understand what you have excellently pointed out - blocking and yielding control back, that's why often I see code that ignores proper context switching.

Luckily, the newest manifestation of .NET, the .NET Core, has removed the sync context in their ASP.NET web platform, which again now takes more education for developers as people are very puzzled when to use .ConfigureAwait(false) (you don't have to as S. Cleary points out in this article in details, but still one has to take care now of other problems due to implicit parallelism which might occur as a non-easy to observe side effect) and other performance optimization solutions.

By the way I dislike the similarity of JavaScript syntax as it basically covers up a ton of generator + Promise (a better callback actually) code, but still kudos to code-readability which is must these days. :-)

Collapse
rhymes profile image
rhymes

Hi @vekzdran thanks for the feedback! It took me a while to write the comment haha. I'm thinking I should "upgrade" it to an article.

There is definitely huge effort behind the language and framework design as you state, and it resulted in such a concise and succinct interface. And yes the thread pool is included in the whole problematic, and the key is not to exhaust it with writing bad async/await.

As every technical decision there are advantages and drawbacks. I applaud their decision though, they created a concise interface as you say on top of two complicated mechanisms: asynchronicity and thread pooling.

The clear advantage is that you can utilize more resources of the hosting machine and in the future optimize the pool as you see fit. Maybe they can even in a future version allow the user to inject a "single threaded event handler" for people who don't want to use multiple threads under the hood, but I'm in fantasy land now :D

What I found most in my short experience as a C# developer is that people do not understand what you have excellently pointed out - blocking and yielding control back, that's why often I see code that ignores proper context switching.

That's normal, I think it's because most people learn a form of sequential imperative programming which tends to be blocking. You do A, then B, then C and then you compute the result. That's it. All concurrency model require us programmers to think of what could happen outside of the "main flow".

From the article you linked I read this:

However, you shouldn’t. Because the moment you block on asynchronous code, you’re giving up every benefit of asynchronous code in the first place. The enhanced scalability of asynchronous handlers is nullified as soon as you block a thread.

That's it. If you block too much, the thread is not released to the pool and you slow everything down.

but still one has to take care now of other problems due to implicit parallelism which might occur as a non-easy to observe side effect

Yes, I read the part in the article with the example with a List<string>. The fact that threads are hidden to the user, does not mean all the usual problems magically disappear. I wonder if Rust really nailed it and answered all of these problems with their concept of borrowing. I don't have experience with it, I've read a few articles and in a situation like that the write at the same time (thus losing one of the elements of the list) shouldn't happen because only one thread can have the list reference at any time and that's enforced by their compiler.

Collapse
yaser profile image
Yaser Al-Najjar Author • Edited on

I agree with you, async-ness has been there for long in different flavors.

And as you said regarding Twisted, EDA (event driven architecture) is never pleasant, and it's hard to predict and even harder to test!

I also really believe that simpler techs like Node which gives the developers easier way to achieve good-enough products (for tens of thousands of users) made the adoption easier rather than all-async-hard-to-understand techs.

async is implemented on top of a thread pools

async/await uses no threads, you can imagine it as a call-back. (this is a more detailed explanation)

the fact that it is implemented transparently on top of threads means that it can potentially use multiple cores (though it's not guaranteed)

Since no threading is used with async/await, it's mainly meant for IO-bound operations (threads still have their place in CPU intensive operations.)


Fun story:

We built once a simple registration sub-system that sends a verification email to users once they register, very simple and straight forward with SMTP.
And that time we used the magic of C# async/await, it was good... but in some rare cases, the mails aren't sent simply cuz that's how SMTP works (it might drop the connection time to time with more requests coming in).

We decided to switch into Django/Python cuz it has tons of 3rd part libs that serve our goals really well. So it means we will lose the magic of C# async/await!

Of course more mails were dropping after the switch, until we thought about using Amazon SQS to keep the request of sending the mail in the queues and never allow it to pass from the queue till it's successfully sent... guess what happened? we got a heck more reliable and faster mail sending, and I've never heard any user complaining about his verification email after!

Collapse
rhymes profile image
rhymes • Edited on

async/await uses no threads, you can imagine it as a call-back. (this is a more detailed explanation)

are you sure? I'd agree normally but in this article I found on MSDN: Task-based asynchronous pattern (TAP) and Async in depth linked from Async overview they clearly talk about async tasks mapped on a thread pool.

@vekzdran also linked article ASP.NET Core SynchronizationContext which talks about thread pooling again.

But probably we're talking about two slightly different things due to my lack of familiarity with .NET and C#.

async/await by themselves don't introduce threads, but ASP.NET uses async on top of a pool to handle concurrent requests? Is that the source of my confusion?

Since no threading is used with async/await, it's mainly meant for IO-bound operations (threads still have their place in CPU intensive operations.)

I read this in the article you linked:

The async and await keywords don't cause additional threads to be created. Async methods don't require multithreading because an async method doesn't run on its own thread. The method runs on the current synchronization context and uses time on the thread only when the method is active. You can use Task.Run to move CPU-bound work to a background thread, but a background thread doesn't help with a process that's just waiting for results to become available.

I'm a little confused by the paragraph you linked. What this paragraph says to me is that calling await something does not generate a thread (which makes sense, otherwise every call would create its own thread and add a lot of context switching). But it doesn't exclude the fact that an async operation MIGHT be running on a different thread than the one it was called from (which is compatible with the idea of pooling, see what happens in Go with goroutines which are transparently mapped on a pool of threads). As it says, if you have a CPU intensive operation (perhaps blocking the context switch of the continuations) you can explicitly tell the compiler to move the operation to a background thread. So yeah, from what I've gathered by this and the other articles thread pooling MIGHT be involved or not, it's just not visible to the programmer (which is why it's a great implementation).

we got a heck more reliable and faster mail sending, and I've never heard any user complaining about his verification email after!

Yeah, putting a queue between you and the SMTP server was a good idea :)
You might want to follow Christine Spang if you're still interested in all things "Python + email":

spang image
Thread Thread
yaser profile image
Yaser Al-Najjar Author

My rule of thumb is:

  • If I want nonblocking code that is related to IO (say talk to db) without creating a thread, I simply throw async/await everywhere.

  • If I want to create a thread to do some cpu heavy computation, I add Task.Run and fill in the lambda my sync code (and for sure that requires adding async/await).

This is the way I do async C#... I should say I never tried to check those statements 😁

Also, some operations might lead the OS to create a thread, but that doesn't count on our app process (it's the way the OS does its business instead).

Thanks for the recommendation, I just read her posts and they seem interesting!

Collapse
vekzdran profile image
Vedran Mandić

This is a cool example on how async/await really helps (you had to go into a message queue service instead of wrangling async await, woah, hope you solved that somehow). You did fix it but introducing so much infrastructure, is that right? I mean, you solved the issue that's what counts, but is it worth? Thanks.

Thread Thread
yaser profile image
Yaser Al-Najjar Author • Edited on

That infrastructure is really cheap, we thought about adding redis or rappit MQ, but we made our minds with SQS cuz it's 10x cheaper (cuz we didn't wanna change our server plan to add more memory, and not mentioning ensuring the MQ runs with no problems).

For the technical cost, it took us about 3 days to add celery in front of the mail sender and deploy it on production (after deploying and trying it on staging).

I know took a bit of time and effort to get things done, even we had already the resend verification button... but we kept getting moaning from our beloved users 😄

So, for stopping the users' moaning, I would say it's totally worth it!

Collapse
turnerj profile image
James Turner

I do like the way async/await works in C#, it only felt like a burden when my entire app was synchronous but once I started moving to async, it became easier and easier.

With some of the examples in your post though, I would be careful with how they use async. Things like async void, returning a task without awaiting, and calling task.Result can have different issues in different circumstances.

Here is a good guide of when and how to use async/await which covers how/why some of those aspects can be bad: github.com/davidfowl/AspNetCoreDia...

That guide might be aimed at ASP.Net Core however many (maybe all) aspects are still relevant outside of it.

Collapse
yaser profile image
Yaser Al-Najjar Author • Edited on

Thanks for letting me know... for the sake of completing the article I didn't look carefully into the codes, I just copied and pasted couple of examples from various places and mentioned their sources; I will update the examples soon.

Collapse
turnerj profile image
James Turner • Edited on

No problem 🙂

Some of these things about async/await aren't all that obvious and different understanding of how to use it likely changed over time. The people that probably wrote the example code probably had it based on old information.

One of the things that gets me still with the async/await pattern is whether I need to call ConfigureAwait(false) or not - there seems to be a lot of different opinions about it online from a variety of people smarter than me.

Thread Thread
yaser profile image
Yaser Al-Najjar Author

Yeah, I still remember the first days I was cluelessly reading different articles about async C# and mixing new code with old one :D

C# is moving a bit fast that I feel sometimes it's burdensome to catch up with their latest updates.

Thread Thread
turnerj profile image
James Turner

C# is moving a bit fast that I feel sometimes it's burdensome to catch up with their latest updates.

I feel the same way about JavaScript!

As much as some of the new features that are being released (in JS, C#, CSS etc) are helpful and solves certain problems, there are some cases where I just look at it and wonder how we got here as it doesn't look easier/better than what existed.

Thread Thread
yaser profile image
Yaser Al-Najjar Author

JavaScript is a whole different story 😆

They just add all sort of keywords and features into the language just to feel "Oh, JavaScript can do that too" 😑

Thread Thread
asparallel profile image
AsParallel

Ecmascript is actually the result of a large, multi-national consortium of some of the best minds in the industry. They put a lot of effort into producing the spec and provide reasons for inclusion or exclusion of features in gory detail as part of that process.

It's not the javascript of 20 years ago.

Collapse
louy2 profile image
Yufan Lou • Edited on

For imperative style async is probably the best. There is no practical way or need to abstract to higher-kind, so it is necessary and sufficient to have keywords for a few managed effect and make special case type checks for them.

For purely functional style asynchronicity is just another monad in the effect stack. More specifically, the standard Concurrently is not even a monad, but a monoid, so you can only express parallel composition, and any dependency (blocking) has to be lifted and chained to the inner IO. The next step in this is streaming libraries and the functional reactive style.

Another famous async model is Erlang's actor model.

Collapse
vekzdran profile image
Vedran Mandić

Yaser this is a wonderful summary, thank you sincerely! It will help me a lot in my future presentation in which I plan to point all common pitfalls in usage of async and await.

🌚 Life is too short to browse without dark mode