Using TaskCompletionSource in C# to Delay Processing
I was recently faced with an issue where I needed the processing of a call to delay until a separate service had finished initialising. I managed to get it working by inserting an artificial delay using Task.Delay. That’s not a workable long-term solution. It’s unreliable at best and ends up with the time needing to increase as time goes on.
Unhappy with this, I searched for other options when I came across TaskCompletionSource. This provides a way to use async/await with a value that can be set. Let’s take a look at an example.
The Scenario
My scenario involved separate services but could apply to communication between different classes. To accommodate this I’ll be keeping the terminology vague. The scenario looked like this:
So here we have a request initiating, a processor that handles it, and a dependency that provides extra data. The problem arises when we don’t know when the dependency has the data ready.
The Solution
This is where TaskCompletionSource comes in. We can wait for a message from the dependency to tell us it’s got everything ready. Only after that message do we run a request for information. A simple implementation looks something like this:
public class DependencyReadyService
{
private readonly TaskCompletionSource<bool> _dependencyReadyTask = new TaskCompletionSource<bool>();
public async Task WaitForDependency()
{
await _dependencyReadyTask.Task;
}
public void SetDependencyReady()
{
_dependencyReadyTask.TrySetResult(true);
}
}
The processor can then use it like this:
public async Task ProcessRequest() {
await _dependencyReadyService.WaitForDependency();
// Make request to dependency
}
And to mark it as ready the handling for the notification from the dependency can call:
_dependencyReadyService.SetDependencyReady();
The updated sequence then looks like this:
Considerations
The above solution is a simplification to highlight the usage of TaskCompletionSource
. There are some things that need to be taken in to account when using an approach like this.
Requestor Expectations
In my scenario the requestor was making requests via a REST API. This means there is a risk of the request timing out and being cancelled if it takes too long. I was lucky because the requestor didn’t actually care about a response. I could put the wait on a separate thread and return an accepted message.
If the requestor needed the result of the processing I would have needed a different solution. So don’t blindly implement this solution and hope for the best. Be aware of the context and the expectations of other components.
Blocking Threads
The downside of this approach is you’re essentially holding a thread open for an indefinite amount of time. You need to be aware of which thread you will be blocking and how many requests will be trying to come through on different threads. You don’t want to risk crashing you’re application.
In my scenario it was fairly safe to run this because:
I’m pushing the request handling on to a separate thread so I’m not blocking anything on the caller
The results of the request being processed are not being relied on by the requestor
There will be very few of these requests coming through so there’s no chance of thousands coming in at once
To mitigate some of this you could modify the implementation to include a timeout. Either the dependency is ready in time or an exception is thrown.
Conclusion
TaskCompletionSource
has it’s uses but shouldn’t be implemented without thinking through the consequences. It’s good to know about and I’m sure there are plenty of other uses for it than just my scenario. It saved me having to rewrite vast parts of the codebase or implement state machines across a suite of services.
I’m sure there would be other solutions that would work here, but TaskCompletionSource
was easy to implement and worked exactly how I expected it to. Definitely something to bear in mind.