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

时间:2018-03-26 08:44:32

标签: c# .net thread-safety volatile

假设我们有一些包含

奇怪的
  • 公共财产ReadOnlyDictionary<string, string> Map {get; private set;}
  • 方法Update,在调用时重新设置Map字典。

从一个线程调用方法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并返回null或处于错误状态的时刻,因为setter执行不是原子的?

4 个答案:

答案 0 :(得分:3)

&#34;线程安全&#34;是一个模糊的术语。维基百科将线程安全代码定义为:

  

线程安全代码仅以某种方式操作共享数据结构   确保所有线程都能正常运行并完成其设计   没有意外互动的规范

所以我们不能说某段代码是&#34;线程安全&#34;不知道设计规范,包括如何实际使用该代码。

此代码是&#34;线程安全&#34;从某种意义上说,任何线程都无法在损坏或部分状态下观察Example对象。

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

然而,做这样的事情:

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

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

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

很好。任何不依赖于example.Map在读取之间保持相同的代码应该是&#34;安全&#34;。

所以你必须确保给定的代码是&#34;线程安全&#34;在你的特定用例中。

请注意,使用ConcurrentDictionary在这里绝对没用。由于您的字典是只读的 - 从多个线程中读取它是安全的。

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

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

答案 1 :(得分:0)

  

因为setter执行不是原子的

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

答案 2 :(得分:0)

为了使您的代码更简单,更容易出错,您可以使用 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/

另一个线程安全字典是 ConcurrentDictionary 。使用它可能不一定提供额外的成本,因为读取操作是无锁的。文档说明:

  

字典上的读取操作以无锁方式执行。

所以这两个词典在任何情况下都是线程安全的(在字典API级别上),而 Dictionary 在某些类似于你的实现中是线程安全的。

答案 3 :(得分:0)

Concurrent Collections =线程安全:)

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