This article is about asynchronous programming in C# using Task and Task<T>.
Do you really need to make your code asynchronous?
The first misconception about asynchronous programming is you don’t necessarily need to use it. Additionally, it can actually cause far more problems than you think it solves, but I am not going to dive into that for this rant. Use it where you need it is my point, don’t go out of your way to make everything asynchronous because all you are doing is adding unnecessary overhead and complexity to your code. This is a lesson in, “Just because you can, doesn’t mean you should.”
My approach
I take an iterative approach to my coding. So I use the same rules I apply to any of these questions to async code:
Question | Answer |
---|---|
Should I make this construct private or public? | I always start private. If I need to make it public later, I change it. |
Should I create a method for this code? (DRY) | There is no black and white answer for this, but generally speaking if it isn’t being used anywhere else then no. If it can be broken down into logical parts then yes. The moment I see myself needing to copy logic elsewhere I try to turn it into a method or make it reusable in some way. You DO want a single point of failure in this instance as unintuitive as that sounds. |
Can this be made more generic? | This is very closely related to “Should I make a method”. However, for this question it is more about portability. Can I use an object to transport three or more parameters through my pipeline? Can I use an interface so that I don’t require overloads? Can I actually use a generic type of T to literally make this generic? |
Should this code be moved to its own class? (Single-responsibility principal) | The moment you see two themes happening inside of a class, it’s time to undo the spaghetti and separate the responsibilities into separate classes where they can be unit tested on their own. |
The common theme between all of these things if you missed it is, start without it, then see if you need it. This is akin to the advice I give to new developers and people who get stuck on a project:
If you have analysis paralysis, then just start coding. Avoid over thinking it, you can just do your first rough draft then go over it again later. Use an iterative approach.
Me
I implore everyone to not worry about making it perfect the first time. Just get it to work first, then go back and perform normal refactoring. Always attempt to use unit tests.
So when do I make my code asynchronous?
Unless you are explicitly told to make your code asynchronous, don’t bother. Do you see an immediate need or benefit of using asynchronous programming for what you are doing? If the answer is no, or you are feeling dubious, then don’t do it. The same goes for using parallelized loops when you don’t need to. Only bring out the big guns when you need them. Again, just because you can, doesn’t mean you should.
I can confidently say about 90% of the code I have seen using asynchronous approach is completely unnecessary. It becomes very obvious when you are using the await
keyword after every single async
call. This itself is an admission that you needed your code to be synchronous, there is nothing asynchronous about it if you are waiting on anything. However, that is the unspoken contract you sign the moment you start using asynchronous programming. Everything in your code becomes infected with it.
I am not convinced asynchronous programming is needed in situations where all of the code is clearly being called in a synchronous fashion. What was accomplished other than making the code uglier?
Doubts to be had
I am not fully convinced that it is completely useful for API calls or at all in a Web Context. So many developers don’t understand how web servers work and I encourage them to look into how at least IIS works so they can understand that in most cases the webserver is already multithreaded. This is a deep discussion because you can configure your worker process (w3wp.exe) to only spin up one process, but that’s not normal. The mere fact that so many applications are load balanced and now leveraging containers makes the asynchronous programming even less important. It doesn’t solve anything.
Another deep discussion that I won’t get into is how strange the database acts when async
is involved in certain situations. Things get really weird when you launch 100 simultaneous queries off of the same connection for example. It has completely unexpected results, but I will leave that for another time.
Examples of where it is useful
I have found several situations where asynchronous programming is incredibly useful:
- Processing a list of mutually exclusive tasks in parallel. This is especially helpful when it is a long running task with an unknown completion time.
- Any time I can process a large list in parallel I will because it really is a time saver.
- When dealing with HTTP requests, instead of handling it in serial, you can launch multiple requests simultaneously if order doesn’t matter.
- Single threaded applications such as Windows Services where you have a main execution thread. Instead of tying up the main thread, you can spin off processes into tasks. Think about how message busses work.
- Desktop applications that are also single threaded to a degree where you have what is referred to as the GUI or UI thread. You do not want to hang the UI thread because it makes for a poor user experience. We have all experienced this with windows, “This application is not responding. Close or Wait”. Or those times you click on a drop down to find a computer on the network by accident and you hate life for about two minutes. These are experiences that can be improved by putting those processes into a separate thread.
Misconceptions
There are several developer driver urban myths I have encountered that I just flat out don’t agree with regarding asynchronous programming.
ID | Misconception | Why it’s misguided |
---|---|---|
A | You should never use just Task , always use at least Task<bool> . | This one bothers me a lot because it just shows me the developers making this claim don’t understand how TPL works. The first problem with this is that you are now returning a meaningless true in your code. This quite literally is more damaging than just leaving it as Task because you are just going to confuse everyone else who sees the bool coming back into thinking an operation completed successfully. It’s completely irresponsible, please stop doing this.The second problem is that this is basically stating that void doesn’t exist. Simply put, you use Task when you are making a void method asynchronous. I have been given weak anecdotal evidence at best claiming that just using Task will stop exceptions from propagating. I can prove that’s not true. |
B | Never use async void , always use Task or Task<bool> instead. | This one bothers me less than the first because there is a reason to avoid async void , but again it’s not written in stone to do. Once again, it just shows me the developers making this claim don’t understand how TPL works.There is nothing wrong with using async void . The only time it becomes a problem is if the contents of your method are not awaiting anything. Then you should not use async void , it should just be a void method because in other words, this method is NOT ASYNCHRONOUS. This brings me back full circle to the beginning of this rant, “Why are you making it asynchronous?”Changing your methods to return a meaningless bool is just stupid. Please stop.I was given the same weak anecdotal evidence claiming that async void will stop exceptions from propagating. Again, I will prove this is not true at all. |
C | You should always await every async method instead of just returning the Task . | This claim is as misguided as not knowing the difference between Task and Task<bool> . Task is an object, you can return it because it’s an object. The rules of object oriented programming didn’t change all of a sudden for asynchronous programming so this claim is complete bunk. Also this is not COM, so I don’t understand where the hell people are getting these weird ideas. Furthermore, there is absolutely no requirement to await anything because you can also just call task.Wait() it does the same thing! |
Code examples
Here is that proof I was referring to above. Understand that there are too many permutations to show here, so these are only the most important examples I can think of. Everything below is a LinqPad example which is attached to the bottom of this article.
Misconception A: You should never use just Task
, always use at least Task<bool>
.
Below Task<bool>
is being used only.
public async Task Main()
{
try
{
//Stack trace is going to show the whole lineage because every step of the way is awaited for
await Blah1();
}
catch (Exception ex)
{
ex.ToString().Dump();
}
}
public async Task<bool> Blah1()
{
return await Blah2();
}
public async Task<bool> Blah2()
{
return await Blah3();
}
public Task<bool> Blah3()
{
var x = 1;
var y = 0;
var z = x / y;
return Task.FromResult(true);
}
Below Task
is being used only.
public async Task Main()
{
try
{
//Stack trace is going to show the whole lineage because every step of the way is awaited for
await Blah1();
}
catch (Exception ex)
{
ex.ToString().Dump();
}
}
public async Task Blah1()
{
await Blah2();
}
public async Task Blah2()
{
await Blah3();
}
public Task Blah3()
{
var x = 1;
var y = 0;
var z = x / y;
return Task.CompletedTask;
}
Exception output for both one on top of the other.
Hrmmm… when you take a close look can you see a discernable difference here?
Seems like the ONLY difference is that “`1” that shows up. So again, what exactly is the bool offering us other than confusion and unnecessary return types that you would never use otherwise?
Misconception B: Never use async void
, always use Task
or Task<bool>
instead.
Below async void
is being used for the method that raises the exception. What happens here is a compiler warning is issued telling you that this is a problem. You should not ignore this warning because what will happen here is that your exception will not propagate. However, what this means is if your method can raise an exception then you should be returning a Task
and should not use async void
. But, this does not mean NEVER use async void
.
For the skeptics out there it says so right in the Microsoft documentation.
public async Task Main()
{
try
{
//Exception never reaches this point
await Blah1();
}
catch (Exception ex)
{
ex.ToString().Dump();
}
}
public async Task Blah1()
{
await Blah2();
}
/* Compiler Warning: CS1998 This async method lacks 'await'
* operators and will run synchronously. Consider using the
* 'await' operator to await non-blocking API calls, or 'await
* Task.Run(...)' to do CPU-bound work on a background thread. */
public async Task Blah2()
{
Blah3();
}
//Compiler Warning: CS1998 same as above
public async void Blah3()
{
var x = 1;
var y = 0;
//Exception will be thrown here, no stack trace provided because no task is returned here
var z = x / y;
}
Therefore, to reiterate if you are going to be throwing an exception, then yes you should be using Task here as your return type. The times it is acceptable to use async void
is when the operations happening inside of the method are not asynchronous, in other words there is no way to make a synchronous operation asynchronous for the hell of it. That’s why this is even a problem! Circling back again to the beginning of this rant, “Why are you using asynchronous programming? Are you sure you need it?” This is a clear indication that you don’t need it, but because everything else is using it now you are forced to make synchronous code fake being asynchronous. Fun.
Below Task
is being used to make a synchronous call asynchronous. It’s actually pointless, but this is for demonstration purposes. You will see that the compiler warning CS1998 has been reduced to one instance. I could have eliminated all instances of this warning by taking the contents of Blah3()
and putting it directly into Blah2()
but for the sake of demonstration and comparison I wanted to keep all three demo methods.
public async Task Main()
{
try
{
//Exception never reaches this point
await Blah1();
}
catch (Exception ex)
{
ex.ToString().Dump();
}
}
public Task Blah1()
{
return Blah2();
}
public Task Blah2()
{
Blah3();
return Task.CompletedTask;
}
/* Compiler Warning: CS1998 This async method lacks 'await'
* operators and will run synchronously. Consider using the
* 'await' operator to await non-blocking API calls, or 'await
* Task.Run(...)' to do CPU-bound work on a background thread. */
public async void Blah3()
{
var x = 1;
var y = 0;
//Exception will be thrown here, no stack trace provided because no task is returned here
var z = x / y;
}
In both cases a DivideByZeroException
is raised, but does not propagate so there isn’t much else to share. The exception is raised in Blah3()
and the thread dies. You obviously want to avoid this. I did not provide a Task<bool>
example because I have already proved it won’t do anything different from Task
.
Misconception C: You should always await
every async
method instead of just returning the Task
.
Only providing one example for this because you can compare this with the examples from Misconception A above.
public async Task Main()
{
try
{
//Stack trace is going to show the whole lineage and only one Task is awaited
await Blah1();
}
catch (Exception ex)
{
ex.ToString().Dump();
}
}
public Task Blah1()
{
return Blah2();
}
public Task Blah2()
{
return Blah3();
}
public Task Blah3()
{
var x = 1;
var y = 0;
var z = x / y;
return Task.CompletedTask;
}
Exception output for the above code.
Exception output for Task
. As you can see, all this does is show you that there is more overhead with awaiting at every step of the wait, especially when you don’t need to. In the above stack trace, it is much cleaner and straight to the point.
LinqPad script source
The source for the examples I am showing above can be run on your local via LinqPad or run it however you want. You can look at these examples on my GitHub account. I am linking to the folder just in case I end up changing the individual files: https://github.com/dyslexicanaboko/code-samples/tree/master/c-sharp/Async%20Tasks
Conclusion
Things are not black and white in programming. They rarely are, so be careful with these arbitrary rules based on previous bad experiences. I think what happened here is people experienced a framework level bug and then resigned to these irrational oddball opinions because of whatever harm it caused. Framework level bugs happen, they get fixed like anything else in code.
11/20/2022 Update
While reviewing code at work I stumbled across a good find that finally shows me where this bizarre claim of “Always use Task<bool>
” has come from! I found a ReSharper warning underlining a piece of code and it provides a link. The JetBrains website documents the problem a little and provides a link to a Microsoft document that somewhat confusing, disingenuous and misleading.
I don’t feel that any of this is explained well. I am not going to reiterate anything I wrote already above. This is just more of Misconception B. Don’t handle synchronous code inside of an asynchronous method. It has very little to do with void and more of developers not understanding asynchronous programming.