使用固定键在字典上进行多线程

本文关键字:多线程 字典 | 更新日期: 2023-09-27 18:35:37

我有一个字典,其中包含固定的键集合,我在程序开始时创建。后来,我有一些线程用值更新字典。

  • 线程启动后,不会添加或删除任何对。
  • 每个线程都有自己的密钥。 这意味着,只有一个线程将访问某个密钥
  • 线程可能会更新该值

问题是,我应该锁定字典吗?

更新:

谢谢大家的回答,

当我问这个问题时,我试图简化情况,只是为了理解字典的行为。

为了清楚起见,以下是完整版本:我有一个包含 ~3000 个条目(固定键)的字典,并且我有多个线程访问密钥(共享资源),但我知道一个事实,一次只有一个线程访问一个键条目。

那么,我应该锁定字典吗? 而且 - 当你现在有完整版本时,字典是正确的选择吗?

谢谢!

使用固定键在字典上进行多线程

来自MSDN

一个词典可以同时支持多个读取器,只要集合不被修改。

若要允许多个线程访问集合以进行读取和写入,必须实现自己的同步。

有关线程安全的替代方法,请参阅ConcurrentDictionary<TKey, TValue>.

让我们一次

处理一个解释的问题。

第一种解释:考虑到Dictionary<TKey, TValue>是如何实现的,根据我给出的上下文,我需要锁定字典吗?

不,你没有。

第二种解释:鉴于Dictionary<TKey, TValue是如何记录的,根据我给出的上下文,我是否需要锁定字典?

是的,你绝对应该。

不能保证在多线程世界中,访问(今天可能没问题)明天也可以,因为该类型被记录为非线程安全。这允许程序员对类型的状态和完整性做出某些假设,否则他们将不得不构建保证。

对 .NET 或全新版本的修补程序或更新可能会更改实现并使其中断,这是您依赖未记录行为的错。

第三种解释:鉴于我给出的上下文,字典是正确的选择吗?

不,不是。要么切换到线程安全类型,要么干脆不使用字典。为什么不直接使用每个线程的变量呢?

结论:如果您打算使用字典,请锁定字典。如果可以切换到其他内容,请执行。

使用ConcurrentDictionary,不要重新发明轮子。

更好的是,重构代码以避免这种不必要的争用。


如果线程之间没有通信,您可以执行以下操作:

假设一个改变值的函数。

private static KeyValuePair<TKey, TValue> ValueChanger<TKey, TValue>(
        KeyValuePair<TKey, TValue> initial)
{
    // I don't know what you do so, i'll just return the value.
    return initial;
}

假设您有一些起始数据,

var start = Enumerable.Range(1, 3000)
                .Select(i => new KeyValuePair<int, object>(i, new object()));

你可以像这样一次处理它们,

var results = start.AsParallel().Select(ValueChanger);

评估results时,所有 3000 个ValueChangers将同时运行,从而产生IEnumerable<KeyValuePair<int, object>>

线程之间不会有交互,因此没有可能的并发问题。

如果你想把结果变成一个Dictionary你可以,

var resultsDictionary = results.ToDictionary(p => p.Key, p => p.Value);

这在您的情况下可能有用,也可能没有用,但是,如果没有更多细节,很难说。

如果每个线程只访问一个"值",如果你不关心其他人,我会说你根本不需要字典。您可以使用 ThreadLocal 或 ThreadStatic 变量。

如果你需要一把Dictionary你肯定需要一把锁。

如果您使用的是.Net 4.0或更高版本,我强烈建议您使用ConcurrentDictionary,使用ConcurrentDictionary时不需要同步访问,因为它已经是"ThreadSafe"。

Diectionary 不是线程安全的,但在您的代码中您不必这样做; 你说一个线程更新一个值,所以你没有多线程问题!我没有代码,所以我不确定 100%。

还要检查这个:使字典访问线程安全?

如果您不是要添加键,而只是修改值,为什么不通过将复杂对象存储为值并修改复杂类型中的值来完全消除直接写入字典的需要。这样,您就可以遵守字典的线程安全约束。

所以:

class ValueWrapper<T>
{
    public T Value{get;set;}
}
//...
var myDic = new Dictionary<KeyType, ValueWrapper<ValueType>>();
//...
myDic[someKey].Value = newValue;

您现在不直接写入字典,但可以修改值。

不要尝试对密钥执行相同的操作。它们应该是不可变

鉴于约束"我知道一次只有一个线程访问密钥条目",我认为您没有任何问题。

Dictionary的可能修改包括:添加、更新和删除。

  1. 如果修改或允许修改Dictionary,则必须使用所选的同步机制来消除潜在的争用条件,其中一个线程读取旧的脏值,而另一个线程当前正在替换该值/更新键.
    若要保护某些工作,请使用此方案中的ConcurentDictionary

  2. 如果Dictionary在创建后从未修改,则不会有任何争用条件。因此不需要同步。
    这是一种特殊方案,您可以在其中将表替换为只读表。若要添加重要的健壮性,例如通过意外操作表来防范潜在的错误,应使Dictionary不可变(或只读)。为了给开发人员编译器提供支持,这种不可变的实现必须在任何操作尝试时引发异常.
    为了保护你的一些工作,你可以在此方案中使用ReadOnlyDictionary。请注意,ReadOnlyDictionary的基础Dictionary仍然是可变的,并且其更改将传播到ReadOnlyDictionary外观。ReadOnlyDictionary仅有助于确保表不会被其使用者意外修改。

这意味着:Dictionary在多线程上下文中永远不是一个选项.
而是使用ConcurrentDictionary或一般的同步机制(或者,如果可以保证原始源集合永远不会更改,则使用ReadOnlyDictionary)。

由于您允许并期望对表进行操作("[...]线程可能会更新值"),因此必须使用所选的同步机制或ConcurrentDictionary