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