DEV Community

Steven Boyd-Thompson
Steven Boyd-Thompson

Posted on • Originally published at drownedintech.dev on

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:

Scenario sequence diagram

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

The processor can then use it like this:

public async Task ProcessRequest() {
        await _dependencyReadyService.WaitForDependency();
        // Make request to dependency
}
Enter fullscreen mode Exit fullscreen mode

And to mark it as ready the handling for the notification from the dependency can call:

_dependencyReadyService.SetDependencyReady();
Enter fullscreen mode Exit fullscreen mode

The updated sequence then looks like this:

Solution sequence diagram

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.

Top comments (3)

Collapse
 
ghadzhigeorgiev profile image
Georgi Hadzhigeorgiev

Can you fix those broken images, please

Collapse
 
drownedintech profile image
Steven Boyd-Thompson

Should be sorted now

Collapse
 
ghadzhigeorgiev profile image
Georgi Hadzhigeorgiev

Yep, cheers!