Async/await in C#: pitfalls
I’d like to discuss some common pitfalls of async/await feature in C# and provide you with workarounds for them.
How async/await works
The internals of async/await feature are very well described by Alex Davies in his book, so I will only briefly explain it here. Consider the following code example:
public asyncTask ReadFirstBytesAsync(string filePath1, string filePath2)
{
using (FileStream fs1 = new FileStream(filePath1, FileMode.Open))
using (FileStream fs2 = new FileStream(filePath2, FileMode.Open))
{
await fs1.ReadAsync(new byte[1], 0, 1); // 1
await fs2.ReadAsync(new byte[1], 0, 1); // 2
}
}
This function reads the first bytes from two files passed in (I know, it’s quite a synthetic example). What would happen at "1" and "2" lines? Will they execute simultaneously? No. What will happen is this function will actually be split by "await" keyword in three pieces: the part before the "1" line, the part between "1" and "2" lines and the part after the "2" line.
The function will create a new I/O bound thread at the line "1", pass it the second part of itself (which is between "1" and "2" lines) as a callback and return the control to the caller. After the I/O thread completes, it calls the callback, and the method continues executing. It creates the second I/O thread at the line "2", passes the third part of itself as a callback and returns again. After the second I/O thread completes it calls the rest part of the method.
The magic is here because of the compiler rewriting the code of the method marked as async to a state machine, just like it does with iterators.
When to use async/await?
There are two major cases in which using async/await feature is preferred.
First of all, it can be used in thick clients to deliver better user experience. When a user presses a button starting a heavy operation, it’s better to perform this operation asynchronously without locking the UI thread. Such logic required a lot of effort to be implemented before .NET 4.5 had been released. Here how it can look now:
private asyncvoid btnRead_Click(object sender, EventArgs e)
{
btnRead.Enabled = false;
using (FileStream fs = new FileStream("File path", FileMode.Open))
using (StreamReader sr = new StreamReader(fs))
{
Content = await sr.ReadToEndAsync();
}
btnRead.Enabled = true;
}
Note that Enabled flag is changed by the UI thread in both cases. This approach removes the necessity of such ugly code:
if (btnRead.InvokeRequired)
{
btnRead.Invoke((Action)(() => btnRead.Enabled = false));
}
else
{
btnRead.Enabled = false;
}
In other words, all the "light" code is executed by the called thread, whereas "heavy" code is delegated to a separate thread (I/O or CPU-bound). This approach allows us to significantly reduce the amount of work required to synchronize access to UI elements as they are always managed by the UI thread only.
Secondly, async/await feature can be used in web applications for better thread utilization. ASP.NET MVC team have done a lot of work making asynchronous controllers easy to implement. You can just write an action like the following and ASP.NET will do all of the rest work.
public class HomeController : Controller
{
public asyncTask<string > Index()
{
using (FileStream fs = new FileStream("File path", FileMode.Open))
using (StreamReader sr = new StreamReader(fs))
{
return await sr.ReadToEndAsync(); // 1
}
}
}
At the example above the worker thread executing the method starts a new I/O thread at the line "1" and returns to the thread pool. After the I/O thread finishes working, a new thread from the thread pool is picked up to continue method execution. Hence, CPU-bound threads from the thread pool are utilized more economically.
Async/await in C#: pitfalls
If you are developing a third-party library, it is always vital to configure await in such a way that the rest of the method will be executed by an arbitrary thread from the thread pool. First of all, third party libraries (if they are not UI libraries) usually don’t work with UI controls, so there’s no need to bind the UI thread. You can slightly increase performance by allowing CLR to execute your code by any thread from the thread pool. Secondly, by using the default implementation (or explicitly writing ConfigureAwait(true)), you leave a hole for possible deadlocks. Moreover, client code won’t be able to change this implementation. Consider the following example:
private asyncvoid button1_Click(object sender, EventArgs e)
{
int result = DoSomeWorkAsync().Result; // 1
}
private asyncTask<int > DoSomeWorkAsync()
{
awaitTask.Delay(100).ConfigureAwait(true); //2
return 1;
}
A button click leads to a deadlock here. The UI thread starts a new I/O thread at "2" and falls to sleep at "1", waiting for I/O work to be completed. After the I/O thread is done, it dispatches the rest of the DoSomeWorkAsync method to the thread the method was called by. But that thread is waiting for the method completion. Deadlock.
ASP.NET will behave the same way, because although ASP.NET doesn’t have a single UI thread, the code in controllers' actions can’t be executed by more than one thread simultaneously.
Of course, you could use await keyword instead of calling the Result property to avoid the deadlock:
private asyncvoid button1_Click(object sender, EventArgs e)
{
int result = awaitDoSomeWorkAsync();
}
private asyncTask<int > DoSomeWorkAsync()
{
awaitTask.Delay(100).ConfigureAwait(true);
return 1;
}
But there is still one case where you can’t avoid deadlocks. You can’t use async methods in ASP.NET child actions, because they are not supported. So you will have to access the Result property directly and will get a deadlock if the async method your controller calls didn’t configure Awater properly. For example, if you do something like the following and the SomeAction action calls an async method’s Result property and that method is not configured by ConfigureAwait(false) statement, you’ll get a deadlock.
@Html.Action("SomeAction", "SomeController")
Clients of your library won’t be able to change its code (unless they decompile it), so always put ConfigureAwait(false) in your async methods.
How you definitely shouldn’t use PLINQ and async/await
Look at the example:
private asyncvoid button1_Click(object sender, EventArgs e)
{
btnRead.Enabled = false;
string content = awaitReadFileAsync();
btnRead.Enabled = true;
}
private Task<string > ReadFileAsync()
{
return Task.Run(() => // 1
{
using (FileStream fs = new FileStream("File path", FileMode.Open))
using (StreamReader sr = new StreamReader(fs))
{
return sr.ReadToEnd(); // 2
}
});
}
Is this code asynchronous? Yes. Is it a correct way to write asynchronous code? No. The UI thread starts a new CPU-bound thread at "1" and returns. The new thread then starts a new I/O thread at "2" and falls to sleep waiting for its completion.
So, what happens here? Instead of creating just an I/O thread we create both CPU thread at "1" and I/O thread at "2". It’s a waste of threads. To fix it, we need to call the async version of the Read method:
private Task<string > ReadFileAsync()
{
using (FileStream fs = new FileStream("File path", FileMode.Open))
using (StreamReader sr = new StreamReader(fs))
{
return sr.ReadToEndAsync();
}
}
Here is another example:
public void SendRequests()
{
_urls.AsParallel().ForAll(url =>
{
var httpClient = new HttpClient();
httpClient.PostAsync(url, new StringContent("Some data"));
});
}
Looks like we are sending requests in parallel, right? Yes, we are, but there’s the same issue we had before: instead of creating just an I/O thread we create both I/O and CPU-bound threads for every request. To fix the code we need to use Task.WaitAll method:
public void SendRequests()
{
IEnumerable<Task > tasks = _urls.Select(url =>
{
var httpClient = new HttpClient();
return httpClient.PostAsync(url, new StringContent("Some data"));
});
Task.WaitAll(tasks.ToArray());
}
Is it always necessary to run IO operations without binding CPU-bound threads?
Well, it depends. Sometimes it’s impossible to do it, sometimes it brings too much of complexity. For example, NHibernate doesn’t implement asynchronous data fetching. EntityFramework, on the other hand, does, but there might not be a lot of sense in using it in some cases. You should always consider pros and cons of every design decision.
Also, thick clients (like WPF or WinForms) usually don’t have a lot of load, so there’s actually no difference in choosing one approach over another. But anyway, you should always know what is happening under the cover so you could make a conscious decision in every single case.
Further reading
Subscribe
Comments
comments powered by Disqus