В настоящее время я просматриваю копию -в реализации набора для записи -и хочу подтвердить, что она потокобезопасна. Я почти уверен, что единственный способ, которым это может быть не так, - это если компилятору разрешено переупорядочивать операторы в определенных методах. Например, метод Remove
выглядит как:
public bool Remove(T item)
{
var newHashSet = new HashSet(hashSet);
var removed = newHashSet.Remove(item);
hashSet = newHashSet;
return removed;
}
Где hashSet определяется как
private volatile HashSet hashSet;
Итак, мой вопрос: учитывая, что hashSet равен volatile
, означает ли это, что Remove
в новом наборе происходит до записи в переменную-член? Если нет, то другие потоки могут увидеть набор до того, как произойдет удаление.
На самом деле я не видел никаких проблем с этим в производстве, но я просто хочу подтвердить, что это гарантированно безопасно.
ОБНОВЛЕНИЕ
Чтобы быть более конкретным, есть еще один способ получитьIEnumerator
:
public IEnumerator GetEnumerator()
{
return hashSet.GetEnumerator();
}
Итак, более конкретный вопрос: :есть ли гарантия, что возвращенный IEnumerator
никогда не вызовет ConcurrentModificationException
из удаления?
ОБНОВЛЕНИЕ 2
Извините, все ответы касаются безопасности потоков от нескольких авторов. Хорошие очки подняты,но это не то, что я пытаюсь выяснить здесь. Я хотел бы знать, разрешено ли компилятору -упорядочивать операции в Remove
примерно так:
var newHashSet = new HashSet(hashSet);
hashSet = newHashSet; // swapped
var removed = newHashSet.Remove(item); // swapped
return removed;
Если бы это было возможно, это означало бы, что поток мог вызвать GetEnumerator
после того, как hashSet
было назначено, но до того, как item
было удалено, что могло привести к изменению коллекции во время перечисления.
У Джо Даффи есть статья в блоге , в которой говорится:
Volatile on loads means ACQUIRE, no more, no less. (There are additional compiler optimization restrictions, of course, like not allowing hoisting outside of loops, but let’s focus on the MM aspects for now.) The standard definition of ACQUIRE is that subsequent memory operations may not move before the ACQUIRE instruction; e.g. given { ld.acq X, ld Y }, the ld Y cannot occur before ld.acq X. However, previous memory operations can certainly move after it; e.g. given { ld X, ld.acq Y }, the ld.acq Y can indeed occur before the ld X. The only processor Microsoft.NET code currently runs on for which this actually occurs is IA64, but this is a notable area where CLR’s MM is weaker than most machines. Next, all stores on.NET are RELEASE (regardless of volatile, i.e. volatile is a no-op in terms of jitted code). The standard definition of RELEASE is that previous memory operations may not move after a RELEASE operation; e.g. given { st X, st.rel Y }, the st.rel Y cannot occur before st X. However, subsequent memory operations can indeed move before it; e.g. given { st.rel X, ld Y }, the ld Y can move before st.rel X.
Насколько я понимаю, для вызова newHashSet.Remove
требуется ld newHashSet
, а для записи в hashSet
требуется st.rel newHashSet
. Из приведенного выше определения RELEASE никакие грузы не могут перемещаться после RELEASE сохранения, поэтому операторы не могут быть переупорядочены ! Может ли кто-нибудь подтвердить, пожалуйста, подтвердите, что моя интерпретация верна?