There were multiple times in different applications that I needed to accomplish the following behavior with C# Task and I did it in a certain way, and would like to receive an insight whether it's the best way to achieve the desired effect, or there are other better ways.
The issue is that in certain circumstances I would like a specific Task to exist only in one instance. For example, if someone requests, let's say a list of products by executed a method like Task GetProductsAsync()
, and someone else tries to request the same thing, it wouldn't fire another task, but rather return already existing task. When the GetProductsAsync
finishes, all of those callers who had previously requested the result will receive the same result. So, there should ever be only one GetProductsAsync
execution at a given point of time.
After failed trials to find something similar and well known design pattern to solve this issue, I came up with my own implementation. Here is it
public class TaskManager : ITaskManager
{
private readonly object _taskLocker = new object();
private readonly Dictionary<string, Task> _tasks = new Dictionary<string, Task>();
private readonly Dictionary<string, Task> _continuations = new Dictionary<string, Task>();
public Task<T> ExecuteOnceAsync<T>(string taskId, Func<Task<T>> taskFactory)
{
lock(_taskLocker)
{
if(_tasks.TryGetValue(taskId, out Task task))
{
if(!(task is Task<T> concreteTask))
{
throw new TaskManagerException($"Task with id {taskId} already exists but it has a different type {task.GetType()}. {typeof(Task<T>)} was expected");
}
else
{
return concreteTask;
}
}
else
{
Task<T> concreteTask = taskFactory();
_tasks.Add(taskId, concreteTask);
_continuations.Add(taskId, concreteTask.ContinueWith(_ => RemoveTask(taskId)));
return concreteTask;
}
}
}
private void RemoveTask(string taskId)
{
lock(_taskLocker)
{
if(_tasks.ContainsKey(taskId))
{
_tasks.Remove(taskId);
}
if(_continuations.ContainsKey(taskId))
{
_continuations.Remove(taskId);
}
}
}
}
The idea is that we will have a single instance of TaskManager
throughout the application lifetime. Any async Task request that should be executed only once at a given point in time, will call ExecuteOnceAsync
providing the factory method to create the Task itself, and desired application wide unique ID. Any other task that will come in with the same ID, the Task manager with reply with the same instance of Task created before. Only if there are no other Tasks with that ID, the manager will call the factory method and will start the task. I have added locks around code task creation and removal, to ensure thread safety. Also, in order to remove the task from the stored dictionary after Task has been completed, I've added a continuation task using ContinueWith
method. So, after task has been completed, both the task itself, and its continuation will be removed.
From my side this seems to be a pretty common scenario. I would assume there is a well established design pattern, or perhaps C# API that accomplishes this exact same thing. So, any insights or suggestions will be very appreciated.