【问题标题】:C# Is it threadsafe to set dictionary object from one thread, get from other threadC#从一个线程设置字典对象是否线程安全,从另一个线程获取
【发布时间】:2018-03-26 19:19:16
【问题描述】:

假设我们有一些包含

strange
  • 公共财产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 执行不是原子的?

【问题讨论】:

  • 取决于您对Example 实例的处理,您在使用MapSet 时是否lock
  • @TimSchmelter no
  • Map 是一个引用类型,对引用的赋值是原子的——所以它的一部分是线程安全的。但是,如果另一个线程正在修改 itemsUpdate() 正在运行,那么您将度过一段糟糕的时光。
  • @sleepy 引用分配保证是原子的。 Map 将返回旧值或 ToReadOnlyDictionary 返回的新值。
  • 您将遇到的唯一问题是,使用它的代码是否依赖于每次访问属性时都获得相同的Map。例如,它可能使用ContainsKey 然后[] 来查找密钥。从技术上讲,这不是线程安全问题 - 但它可能会引发异常(因为第一个 Map 可能有某个键,但第二个 Map 可能没有)。

标签: c# .net thread-safety volatile


【解决方案1】:

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

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

所以我们不能在不知道设计规范的情况下说某段代码是“线程安全的”,其中包括该代码的实际使用方式。

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

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

但是,做这样的事情:

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 在读取之间保持不变的代码都应该是“安全的”。

因此,您必须确保给定代码在您的特定用例中是“线程安全的”。

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

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

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

【讨论】:

  • “请注意,在这里使用 ConcurrentDictionary 绝对没用。因为您的字典是只读的 - 从多个线程中读取它已经是安全的”ReadOnlyDictionary 只是一个包装器原始字典不是线程安全的,因此Example 根本不是线程安全的,因为Mappublic
  • @TimSchmelter 我不同意。此代码中的ReadOnlyDictionary 不是从共享可变字典的某些实例创建的。它是使用ToDictionary().ToReadOnlyDictionary() 调用创建的,来自非共享的一次性字典实例。所以可变版本只存储在ReadOnlyDictionary 内部,这可以防止它的变异。
  • 忽略了,真的
【解决方案2】:

因为setter执行不是原子的

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

【讨论】:

    【解决方案3】:

    并发集合 = 线程安全 :)

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

    【讨论】:

      【解决方案4】:

      为了使您的代码更简单且不易出错,您可以使用 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 在像你这样的一些实现中是线程安全的。

      【讨论】:

        猜你喜欢
        • 2013-01-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-02-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多