C# 围绕volatile的操作重新排序

C# 围绕volatile的操作重新排序,c#,volatile,lock-free,C#,Volatile,Lock Free,我目前正在研究写集拷贝实现,并希望确认它是线程安全的。我敢肯定,唯一不可能的方法是允许编译器在某些方法中对语句重新排序。例如,Remove方法如下所示: public bool Remove(T item) { var newHashSet = new HashSet<T>(hashSet); var removed = newHashSet.Remove(item); hashSet = newHashSet; return removed; }

我目前正在研究写集拷贝实现,并希望确认它是线程安全的。我敢肯定,唯一不可能的方法是允许编译器在某些方法中对语句重新排序。例如,
Remove
方法如下所示:

public bool Remove(T item)
{
    var newHashSet = new HashSet<T>(hashSet);
    var removed = newHashSet.Remove(item);
    hashSet = newHashSet;
    return removed;
}
因此,更具体的问题是:是否可以保证返回的
IEnumerator
不会从删除中抛出
ConcurrentModificationException

更新2

抱歉,这些答案都是针对多个写入程序的线程安全性的。有人提出了一些好的观点,但这不是我想在这里找到的。我想知道是否允许编译器将
Remove
中的操作重新排序为以下内容:

    var newHashSet = new HashSet<T>(hashSet);
    hashSet = newHashSet;                  // swapped
    var removed = newHashSet.Remove(item); // swapped
    return removed;
var newHashSet=newHashSet(HashSet);
hashSet=newHashSet;//交换了的
var removed=newHashSet.Remove(项);//交换了的
移除返回;
如果这是可能的,则意味着线程可以在分配
hashSet
之后,但在删除
项之前调用
GetEnumerator
,这可能导致在枚举期间修改集合

Joe Duffy有一个声明:

Volatile on load意味着获取,不多也不少。(有 当然,还有其他编译器优化限制,比如 允许在环外提升,但让我们关注MM方面 ACQUIRE的标准定义是 存储器操作不能在获取指令之前移动;例如 给定{ld.acq X,ld Y},ld Y不能出现在ld.acq X之前。 然而,以前的内存操作肯定会在它之后移动;例如 给定{ldx,ld.acq Y},ld.acq Y确实可以出现在ld之前 X.当前运行的唯一处理器Microsoft.NET代码 这实际上发生在IA64上,但这是CLR MM比大多数机器都弱。接下来,将发布.NET上的所有商店 (不考虑volatile,即volatile在jitted方面是不可操作的 代码)。释放的标准定义是先前的内存 释放操作后,操作不得移动;e、 g.给定{st X, st.rel Y},st.rel Y不能出现在st X之前。但是, 后续的内存操作确实可以在它之前移动;e、 g.给定{ st.rel X,ld Y},ld Y可以在st.rel X之前移动

我的理解是,调用
newHashSet.Remove
需要
ld newHashSet
,写入
hashSet
需要
st.rel newHashSet
。从上面的释放定义来看,在存储释放之后,没有负载可以移动,因此语句不能重新排序!有人能确认我的解释是正确的吗?

考虑使用-它将保证订购,或者作为一个附带好处,它将让您检测(并可能从)对集合的同时写入。显然,它增加了一些额外的同步级别,因此它不同于您当前的代码,但更易于推理

public bool Remove(T item) 
{ 
    var old = hashSet;
    var newHashSet = new HashSet<T>(old); 
    var removed = newHashSet.Remove(item); 
    var current = Interlocked.CompareExchange(ref hashSet, newHashSet, old);
    if (current != old)
    {
       // failed... throw or retry...
    }
    return removed; 
} 
public bool Remove(T项)
{ 
var old=hashSet;
var newHashSet=新HashSet(旧);
var removed=newHashSet.Remove(项);
var current=联锁的。比较交换(参考哈希集,新哈希集,旧);
如果(当前!=旧)
{
//失败…抛出或重试。。。
}
移除返回;
} 
我认为在这种情况下,您仍然需要
volatile hashSet

我不能代表C#,但在C中volatile原则上表示并且仅表示变量的内容可以随时更改。它不提供有关编译器或CPU重新排序的约束。您得到的只是编译器/CPU总是从内存中读取值,而不是信任缓存版本

然而,我相信在最近的MSVC(很可能是C#)中,读取一个volatile作为加载的记忆屏障,写入作为存储的记忆屏障,例如,CPU将暂停,直到所有加载完成,并且没有加载可以通过在volatile read下重新排序而逃脱(尽管稍后的独立加载仍可能在内存屏障上方向上移动);CPU将暂停,直到are存储完成,并且没有存储可以通过在易失性写入下方重新排序来逃避此限制(尽管稍后的独立写入仍可能在内存屏障上方向上移动)

当只有一个线程写入给定变量(但许多线程正在读取)时,正确的操作只需要内存屏障。当多个线程可能写入给定变量时,必须使用原子操作,因为CPU设计基本上存在写入竞争条件,因此写入可能会丢失。

编辑:

感谢您澄清了要删除的调用(和其他集合突变)的外部锁的存在

由于RELEASE语义,在变量
removed
的值被赋值之前(因为
st removed
不能在
st.rel hashSet
之后移动),您不会将新值存储到
hashSet


因此,
GetEnumerator
的“快照”行为将按预期工作,至少在删除和以类似方式实现的其他变体方面是如此。

volatile
将允许更少的重新排序。在这一点上,您的代码是安全的。但此解决方案永远不能被认为是线程安全的,而且它看起来不像copy-on-write。你能详细解释一下为什么你认为它不是写时拷贝吗?它不完整,但因为hashSet是私有的,所以它看起来像是写时替换。我想我可以理解它的意图。但是你只需要将它称为Remove()是的,我同意。我在评论中提到,写入集合的内容需要序列化。为了澄清,你是说只是将
hashSet
标记为
vola吗
    var newHashSet = new HashSet<T>(hashSet);
    hashSet = newHashSet;                  // swapped
    var removed = newHashSet.Remove(item); // swapped
    return removed;
public bool Remove(T item) 
{ 
    var old = hashSet;
    var newHashSet = new HashSet<T>(old); 
    var removed = newHashSet.Remove(item); 
    var current = Interlocked.CompareExchange(ref hashSet, newHashSet, old);
    if (current != old)
    {
       // failed... throw or retry...
    }
    return removed; 
}