Author avatar

Nate Cook

Task vs. TaskCompletionSource in C#

Nate Cook

  • Apr 12, 2019
  • 11 Min read
  • 88 Views
  • Apr 12, 2019
  • 11 Min read
  • 88 Views
Hosted
C#

Not to Be Confused

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?

"Asyncification" With TaskCompletionSource

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.

Replacing a Callback With TaskCompletionSource

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:

1
public static void UploadFile(string name, byte[] data, Action<bool> onCompleted);
csharp

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:

1
2
3
4
5
6
7
8
9
10
public async void OnUploadButtonClicked()
{
  statusText.Text = "Generating Image...";
  byte[] imageData = await GenerateImage();
  statusText.Text = "Uploading Image...";  
  MyBox.UploadFile("image.jpg", imageData, success => 
  {
    statusText.Text = success ? string.Empty : "Error Uploading";
  });
}
csharp

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.

1
var taskCompletionSource = new TaskCompletionSource<bool>();
csharp

Let's put that definition inside a helper class/method.

1
2
3
4
5
6
7
public static class MyBoxHelper
{
  public static Task<bool> UploadFile(string name, byte[] data)
  {
    var taskCompletionSource = new TaskCompletionSource<bool>();
  }
}
csharp

Notice that we use the return type Task<bool>, in anticipation that we will be able to generate a Task which returns a boolean using our TaskCompletionSource.

Zooming in on the method now, we also need to add the call to the "MyBox" library method, which will do the uploading:

1
2
3
4
5
public static Task<bool> UploadFile(string name, byte[] data)
{
  var taskCompletionSource = new TaskCompletionSource<bool>();
  MyBox.UploadFile(name, data, success => { });
}
csharp

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:

1
2
3
4
5
6
7
8
public static Task<bool> UploadFile(string name, byte[] data)
{
  var taskCompletionSource = new TaskCompletionSource<bool>();
  MyBox.UploadFile(name, data, success => 
  {
    taskCompletionSource.SetResult(success);
  });
}
csharp

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.

1
2
3
4
5
6
7
8
9
public static Task<bool> UploadFile(string name, byte[] data)
{
  var taskCompletionSource = new TaskCompletionSource<bool>();
  MyBox.UploadFile(name, data, success => 
  {
    taskCompletionSource.SetResult(success);
  });
  return taskCompletionSource.Task;
}
csharp

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static Task<bool> UploadFile(string name, byte[] data)
{
  var taskCompletionSource = new TaskCompletionSource<bool>();
  try
  {
    MyBox.UploadFile(name, data, success => 
    {
      taskCompletionSource.SetResult(success);
    });
  }
  catch (Exception ex)
  {
    taskCompletionSource.SetException(ex);
  }
  return taskCompletionSource.Task;
}
csharp

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 of TaskCompletionSource 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:

1
2
3
4
5
6
7
8
public async void OnUploadButtonClicked()
{
  statusText.Text = "Generating Image...";
  byte[] imageData = await GenerateImage();
  statusText.Text = "Uploading Image...";  
  bool success = await MyBoxHelper.UploadFile("image.jpg", imageData);
  statusText.Text = success ? string.Empty : "Error Uploading";
}
csharp

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 or SetException, there is actually one more thing to do, which is to add a try/catch block wrapping the call to MyBoxHelper.UploadFile. This is simply to avoid your application crashing in the case of a TaskCanceledException or similar. Feel free to change MyBoxHelper so that it calls SetResult(false) in its exception handler instead; in that case the caller would not need any exception handling.

Other Uses of TaskCompletionSource

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void OnDeleteButtonClicked()
{
  ShowModalDialog("Are you sure?");
}

public void ModalDialog_OkButtonClicked()
{
  HideModalDialog();
  Delete();
}

public void ModalDialog_CancelButtonClicked()
{
  HideModalDialog();
}
csharp

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:

1
2
3
4
5
6
7
public async void OnDeleteButtonClicked()
{
  if (await ModalDialogHelper.Show("Are you sure?"))
  {
    Delete();
  }
}
csharp

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 a TaskCompletionSource, 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 the TaskCompletionSource might indicate a bug. If that is undesirable, you can instead use the corresponding TrySet* 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.

Take Advantage

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!

1