【发布时间】:2023-04-09 23:24:01
【问题描述】:
在不同的应用程序中,我多次需要使用 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 可以完成同样的事情。因此,任何见解或建议将不胜感激。
【问题讨论】:
-
难道不能使用 Autofac 等 IoC 容器来实现吗?使用单个实例生命周期范围注册特定的 Task
?然后,您只需通过构造函数请求所需的任务,无需了解或担心其生命周期管理。 -
感谢@olitee 的回复。这实际上可能是可能的。但我认为它需要几乎与我相同的代码 - 比如识别任务,通过 ID 解决它,然后当它完成处理它时。所以,我需要让这件作品继续,才能删除任务。然后,我需要锁定继续和任务创建部分,因为这些操作应该是原子的。你怎么看?
-
现有API可以考虑使用
Lazy<T>。不利的一面是,您必须在Lazy<T>变量中为您只想执行一次的每个操作保留引用。 -
@DmytroMukalov 好点。可能使用 Lazy 任务只能解决工厂部分,不是吗?我仍然需要跟踪引用并将它们从列表中删除吗?
-
尝试在任务级别解决这个问题似乎真的很容易出错和竞争条件。如果您需要结果缓存,只需在中间放置一个实际缓存并让(仍然不同的)任务从那里获取结果。我想说,发生应该合并(而不是序列化)的实际并发请求的情况根本不常见——这表明在更高级别上存在一个问题,即发出了应该合并的不必要的请求在其他地方,控制并行度更容易。
标签: c# async-await task