问题 在HashSet 中包含线程安全


看代码 Contains 在里面 HashSet<T> 在.NET源代码中的类,我找不到任何理由 Contains 不是线程安全的吗?

我正在加载一个 HashSet<T> 提前使用值,然后检查 Contains 在多线程中。AsParallel() 循环。

这有什么理由不安全吗? 我不愿意使用 ConcurrentDictionary 当我实际上不需要存储值。


6106
2018-03-10 09:52


起源

你是在写它一次,然后只在多线程中读取它吗? - Yuval Itzchakov
包含是线程安全的,只要您不从集合中添加/删除任何内容(当您使用包含时) - nafas
为什么不阅读手册? msdn.microsoft.com/en-us/library/bb359438.aspx: 此类型的任何公共静态(在Visual Basic中为Shared)成员都是线程安全的。任何实例成员都不保证是线程安全的。 - Andreas Niedermair
MSDN并没有说它“不是线程安全的”。他们只是不保证。原因可能是它未经过测试,或者可能会在未来的某些版本中发生变化 - Ondra
@nafas还有另一个问题......你必须确保在最后一次写入后有一个MemoryBarrier,否则读取可能会读取一些不完整的数据 - xanatos


答案:


一般 (一般)仅用于读取的集合是“非正式”线程安全的(.NET中没有我知道在读取期间修改自身的集合)。有一些警告:

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

使用memoization / lazy loading /任何你想要调用它的类的简单示例可以打破线程的安全性。

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;
    }
}

12
2018-03-10 10:09



+1虽然我会更加强调“非正式”。例如,由于记忆,读取操作完全不可能是线程安全的,尽管在这种特殊情况下不是这种情况,AFAICT。 - Jon Hanna
@JonHanna我添加了一个 GetHashCode()和Equals()必须是线程安全的。例如,如果他们访问按需加载的延迟对象,则它们可能不是线程安全的,或者可能是缓存数据以加速后续操作 - xanatos
是的,我正在考虑更多的概括;没有你在这里说的是远程错误,但我完全可以看到有人读这篇文章,并跳过“正常”。一个人真的需要检查有问题的代码,以确保您的答案适用。 - Jon Hanna