【问题标题】:C#: volatile reads and writes of HashSetC#:HashSet 的 volatile 读写
【发布时间】:2019-02-14 12:20:35
【问题描述】:

我有一堂课:

public class Checker
{
    private HashSet<int> _hs = new HashSet<int>();

    public bool Check(int a)
    {
        return Volatile.Read(ref _hs).Contains(a);
    }

    public void Update(IEnumerable<int> items)
    {
        Volatile.Write(ref _hs, new HashSet<int>(items));
    }
}

方法Check 经常被多个线程调用。方法Update 是从监视某些源(数据库、http 服务等)的单个线程调用的。这种Volatile.Read / Volatile.Write 的使用模式正确吗?

【问题讨论】:

  • 如果是同一个结构,一个元素可以被修改,而另一个线程读取它,那就有问题了。你需要在写作时让读者远离。信号量似乎是最佳解决方案,或者如果散列的类型允许原子行为,也应该适用于作者。它会锁定读者。
  • @LoveTätting 在所呈现的场景中,读者和作者无法竞争;作者创建一个 new 哈希集,然后在完成后交换引用;读者总是会得到一个不再被变异的哈希集,无论是“旧”还是“新”
  • 啊,谢谢指出,我误会了

标签: c# .net multithreading volatile


【解决方案1】:

如果您的意思是“Check 将始终使用该字段的最新版本”,那么是的,作为波动的副作用,这将是这种情况 - 并且交换整个参考比经常更便宜同步(.NET 确保您不能有损坏的引用,因此保证引用交换是原子的)。

注意:这种情况下的线程安全严格取决于哈希集在创建和交换引用后不会发生变异的事实,这就是代码中发生的情况在问题中。

不过,通过将字段声明为volatile,您可以更方便地获得相同的结果:

public class Checker
{
    private volatile HashSet<int> _hs = new HashSet<int>();

    public bool Check(int a) => _hs.Contains(a);

    public void Update(IEnumerable<int> items) => _hs = new HashSet<int>(items);
}

【讨论】:

  • 我又想回答太快,误解了问题。看起来你是对的;虽然我不确定Contains() 是否保证是(或保持)线程安全的。当然取决于实施。在 .NET Framework 4.7 上使用 JustDecompile 快速浏览一下,一目了然。
  • @CodeCaster 我必须同意你是对的,因为文档没有做出任何声明不会改变状态;从技术上讲,它可以进行某种延迟散列或动态再平衡;实际上:它没有 - 但是是的,我承认这不是类型的正式保证