c# - 如何强制 C# Task 对于​​给定 ID 一次仅存在一次

标签 c# async-await task

在不同的应用程序中,我多次需要使用 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/

相关文章:

c# - 字节数组转int16数组

c# - 让一些私有(private)方法公开 IQueryable<T> 而所有公共(public)方法公开 IEnumerable<T> 有什么问题吗?

c# - 如何在 ASP.NET MVC 中获取模型的 HtmlHelper<TModel> 实例?

Flutter ImagePicker - 在 onPressed 中异步获取图像显示 lint 警告

c# - 带有可取消 EventArgs 的 INotifyPropertyChanging EventHandler 中的异步/等待

c# - 如何在 Silverlight 中使用新的印度卢比符号?

javascript - 使用 async/await 和 then() 如何将结果保存在数组中

java - Gradle:复制子项目资源

android - 任务、后退按钮和 onSaveInstanceState 方法

java - 如何从JavaFX中的Service handler方法获取Task实例