When working with async
/await
in C# you end up using the Task
class quite a bit. Aside from void
, which has specific use cases, Task
is the primary return type used with async
methods, along with the lesser used ValueTask
. It might be surprising, though, to learn that the similarly named TaskCompletionSource
is not an acceptable return type for async
methods. What is the purpose of TaskCompletionSource
, and how does it fit into the picture of C# asynchronous programming?
In short, the TaskCompletionSource
class is used for asyncification. Ok, admittedly that's not actually a real word, but you might intuit that we're talking about making something asynchronous.
"Doesn't
Task.Run
also make stuff asynchronous?"
Good question! While Task.Run
turns something synchronous into a Task
(by running it on a separate thread), TaskCompletionSource
turns something that is already asynchronous into a Task
.
"If it is already asynchronous, why does it need to be turned into a
Task
?"
Well, keep in mind that the Task
class is required to enable C#'s async
/await
language support, and Task
did not exist when .NET was first released. There are therefore other (some might say "legacy") ways to achieve asynchrony in C# that do not involve the Task
class, and are therefore not compatible with async
/await
. For example, there is the Asynchronous Programming Model, where you pass an AsyncCallback
into a BeginInvoke
method. There is also the Event-based Asynchronous Pattern, where you subscribe to an event that is raised upon completion. Both of these are forms of callbacks, where a method you provide is invoked after an operation is completed.
Regardless of the exact pattern used, you are likely to encounter a callback at some point in your C# career. When you do, you might find yourself wishing there was a way to convert callback code into a Task
that you can await
. Thankfully, the TaskCompletionSource
class lets you do exactly that! It follows, then, that TaskCompletionSource
is not itself awaitable, nor is it a valid async
method return type. Once the TaskCompletionSource
gives you a Task
, you can simply return that Task
as you do any other in your async
methods.
Let's look at a concrete example of using TaskCompletionSource
to replace a callback. In a previous guide we talked about an application that generates images; let's suppose you need your application to upload those images somewhere. Imagine there is a cloud file storage service called "MyBox" that has a .NET library, and the library method to upload a file is as follows:
1public static void UploadFile(string name, byte[] data, Action<bool> onCompleted);
Consulting the library's documentation, you find that onCompleted
is a callback invoked when the upload completes, with a value of true
if the upload succeeds and false
if the upload fails. To upload a file using this library method, you have to do something like the following, assuming your application has something in which to display text called statusText
:
1public async void OnUploadButtonClicked()
2{
3 statusText.Text = "Generating Image...";
4 byte[] imageData = await GenerateImage();
5 statusText.Text = "Uploading Image...";
6 MyBox.UploadFile("image.jpg", imageData, success =>
7 {
8 statusText.Text = success ? string.Empty : "Error Uploading";
9 });
10}
This works fine, but you would really prefer to use async
/await
instead of the callback approach, especially if you plan on using that library a lot in your application. You can do that by making a helper method that uses TaskCompletionSource
to hide the callback from callers of the helper method. First, define an instance of TaskCompletionSource
. It accepts a generic parameter representing the type of whatever-it-is you want to return. In this case, we want to return the value of the success
variable, which is a boolean, so use bool
as the generic parameter.
1var taskCompletionSource = new TaskCompletionSource<bool>();
Let's put that definition inside a helper class/method.
1public static class MyBoxHelper
2{
3 public static Task<bool> UploadFile(string name, byte[] data)
4 {
5 var taskCompletionSource = new TaskCompletionSource<bool>();
6 }
7}
Notice that we use the return type
Task<bool>
, in anticipation that we will be able to generate aTask
which returns a boolean using ourTaskCompletionSource
.
Zooming in on the method now, we also need to add the call to the "MyBox" library method, which will do the uploading:
1public static Task<bool> UploadFile(string name, byte[] data)
2{
3 var taskCompletionSource = new TaskCompletionSource<bool>();
4 MyBox.UploadFile(name, data, success => { });
5}
Next, we need to tell the TaskCompletionSource
when its operation has completed, and pass it the result of the operation. There is a SetResult
method in TaskCompletionSource
for that exact purpose. Not surprisingly, we must call that in the callback, when we know the result:
1public static Task<bool> UploadFile(string name, byte[] data)
2{
3 var taskCompletionSource = new TaskCompletionSource<bool>();
4 MyBox.UploadFile(name, data, success =>
5 {
6 taskCompletionSource.SetResult(success);
7 });
8}
Last, but not least, we need to ask the TaskCompletionSource
to give us a Task
. This turns out to be super easy since TaskCompletionSource
has a property called Task
! Just return that directly.
1public static Task<bool> UploadFile(string name, byte[] data)
2{
3 var taskCompletionSource = new TaskCompletionSource<bool>();
4 MyBox.UploadFile(name, data, success =>
5 {
6 taskCompletionSource.SetResult(success);
7 });
8 return taskCompletionSource.Task;
9}
To avoid having a Task
that never finishes, be sure to add exception handling so that the Task
can be completed if any problem arises. In the case of an exception, you can simply cancel using SetCanceled
(which would, in turn, throw a generic TaskCanceledException
), but, even better, would be to provide the exception that caused the problem using SetException
.
1public static Task<bool> UploadFile(string name, byte[] data)
2{
3 var taskCompletionSource = new TaskCompletionSource<bool>();
4 try
5 {
6 MyBox.UploadFile(name, data, success =>
7 {
8 taskCompletionSource.SetResult(success);
9 });
10 }
11 catch (Exception ex)
12 {
13 taskCompletionSource.SetException(ex);
14 }
15 return taskCompletionSource.Task;
16}
That's it! Now, whenever MyBox.UploadFile
calls the callback or throws an exception, the Task
generated by the TaskCompletionSource
will be completed to reflect that.
The important thing is that every possible outcome should be handled by your logic; if there were additional callbacks indicating finality (e.g. onCanceled, onTimeout) you would want to call a
Set*
method ofTaskCompletionSource
in each of those as well.
Once you have your helper method in place, you can now await
it anywhere in your code without having to use callbacks. Let's update our button click event handler:
1public async void OnUploadButtonClicked()
2{
3 statusText.Text = "Generating Image...";
4 byte[] imageData = await GenerateImage();
5 statusText.Text = "Uploading Image...";
6 bool success = await MyBoxHelper.UploadFile("image.jpg", imageData);
7 statusText.Text = success ? string.Empty : "Error Uploading";
8}
Wonderful! By hiding the implementation details of the callback inside your helper class, this code can now have a linear flow with less indentation, both benefits of using async
/await
.
If you use
SetCanceled
orSetException
, there is actually one more thing to do, which is to add atry
/catch
block wrapping the call toMyBoxHelper.UploadFile
. This is simply to avoid your application crashing in the case of aTaskCanceledException
or similar. Feel free to changeMyBoxHelper
so that it callsSetResult(false)
in its exception handler instead; in that case the caller would not need any exception handling.
The TaskCompletionSource
class can turn any asynchronous operation into a Task
, but one use case that developers often miss is that it can also be used for user interface interactions. For example, imagine you have a button with a confirmation dialog. To handle the result of the confirmation dialog, you would probably need code such as the following:
1public void OnDeleteButtonClicked()
2{
3 ShowModalDialog("Are you sure?");
4}
5
6public void ModalDialog_OkButtonClicked()
7{
8 HideModalDialog();
9 Delete();
10}
11
12public void ModalDialog_CancelButtonClicked()
13{
14 HideModalDialog();
15}
One useful aspect of Task
is that it has no built-in timeout mechanism, so it can be active any amount of time. That being the case, you can represent a user interface interaction (which is naturally asynchronous) as a Task
, and it can be awaited as long as the user needs. With TaskCompletionSource
, mapping a user interface interaction to a Task
can be just as easy as what we did for callbacks above. Doing that for a modal confirmation dialog, for example, would give us something like the following:
1public async void OnDeleteButtonClicked()
2{
3 if (await ModalDialogHelper.Show("Are you sure?"))
4 {
5 Delete();
6 }
7}
Not only is this less code, but it is also now easier to read and the intent is clear. Just be sure to handle all possible ways that the dialog can be dismissed so that the Task
is not "orphaned". For example, is the modal dialog dismissed if the user clicks behind it, or taps the Esc
key on their keyboard? If so, you'll need to call a Set*
method on the TaskCompletionSource
in those situations as well.
If you call more than one
Set*
method on aTaskCompletionSource
, by default an exception is thrown. This is so you can discover problems in your code since the fact that your code is setting multiple outcomes for theTaskCompletionSource
might indicate a bug. If that is undesirable, you can instead use the correspondingTrySet*
methods (e.g.TrySetResult
) which do not throw exceptions. This is particularly useful for user interactions since a button can sometimes get clicked multiple times in quick succession, for example.
Most applications have asynchronous aspects, but asynchronous code without async
/await
can quickly become difficult to follow and maintain. The TaskCompletionSource
class is a great way to convert such code into a Task
you can simply await
. It's a bit of additional work, but the result is much easier to read and use. Be sure to take full advantage of TaskCompletionSource
in your asynchronous C# code!