【问题标题】:Can I remove different items from List / Dictionary concurrently without locking?我可以在不锁定的情况下同时从列表/字典中删除不同的项目吗?
【发布时间】:2020-04-06 19:36:07
【问题描述】:

假设我有以下代码:

class Test
{
    Dictionary<int, Task> tasks;

    void Run()
    {
        tasks.Add(1, DoStuff(1));
        tasks.Add(2, DoStuff(2));
        tasks.Add(3, DoStuff(3));
    }

    async Task DoStuff(int id)
    {
        try
        {
            // Do stuff that takes a lot of time and can end unsuccessfully.
        }
        finally
        {
            tasks.Remove(id);
        }
    }
}

如果我确定该任务只能删除它自己并且所有 id 都会不同(例如新生成的 guid) - 我可以保留此代码吗?还是我需要某种锁定或在这里使用 ConcurrentDictionary 因为常规 Dictionary 或 List 的内部在同时访问时可能会失败? (假设有 10000 个这样的任务,它们都同时结束)

另外:没有人会阅读这个字典/列表或遍历它。它仅用于保留对后台任务的引用(因此 GC 不会收集它们)。如果您对该用例有更好的建议,欢迎提出建议:)

【问题讨论】:

  • 你不需要加锁就可以从the documentation中移除...:"如果Dictionary&lt;TKey,TValue&gt;不包含具有指定key的元素,Dictionary&lt;TKey,TValue&gt;保持不变. 不会抛出异常。” 然而,这并不意味着它是线程安全的。
  • "仅限 .NET Core 3.0+:可以安全地调用此变异方法,而不会使 Dictionary 实例上的活动枚举数无效。这并不意味着线程安全。"阅读最后一行...
  • 打错了,谢谢。我会修复

标签: c# asynchronous collections concurrency thread-safety


【解决方案1】:

你肯定需要某种锁定机制。就个人而言,我会为此使用 ConcurrentDictionary。或者,根据您在此处尝试完成的任务,您可以这样做:

var list = new List<Task>();

list.Add(DoStuff(1));
list.Add(DoStuff(2));
list.Add(DoStuff(3));

await Task.WhenAll(list);

这将确保在您继续之前完成所有任务。

***编辑 - 更新

如果您希望将它们从列表中连续删除 - 根据您的查看方式,您可以这样做:

private List<Task> _list = new List<Task>();

...

_list.Add(DoStuff(1));
_list.Add(DoStuff(2));
_list.Add(DoStuff(3));

然后,当您需要查看剩余数量时,您可以这样做:

list.Count(a => !a.IsCompleted);

本质上,IsCompleted 仅在线程完成完成时才为真。但是,您可能还需要检查 IsFaulted 或 IsCanceled,具体取决于这些任务正在执行的操作。

***编辑更新 2

在添加到此列表的线程上,您可以毫无顾虑地删除它们:

_list.Add(DoStuff(4));
_list.Add(DoStuff(5));

_list = _list.Where(a => !a.IsCompleted).ToList();

如果您不想弄乱实际的变量引用,您可以执行一个向后的 for 循环并手动删除,只要另一个线程没有添加到此列表中。

【讨论】:

  • 为什么需要锁定?
  • 我需要它们在后台运行,并让调用方法继续执行而不等待它们。它们是由 while 循环连续添加的,所以我不能等待。我只需要确保任何任务完成后都会从列表中删除。
  • 当您从字典中删除时,有某种基础集合,如果您在中间删除,您的索引可能会再次超出范围。每当您使用线程时,请始终锁定需要改变任何内容的所有内容。
  • @DanielLorenz 谢谢,是的,我已经考虑过了。我可以不时调用该方法并删除所有已完成的任务。我只是认为一旦任务完成就删除任务的解决方案更漂亮,但如果这将包括强制锁定,那么我还不确定。无论如何,我怀疑某些底层连接可能被线程搞砸了,感谢您确认!
  • @EwanCoder 如果您只有一个线程添加新线程,则同一线程可以删除已完成的有风险的线程。只有当您尝试在子线程上删除它们时,您才会遇到问题。如果您愿意,请参阅更新 2! :)
【解决方案2】:

Dictionary 类不是线程安全的,因此如果您打算通过多个线程并行操作它,您肯定需要使用lock 保护它。否则类的内部状态可能会损坏。

关于性能,这应该不是问题。从Dictionary 中删除项目非常快,因此如果您仅在此操作期间持有lock,则锁的争用应该几乎不存在。仅当您预计每秒约 100,000 次字典操作或更多的频率时,您才应该开始担心。在这种情况下,明智的做法是切换到 ConcurrentDictionary,因为它的粒度锁定实现可以更好地处理不断增加的争用。

但您的实现中存在竞争条件。 Task 在创建后被添加到字典中,因此理论上它可能会在它出现之前尝试将自己删除。如果发生这种情况,您最终可能会将已完成的任务永远留在字典中。要以优雅的方式解决这个问题并不容易。

【讨论】:

  • 嗯,非常感谢!没想到频率这么高。但我没想到比赛条件!感谢您指出。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-05-09
  • 1970-01-01
  • 2021-05-12
  • 2019-01-04
  • 1970-01-01
  • 2010-11-29
  • 2022-07-26
相关资源
最近更新 更多