我目前正在查看一个写时复制集实现,并想确认它是线程安全的。我相当确定它可能不是的唯一方法是允许编译器在某些方法中重新排序语句。例如,该Remove
方法如下所示:
public bool Remove(T item)
{
var newHashSet = new HashSet<T>(hashSet);
var removed = newHashSet.Remove(item);
hashSet = newHashSet;
return removed;
}
其中 hashSet 定义为
private volatile HashSet<T> hashSet;
所以我的问题是,鉴于 hashSet 是否volatile
意味着Remove
新集合上的 发生在写入成员变量之前?如果没有,那么其他线程可能会在删除发生之前看到该集合。
我实际上在生产中没有看到任何问题,但我只是想确认它是安全的。
更新
更具体地说,还有另一种获取 的方法IEnumerator
:
public IEnumerator<T> GetEnumerator()
{
return hashSet.GetEnumerator();
}
所以更具体的问题是:是否可以保证返回的对象IEnumerator
永远不会ConcurrentModificationException
从删除中抛出一个?
更新 2
抱歉,答案都是针对多个作者的线程安全问题。提出了很好的观点,但这不是我想在这里找到的。我想知道是否允许编译器将操作重新排序为Remove
如下所示:
var newHashSet = new HashSet<T>(hashSet);
hashSet = newHashSet; // swapped
var removed = newHashSet.Remove(item); // swapped
return removed;
如果这是可能的,则意味着线程可以在分配GetEnumerator
之后调用hashSet
,但之前item
被删除,这可能导致在枚举期间修改集合。
Joe Duffy 有一篇博客文章指出:
Volatile on load 意味着 ACQUIRE,不多也不少。(当然还有额外的编译器优化限制,比如不允许在循环之外进行提升,但现在让我们关注 MM 方面。) ACQUIRE 的标准定义是后续内存操作可能不会在 ACQUIRE 指令之前移动;例如,给定 { ld.acq X, ld Y },ld Y 不能出现在 ld.acq X 之前。但是,之前的内存操作肯定可以在它之后移动;例如,给定 { ld X, ld.acq Y },ld.acq Y 确实可以出现在 ld X 之前。当前运行的唯一实际发生这种情况的处理器 Microsoft .NET 代码是 IA64,但这是一个值得注意的领域CLR的MM比大部分机器都弱。接下来,.NET 上的所有存储都是 RELEASE(不管 volatile,即 volatile 就 jitted 代码而言是无操作的)。RELEASE 的标准定义是之前的内存操作在 RELEASE 操作之后可能不会移动;例如,给定 { st X, st.rel Y },st.rel Y 不能出现在 st X 之前。但是,后续的内存操作确实可以在它之前移动;例如,给定 { st.rel X, ld Y },ld Y 可以在 st.rel X 之前移动。
我读这个的方式是调用newHashSet.Remove
需要 ald newHashSet
和写入hashSet
需要 a st.rel newHashSet
。从上面的 RELEASE 定义来看,在 store RELEASE 之后没有负载可以移动,所以语句不能重新排序!有人可以确认请确认我的解释是正确的吗?