I've haven't been posting from a long time ago, but I think this topic is a big one and it shouldn't be passed.
Let's get straight to the point.
Frequently I'm spotting the mistake of calling .Result or .Wait() in projects, forums and blog posts. There are a lot of articles saying that you have not do it, but without enough explanations and examples Why. Most of them are just saying: "use async/await all the way down" with complex explanations. But why not the opposite - using Result or Wait()? Because of potential "thread starvation" and/or "deadlock". Without any examples it is not giving enough light on this topic to me. And you, right?
The most common way to deadlock your application is when your have a synchronization context and the main thread is waiting to complete a task which will be completed when the main thread is ready/free to complete it. Stephen Cleary wrote a great article about this case. Also you can read my article with an example of deadlock with synchronization contexts. So there is no need to cover this case in more details because there are a tons of articles on this topic - just google it.
Ok. Enough. If you are still with me let's get our hands dirty and see some code:
ThreadPool.SetMaxThreads(4, 4);
var tasks = new List<Task>();
for (int i = 0; i < 4; i++)
{
tasks.Add(Task.Run(() => new HttpClient().GetStringAsync("https://google.com").Result));
}
Task.WhenAll(tasks).Wait();
As initial step we are limiting all available threads in the threadpool. After that imagine that every Task that is ran is simulating a new asp.net request. So we have 4 requests coming simultaneously and 4 available threads which can process the requests. Every of this requests are trying to call a rest service or just doing something with somekind of internet resource by using HttpClient. So far, so good. Calling .Result or .Wait() is holding the current thread to wait the async task to complete and this is accomplished by something similar to while(!IsCompleted){}
loop. Also every async I/O operation (in this case web request) is started by one thread and it's completed by the same OR different thread. There is no guarantee the same thread will make the request and complete it. So we have 4 request threads waiting for a Task to complete, no more available threads in the threadpool and when some of the web requests complete - guess what?
You have no more available threads to complete the result of your async I/O operations. It's simple - you have all allocated threads in the pool busy with waiting (while(IsCompleted){}
) tasks which are waiting for web requests to complete and web responses are waiting for available threads to complete.
Now you have one strong reason why you have not to call .Result or .Wait().
Solution (or not exactly?!)
Here is what the community suggests to escape from the problem.
Task.Run(() => Client.PostAsync()).Result;
The problem with the following code is that in our case it will start the web request in a new task which returns the web request's task itself. The result of the web request is queued as a task in the ThreadPoolTaskScheduler (default TaskScheduler). However, when the request completes and you don't have free threads to execute the scheduled task from the TaskScheduler then again you have...
Thread starvation
Even you haven't deadlock you still has a problem using Result or Wait(). Let's move on and see what is thread starvation.
If not all of the web requests to your server are performing async I/O operating using .Wait() or .Result then we can enter another interesting case. Waiting for the web request and completing the I/O (on another thread), we are using 2 of the available threads instead of one. This could lead to thread starvation and lower throughput of your application.
ThreadPool.SetMaxThreads(4, 4);
var tasks = new List<Task>();
for (int i = 0; i < 4; i++)
{
tasks.Add(Task.Run(() => { Thread.Sleep(10000); Console.WriteLine(DateTime.Now); }));
tasks.Add(Task.Run(() => { Thread.Sleep(5000); Console.WriteLine(DateTime.Now); }));
tasks.Add(Task.Run(() => { Thread.Sleep(2000); Console.WriteLine(DateTime.Now); }));
tasks.Add(Task.Run(() => { Thread.Sleep(1000); Console.WriteLine(DateTime.Now); }));
}
Task.WhenAll(tasks).Wait();
In this example we are simulating each asp.net request to take a few busy seconds to complete it's work (it could be waiting for internet resource with .Result). Here are the results of execution:
1/1/1900 10:04:57 AM
1/1/1900 10:04:58 AM
1/1/1900 10:05:01 AM
1/1/1900 10:05:03 AM
1/1/1900 10:05:03 AM
1/1/1900 10:05:04 AM
1/1/1900 10:05:06 AM
1/1/1900 10:05:07 AM
1/1/1900 10:05:08 AM
1/1/1900 10:05:08 AM
1/1/1900 10:05:09 AM
1/1/1900 10:05:11 AM
1/1/1900 10:05:12 AM
1/1/1900 10:05:13 AM
1/1/1900 10:05:13 AM
1/1/1900 10:05:18 AM
You can see how the requests is pausing in groups and starving is in place. Now if we assume our task is slow enough then IsComplete is never or almost never equal to True, so we have something like while(true) that holds the thread for enough time. This will result in a spike for high CPU Queue Length without the processor actually working but instead .WAITing(). High CPU Queue Length could be a sign for thread starvation.
Solution
I'm sorry for the bad news if you are a fan of .Result and Wait(), but the ultimate solution is to use async/await all the way down and never use blocking operations such as .Result and .Wait().
Conclusion
I suggest you to not use async/await because it's just new (to you) or it's cool. If you don't understand how exactly works and the execution context of your code (asp.net, console app, wpf app and etc.) probably it's better to stick with the synchronous methods especially when working with asp.net or console application (wpf and winforms are kinds of a different story). Computers nowadays are extremely powerful to serve hundreds or thousands of requests per second without async I/O operation. If you need high scalability and your application should be high perform-ant then async/await is the way to go. But before that think on how you can improve your application (I think there would be tons of ways to do that) without the complexity of async/await. Another reason to not use it - it's hard to understand to newcomers and it's highly error prone even to most senior developers (you know - everybody has bugs).
Resources
- https://blogs.msdn.microsoft.com/pfxteam/2012/04/13/should-i-expose-synchronous-wrappers-for-asynchronous-methods/
- Return a Task instead of awaiting it
- https://blogs.msdn.microsoft.com/vancem/2018/10/16/diagnosing-net-core-threadpool-starvation-with-perfview-why-my-service-is-not-saturating-all-cores-or-seems-to-stall/
- https://windows10gadgets.pro/tipstricks/processorqueuelength.html