Содержит потокобезопасный в HashSet‹T›

Глядя на код для Contains в классе HashSet<T> в исходном коде .NET, я не могу найти причину, по которой Contains не является потокобезопасным?

Я заранее загружаю HashSet<T> со значениями, а затем проверяю Contains в многопоточном цикле .AsParallel().

Есть ли причина, по которой это не будет безопасно. Я не хочу использовать ConcurrentDictionary, когда мне не нужно хранить значения.


person Jim    schedule 10.03.2015    source источник
comment
Вы пишете на него один раз, а затем только читаете с него в нескольких потоках?   -  person Yuval Itzchakov    schedule 10.03.2015
comment
Содержит является потокобезопасным, пока вы ничего не добавляете/не удаляете из набора (пока вы используете содержит)   -  person nafas    schedule 10.03.2015
comment
Почему бы не прочитать инструкцию? msdn.microsoft.com/en-us/library/bb359438.aspx: Любые общедоступные статические (общие в Visual Basic) члены этого типа являются потокобезопасными. Потокобезопасность любых членов экземпляра не гарантируется.   -  person    schedule 10.03.2015
comment
MSDN не говорит, что это не потокобезопасно. Только они этого не гарантируют. Причина может заключаться в том, что он не тестировался или может измениться в некоторых будущих версиях.   -  person Ondra    schedule 10.03.2015
comment
@Ondra да и нет ... как мы все знаем, с .NET 4 параллельные коллекции введены (особенно по этой причине) ... так что можно с уверенностью сказать, что непараллельные коллекции не потокобезопасны! Также стоит прочитать: stackoverflow.com/questions/12585531/   -  person    schedule 10.03.2015
comment
@nafas Есть еще одна проблема ... Вы должны быть уверены, что после последней записи был MemoryBarrier, иначе чтение может прочитать некоторые неполные данные   -  person xanatos    schedule 10.03.2015
comment
@xanatos ну, я просто предположил, что для начала нет проблем, связанных с записью / удалением. но да, дела идут беспорядочно с записью, хе-хе   -  person nafas    schedule 10.03.2015
comment
@nafas Ярлыки и многопоточность всегда беспорядочны ... Как поход в магазин хрусталя.   -  person xanatos    schedule 10.03.2015
comment
@xanatos, ха-ха, да, я сбился со счета, сколько раз я бился головой об экран из-за глупых тем, которые делали то, что они хотят, а не то, о чем я их прошу ....   -  person nafas    schedule 10.03.2015


Ответы (3)


Обычно (обычно) коллекции, которые используются только для чтения, являются "неофициально" потокобезопасными (я знаю, что в .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()), выход из Wait() вызывает неявный Thread.MemoryBarrier()

Простой пример класса, который использует мемоизацию/ленивую загрузку/как бы вы это ни называли, и таким образом может нарушить потокобезопасность.

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;
    }
}
person xanatos    schedule 10.03.2015
comment
+1, хотя я бы больше подчеркнул неофициально. Вполне возможно, что операции чтения не будут потокобезопасными, например, из-за мемоизации, хотя в данном конкретном случае это не так, AFAICT. - person Jon Hanna; 10.03.2015
comment
@JonHanna Я добавил Тем не менее GetHashCode() и Equals() должны быть потокобезопасными. Если они, например, обращаются к ленивым объектам, которые загружаются по запросу, они могут быть небезопасными для потоков или, возможно, кэшировать данные для ускорения последующих операций - person xanatos; 10.03.2015
comment
Да, я больше думаю об обобщении; ничто из того, что вы здесь говорите, не является чем-то неправильным, но я прекрасно вижу, как кто-то читает это и пропускает нормальное. Нужно действительно изучить код, о котором идет речь, чтобы убедиться, что ваш ответ применим. - person Jon Hanna; 10.03.2015

Учитывая, что вы заранее загружаете свой набор значений, вы можете использовать ImmutableHashSet<T> из библиотеки System.Collections.Immutable. неизменяемые коллекции рекламируют себя как потокобезопасный, поэтому нам не нужно беспокоиться о «неофициальной» безопасности потоков HashSet<T>.

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
{
 ...
}
person Jonathan Little    schedule 12.10.2018
comment
да, это хороший совет, и я бы сейчас им воспользовался. если память не изменяет, их не было около 4 лет назад. это был прочитанный один раз, проверенный многими сценариями, и он содержит готовую работу. Сейчас я бы не рекомендовал его — есть варианты получше. Я прочитал исходный код, и это было на самом деле нормально. - person Jim; 03.12.2019

От Microsoft: Thread-Safe Collections

В .NET Framework 4 представлено пространство имен System.Collections.Concurrent, которое включает несколько классов коллекций, которые являются потокобезопасными и масштабируемыми. Несколько потоков могут безопасно и эффективно добавлять или удалять элементы из этих коллекций, не требуя дополнительной синхронизации в пользовательском коде. Когда вы пишете новый код, используйте классы параллельных коллекций всякий раз, когда несколько потоков будут одновременно записывать в коллекцию. Если вы читаете только из общей коллекции, вы можете использовать классы в пространстве имен System.Collections.Generic. Мы рекомендуем вам не использовать классы коллекций 1.0, если только вам не требуется ориентироваться на . NET Framework 1.1 или более ранней версии среды выполнения.

Поскольку Contains не изменяет коллекцию, это просто операция чтения, а поскольку HashSet находится в System.Collections.Generic, одновременный вызов Contains абсолютно допустим.

person wezten    schedule 01.12.2019