【问题标题】:Is Contains thread safe in HashSet<T>在 HashSet<T> 中是否包含线程安全
【发布时间】:2015-03-10 09:52:38
【问题描述】:

查看.NET 源代码中HashSet&lt;T&gt; 类中Contains 的代码,我找不到任何Contains 不是线程安全的原因?

我正在提前加载带有值的HashSet&lt;T&gt;,然后在多线程.AsParallel() 循环中检查Contains

这有什么不安全的原因吗? 当我实际上不需要存储值时,我不愿意使用ConcurrentDictionary

【问题讨论】:

  • 你是写一次,然后只在多个线程中读取吗?
  • Contains 是线程安全的,只要您不从集合中添加/删除任何内容(当您使用 contains 时)
  • 为什么不阅读手册? msdn.microsoft.com/en-us/library/bb359438.aspx: 这种类型的任何公共静态(在 Visual Basic 中为共享)成员都是线程安全的。不保证任何实例成员都是线程安全的。
  • MSDN 并没有说它是“非线程安全的”。他们只是不保证。原因可能是它没有经过测试,或者它可能会随着一些未来的版本而改变
  • @nafas 还有一个问题...你一定要确定最后一次写入后有MemoryBarrier,否则读取可能会读取到一些不完整的数据

标签: c# multithreading hashset


【解决方案1】:

通常通常)仅用于阅读的集合是“非官方”线程安全的(我知道 .NET 中没有集合在阅读期间会自行修改)。有一些注意事项:

  • 项目本身不能是线程安全的(但是对于HashSet&lt;T&gt;,这个问题应该被最小化,因为你不能从中提取项目。仍然GetHashCode()Equals() 必须是线程安全的。例如,如果它们访问按需加载的惰性对象,它们可能不是线程安全的,或者它们缓存/记忆一些数据以加速后续操作)
  • 您必须确保在最后一次写入之后有 Thread.MemoryBarrier()(在与写入相同的线程中完成)或等效项,否则在另一个线程上读取可能会读取不完整的数据
  • 您必须确保在每个线程(不同于您执行写入的线程)中,在执行第一次读取之前有一个Thread.MemoryBarrier()。请注意,如果 HashSet&lt;T&gt; 在创建/启动其他线程之前已“准备好”(最后使用 Thread.MemoryBarrier()),则不需要 Thread.MemoryBarrier(),因为线程不能过时读取内存(因为它们不存在)。各种操作会导致隐含的Thread.MemoryBarrier()。例如,如果在填充HashSet&lt;T&gt; 之前创建的线程,进入Wait() 并且在填充HashSet&lt;T&gt; 之后是un-Waited(加上它的Thread.MemoryBarrier()),退出Wait() 会导致隐式@987654335 @

一个使用记忆化/延迟加载/任何你想调用它的类的简单示例,这样会破坏线程安全。

public class MyClass
{
    private long value2;

    public int Value1 { get; set; }

    // Value2 is lazily loaded in a very primitive
    // way (note that Lazy<T> *can* be used thread-safely!)
    public long Value2
    {
        get
        {
            if (value2 == 0)
            {
                // value2 is a long. If the .NET is running at 32 bits,
                // the assignment of a long (64 bits) isn't atomic :)
                value2 = LoadFromServer();

                // If thread1 checks and see value2 == 0 and loads it,
                // and then begin writing value2 = (value), but after
                // writing the first 32 bits of value2 we have that
                // thread2 reads value2, then thread2 will read an
                // "incomplete" data. If this "incomplete" data is == 0
                // then a second LoadFromServer() will be done. If the
                // operation was repeatable then there won't be any 
                // problem (other than time wasted). But if the 
                // operation isn't repeatable, or if the incomplete 
                // data that is read is != 0, then there will be a
                // problem (for example an exception if the operation 
                // wasn't repeatable, or different data if the operation
                // wasn't deterministic, or incomplete data if the read
                // was != 0)
            }

            return value2;
        }
    }

    private long LoadFromServer()
    {
        // This is a slow operation that justifies a lazy property
        return 1; 
    }

    public override int GetHashCode()
    {
        // The GetHashCode doesn't use Value2, because it
        // wants to be fast
        return Value1;
    }

    public override bool Equals(object obj)
    {
        MyClass obj2 = obj as MyClass;

        if (obj2 == null)
        {
            return false;
        }

        // The equality operator uses Value2, because it
        // wants to be correct.
        // Note that probably the HashSet<T> doesn't need to
        // use the Equals method on Add, if there are no
        // other objects with the same GetHashCode
        // (and surely, if the HashSet is empty and you Add a
        // single object, that object won't be compared with
        // anything, because there isn't anything to compare
        // it with! :-) )

        // Clearly the Equals is used by the Contains method
        // of the HashSet
        return Value1 == obj2.Value1 && Value2 == obj2.Value2;
    }
}

【讨论】:

  • +1 虽然我会更多地强调“非官方”。例如,由于记忆化,读取操作完全有可能不是线程安全的,尽管在这种特殊情况下,AFAICT 并非如此。
  • @JonHanna 我添加了一个 Still GetHashCode() 并且 Equals() 必须是线程安全的。例如,如果它们访问按需加载的惰性对象,它们可能不是线程安全的,或者可能缓存数据以加速后续操作
  • 是的,我想更多的是概括;您在这里说的任何内容都不是错误的,但是我完全可以看到有人在阅读此内容,并跳过了“正常”。确实需要检查相关代码以确保您的答案适用。
【解决方案2】:

鉴于您提前使用值加载集合,您可以使用来自System.Collections.Immutable 库的ImmutableHashSet&lt;T&gt;immutable collections 宣传自己为thread safe,所以我们不必担心HashSet&lt;T&gt; 的“非官方”线程安全。

var builder = ImmutableHashSet.CreateBuilder<string>(); // The builder is not thread safe

builder.Add("value1");
builder.Add("value2");

ImmutableHashSet<string> set = builder.ToImmutable();

...

if (set.Contains("value1")) // Thread safe operation
{
 ...
}

【讨论】:

  • 是的,这是个好建议,也是我现在要使用的。如果没记错的话,他们不是在 4 年前。这是一次阅读多次检查的场景,并且包含开箱即用的工作。我现在推荐它 - 有更好的选择。看了源码,其实还可以。
【解决方案3】:

来自微软:Thread-Safe Collections

.NET Framework 4 引入了 System.Collections.Concurrent 命名空间,其中包括多个线程安全和可扩展的集合类。多个线程可以安全有效地从这些集合中添加或删除项目,而无需在用户代码中进行额外的同步。编写新代码时,只要多个线程同时写入集合,就使用并发集合类。 如果您只是从共享集合中读取,那么您可以使用 System.Collections.Generic 命名空间中的类。我们建议您不要使用 1.0 集合类,除非您需要将 . NET Framework 1.1 或更早的运行时。

由于Contains没有修改集合,它只是一个读取操作,而且由于HashSetSystem.Collections.Generic中,并发调用Contains是绝对可以的。

【讨论】:

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