Wrap Event based functions into awaitable Tasks - Meet TaskCompletionSource

6/20/2022
3 minute read

In the older days of .NET asynchronous programming was achieved via events. Not only back then but also new API's do use events. As a developer we sometimes faced with the following situation:

var myObject = CreateMyObject();
myObject.OnInitialized += DoSomethingWhenInitizialized();

// Hmm I would like to wait until myObject is really Initialized
myObject.MethodWhichNeedsTheObjectToBeInitialized();

The object tells the environment via an event when it is initialized and ready to use. Another great examples are timer. Let's say we want to wait 1.5 seconds and do something else in a non-block manner without using Task.Delay? You can work with while loops and nice boolean flags but that is not really readable.

That is why we have TaskCompletionSource plus it's generic overload. The main task is that you can control the result and in case of an error the exception of this type. To the outside world we will return the task it represents.

Case 1: Task.Delay

Let's start simple. We can build our own Task.Delay to better understand the principles behind the TaskCompletionSource.

static Task WaitAsync(int timeInMilliSeconds)
{
    var taskCompletionSource = new TaskCompletionSource();
    var timerCallback = new TimerCallback(_ => { });
    var timer = new Timer(timerCallback, null, timeInMilliSeconds, int.MaxValue);

    return taskCompletionSource.Task;
}

In this example we just created a Timer, which invokes its callback exactly once after the given time period. To the outside world we will return an await-table Task. That is super convenient. Well almost. Right now if you would use it like this: WaitAsync(100); it will wait indefinitely. We create the Task but we never tell the task object when it is done. So let's do that.

var timerCallback = new TimerCallback(_ => { taskCompletionSource.SetResult(); });

Given the whole example:

var stopwatch = Stopwatch.StartNew();
await WaitAsync(100);
Console.WriteLine(stopwatch.ElapsedMilliseconds);

static Task WaitAsync(int timeInMilliSeconds)
{
    var taskCompletionSource = new TaskCompletionSource();
    var timerCallback = new TimerCallback(_ => { taskCompletionSource.SetResult(); });
    var timer = new Timer(timerCallback, null, timeInMilliSeconds, int.MaxValue);

    return taskCompletionSource.Task;
}

This will print a number slightly higher than 100. Please don't use this exact code in your code base. For one Task.Delay already exists and two: That code above can lead to memory leaks as no-one cleans up the timer. I just wanted to show case how the TaskCompletionSource works.

Case 2: Results and exceptions

But there is more than just setting the result. We can also set an exception.

var bytesWritten = await UploadFileAsync("test.txt"); 

static Task<int> UploadFileAsync(string filename)
{
    var taskCompletionSource = new TaskCompletionSource<int>();
    var myUploader = new MyUploader();
    myUploader.Upload(filename,
        success => { taskCompletionSource.SetResult(success.BytesWritten); },
        error => { taskCompletionSource.SetException(new Exception($"Could not upload file {filename}")); });
    
    return taskCompletionSource.Task
}

Pitfalls

There is an excellent article from Sergey Tepliakov about the pitfalls of TaskCompletionSource. I will give just a short wrap-up. The examples shown above can lead to deadlocks due to the fact that the continuation-task wants to run in the same thread. We can eliminate that behaviour by explicitly passing TaskCreationOptions options to the constructor like this:

var taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);

Conclusion

TaskCompletionSource is a nice tool to transform already asynchronous functions into something await-able (for example events but also things like BeginInvoke).

Missing Stack trace when eliding the await keyword

You may have heard that when you elide the await keyword in a method that returns a Task or Task<T>, you lose the stack trace. Buy why does that happen? Let's find out!

ASP.NET Core - Why async await is useful

Did you ever wonder why you "should" use async and await in your ASP.NET Core applications? Most probably, you heard something about performance. And there is some truth to it, but not in the way you might think.

So let's discuss this with smaller examples.

Git-Flow, GitHub-Flow, Gitlab-Flow and Trunk Based Development explained

There are plenty of models how to do your branching in git. All of them are viable approaches with their respective pros and cons. So let's have a look at those branching strategies and where they are great and where they are falling off.

So let's deep dive into: Git-Flow, GitHub-Flow, Gitlab-Flow, Trunk Based Development.

An error has occurred. This application may no longer respond until reloaded. Reload x