在不同的应用程序中,我多次需要使用 C# Task 完成以下行为,并且我以某种方式完成了它,并且希望了解它是否是实现预期效果的最佳方式,或者还有其他更好的方法。
问题是,在某些情况下,我希望特定任务仅存在于一个实例中。例如,如果有人通过执行类似 Task GetProductsAsync()
的方法来请求产品列表,而其他人试图请求相同的东西,它不会触发另一个任务,而是返回已经存在的任务。当 GetProductsAsync
完成时,所有先前请求结果的调用者都将收到相同的结果。因此,在给定的时间点应该只有一次 GetProductsAsync
执行。
在尝试找到类似且众所周知的设计模式来解决此问题的尝试失败后,我想出了自己的实现。这是它
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);
}
}
}
}
我们的想法是,在整个应用程序生命周期中,我们将拥有一个 TaskManager
实例。任何应在给定时间点只执行一次的异步任务请求将调用 ExecuteOnceAsync
提供工厂方法来创建任务本身,以及所需的应用程序范围内的唯一 ID。任何其他具有相同 ID 的任务,任务管理器会回复之前创建的相同任务实例。只有当没有其他任务具有该 ID 时,管理器才会调用工厂方法并启动任务。我在代码任务创建和删除周围添加了锁,以确保线程安全。此外,为了在任务完成后从存储的字典中删除任务,我使用 ContinueWith
方法添加了一个延续任务。因此,任务完成后,任务本身及其延续都将被删除。
在我看来,这似乎是一个很常见的场景。我假设有一个完善的设计模式,或者 C# API 可以完成同样的事情。因此,我们将不胜感激任何见解或建议。
最佳答案
我认为您可以使用一个有用的并发类来做到这一点。
它仅在无法获取任何现有工作时才开始工作,然后等待它完成。如果它之前已经运行过,它将获取已经完成(或正在进行)的任务并等待它完成。
// Add this static dictionary to your class
static readonly ConcurrentDictionary<string, Task> tasks = new();
// Add this to your doing something once method in that class
var work = this.tasks.GetOrAdd(taskId, _ =>
{
return client.DoSomethingAsync();
});
await work;
重要
请参阅下面 Theodor 关于委托(delegate)在锁外运行的评论。这很不幸,可以追溯到我 10 多年前制作的并发词典的设计(并向 Microsoft 提出一种更好的 ConcurrentDictionary 方法)。
我有自己的线程安全字典,用这个方法。
bool TryAdd(K key, T value, out T contains);
这个简单的设计返回了新添加的或现有的值,并提供了一些很棒的模式,特别是对于缓存/请求重复数据删除;添加任务以获取某物与等待现有任务来获取它。
我注意到 ConcurrentDictionary
有一个 TryAdd
但它在失败时不会返回现有值,这很遗憾,但这样的事情可能会起作用:
static readonly ConcurrentDictionary<string, Task<Task>> tasks = new();
//
var newTask = new Task<Task>(() => DoSomethingAsync());
if (this.tasks.TryAdd(taskId, newTask))
{
newTask.Start();
}
var somethingTask = await this.tasks[taskId];
await somethingTask;
它假定任务永远不会被删除。
基本上有一个外部任务,它只能由比赛获胜者启动。如果正在完成的工作是异步的,例如创建文件或网络资源,则需要在其中包含另一个任务。
希望这个人好。
关于c# - 如何强制 C# Task 对于给定 ID 一次仅存在一次,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54573594/