C#从一个线程设置字典对象是否线程安全,从另一个线程获取

C# Is it threadsafe to set dictionary object from one thread, get from other thread

假设我们有一些 strange class 其中包含

从一个线程调用方法 Update 并从另一个线程获取 Map 是否线程安全?

public class Example
{
    public ReadOnlyDictionary<string, string> Map{ get; private set; }

    public void Update(IEnumerable<KeyValuePair<string, string>> items)
    {
        Map = items
            .ToLookup(x => x.Key)
            .ToDictionary(x => x.Key, x => x.First())
            .ToReadOnlyDictionary(); // helper method
    }
}

是否有可能会出现我们调用 Map getter 而它 returns 为 null 或处于不良状态的情况,因为 setter 执行不是原子的?

"Thread safe" 是一个模糊的术语。维基百科将 thread-safe 代码定义为:

Thread-safe code only manipulates shared data structures in a manner that ensures that all threads behave properly and fulfill their design specifications without unintended interaction

所以我们不能在不知道设计规范的情况下说某段代码是 "thread safe",其中包括这段代码的实际使用方式。

从某种意义上说,这段代码是 "thread-safe",任何线程都不可能观察到处于损坏或部分状态的 Example 对象。

引用分配是原子的,因此读取 Map 的线程只能读取它的一个版本或另一个版本,它不能观察 Map 的部分或中间状态。因为 Map 的键和值都是不可变的(字符串)——整个对象是不可变的,并且在从 Map 属性.[=30= 中读取后可以安全地被多个线程使用]

然而,做这样的事情:

var example = GetExample();
if (example.Map.ContainsKey("key")) {
    var key = example.Map["key"];
}

当然不是线程安全的,因为 Map 对象作为一个整体,可能在检查和从字典中读取之间发生了变化,旧版本包含 "key" 键,但新版本版本没有。另一方面这样做:

var example = GetExample();
var map = example.Map;
if (map.ContainsKey("key")) {
    var key = map["key"];
}

很好。任何不依赖 example.Map 在读取之间保持相同的代码都应该是 "safe".

因此您必须确保给定代码在您的特定用例中是 "thread safe"。

注意这里使用ConcurrentDictionary是完全没用的。由于您的字典是只读的 - 从多个线程读取它已经很安全了。

更新:评论中提出的有效观点是 ReadOnlyDictionary 本身并不能保证基础数据不被修改,因为 ReadOnlyDictionary 只是常规可变 Dictionary 的包装器.

但是,在这种情况下,您在 Update 方法中从 non-shared 字典实例(从 ToDictionary 接收)创建 ReadOnlyDictionary,它不用于任何其他用途除了将它包装在 ReadOnlyDictionary 中。这是一种安全的用法,因为除了 ReadOnlyDictionary 本身之外,没有任何代码可以访问可变的底层字典,并且只读字典会阻止其修改。只要没有写入,即使是常规的 Dictionary 读取也是线程安全的。

because of setter execution is not atomic

此代码的安全性与原子性无关(read/write 引用是 .NET 中的原子操作,除非你搞砸了对齐)。
因此,此代码中唯一的问题是内存重新排序。另一个线程可能会获得对部分构造的对象的引用。
但它可能只发生在内存模型较弱的硬件上('weak' 意味着允许大量重新排序并且没有太多限制)。
为防止这种情况,您只需通过 Volatile.Write 发布此参考,对于 x86 和 x86_64 架构是 nop,对于 ARM 和其他架构是一种内存栅栏。

为了使您的代码更简单和更少 bug-prone 您可以使用 ImmutableDictionary:

public class Example
{
    public IReadOnlyDictionary<string, string> Map{ get; private set; } = 
        ImmutableDictionary.Create<string, string>();

    public void Update(IEnumerable<KeyValuePair<string, string>> items)
    {
        Map = items
            .ToImmutableDictionary();
    }
}

ImmutableDictionary 保证是线程安全的,因此无法破坏它。通常的 Dictionary 不是线程安全的,但是在您的实现中不应该有任何线程安全问题,但这需要这种特殊用法。

ImmutableDictionary 可以在这里下载 https://www.nuget.org/packages/System.Collections.Immutable/

另一个 thread-safe 字典是 ConcurrentDictionary。使用它不一定会产生额外的成本,因为读取操作是 lock-free。文档指出:

Read operations on the dictionary are performed in a lock-free manner.

所以这两个字典在任何情况下都是 thread-safe(在字典 API 级别)并且 Dictionary 在某些实现中是 thread-safe你的。

并发集合 = 线程安全:)

我认为你应该使用并发收集。并发泛型对我来说很有意义。