.NET中的可变新鲜度保证(易失性与易失性读取)

我读过很多关于volatile和VoletileRead(ReadAcquireFence)的矛盾信息(msdn,SO等)。

我理解那些内存访问重新排序的限制含义 – 我仍然完全混淆的是新鲜度保证 – 这对我来说非常重要。

msdn doc for volatile提及:

(…)这可确保始终在字段中显示最新值。

用于volatile字段的msdn doc提及:

读取volatile字段称为volatile读取。 易失性读取具有“获取语义”; 也就是说,保证在指令序列之后的任何内存引用之前发生。

VolatileRead的.NET代码是:

public static int VolatileRead(ref int address) { int ret = address; MemoryBarrier(); // Call MemoryBarrier to ensure the proper semantic in a portable way. return ret; } 

根据msdn MemoryBarrier doc内存屏障阻止重新排序。 然而,这似乎对新鲜度没有任何影响 – 对吗?

那怎样才能获得保鲜? 标记字段volatile与使用VolatileRead和VolatileWrite语义访问它有区别吗? 我目前正在执行我的性能关键代码,需要保证新鲜度,但读者有时会得到陈旧的价值。 我想知道如果标记状态不稳定会使情况不同。

EDIT1:

我正在努力实现的目标 – 保证读者线程将尽可能获得共享变量的最新值(由多个编写者编写) – 理想情况下不会超过上下文切换或其他可能推迟立即操作的操作的成本写国家。

如果挥发性或更高级别的构造(例如锁定)具有这种保证(他们是吗?),那么他们如何实现这一目标?

EDIT2:

非常简洁的问题应该是 – 如何在读取过程中尽可能保证新的价值 ? 理想情况下没有锁定(因为不需要独占访问,并且存在高争用的可能性)。

从我在这里学到的东西,我想知道这可能是解决方案(求解(?)行标有注释):

 private SharedState _sharedState; private SpinLock _spinLock = new SpinLock(false); public void Update(SharedState newValue) { bool lockTaken = false; _spinLock.Enter(ref lockTaken); _sharedState = newValue; if (lockTaken) { _spinLock.Exit(); } } public SharedState GetFreshSharedState { get { Thread.MemoryBarrier(); // <---- This is added to give readers freshness guarantee var value = _sharedState; Thread.MemoryBarrier(); return value; } } 

添加了MemoryBarrier调用以确保 – 读取和写入 – 都被完全围栏包裹(与锁定代码相同 – 如此处所示http://www.albahari.com/threading/part4.aspx#_The_volatile_keyword ‘内存屏障和锁定’ 部分)

这看起来是正确的还是有缺陷的?

EDIT3:

感谢非常有趣的讨论,我学到了很多东西,实际上我能够提炼出关于这个主题的简单明确的问题。 它与原版的完全不同,所以我宁愿在这里发布一个新内容: 内存屏障vs内存缓冲对内存的影响一致性时序

我认为这是一个很好的问题。 但是,它也很难回答。 我不确定我能否给你一个明确的答案。 这不是你的错。 只是主题很复杂,并且确实需要知道可能无法枚举的细节。 老实说,看起来你已经很好地了解了这个问题。 我花了很多时间自己研究这个主题,但我仍然不完全理解一切。 不过,无论如何,我仍然会在这里尝试一些相似的答案。

那么线程无论如何都要读取新值是什么意思呢? 这是否意味着读取返回的值保证不超过100ms,50ms或1ms? 或者它是否意味着价值绝对是最新的? 或者它是否意味着如果两次读取背靠背发生,那么假设第一次读取后内存地址发生了变化,第二次保证会获得更新的值? 或者它完全意味着什么呢?

如果您在时间间隔方面考虑事情,我认为您将很难让读者正常工作。 而是根据将读取链接在一起时发生的事情来考虑事物。 为了说明我的观点,请考虑如何使用任意复杂的逻辑实现类似互锁的操作。

 public static T InterlockedOperation(ref T location, T operand) { T initial, computed; do { initial = location; computed = op(initial, operand); // where op is replaced with a specific implementation } while (Interlocked.CompareExchange(ref location, computed, initial) != initial); return computed; } 

在上面的代码中,如果我们利用通过Interlocked.CompareExchange进行的第二次location读取将保证在第一次读取后接收到写入的内存地址时返回更新的值这一事实,我们可以创建任何类似互锁的操作。 这是因为Interlocked.CompareExchange方法会生成内存屏障。 如果读取之间的值发生了变化,则代码会反复循环,直到location停止变化。 此模式不要求代码使用最新最新的值; 只是一个更新的价值。 区别至关重要。 1

我见过的许多无锁代码都适用于这个主体。 也就是说,操作通常被包装成循环,这样操作就会不断重试,直到成功为止。 它不假设第一次尝试使用最新值。 它也不假设每次使用价值都是最新的 。 它只假设每次读取后该值更新

尝试重新思考读者应该如何表现。 尽量让他们对价值的年龄更加不了解。 如果这根本不可能并且必须捕获并处理所有写入,那么您可能会被迫采用更确定的方法,例如将所有写入放入队列并让读者逐个出列。 我确信ConcurrentQueue类在这种情况下会有所帮助。

如果你可以将“fresh”的含义简化为“newer”,那么在每次读取后调用Thread.MemoryBarrier ,使用Volatile.Read ,使用volatile关键字等将绝对保证序列中的一个读取将返回比以前读取的值更新


1 ABA问题开辟了一种新的蠕虫病毒。

内存屏障确实提供了这种保证。 我们可以从屏障保证的重新定义属性中获取您正在寻找的“新鲜度”属性。

通过新鲜度,您可能意味着读取返回最近写入的值。

假设我们有这些操作,每个操作都在不同的线程上:

 x = 1 x = 2 print(x) 

我们怎么可能打印2以外的值? 如果没有易失性,读取可以向上移动一个插槽并返回1.但是,易失性可以防止重新排序。 写不能及时倒退。

简而言之,volatile可以保证您查看最新值。

严格来说,我需要在这里区分易失性和内存障碍。 后者是一个更有力的保证。 我简化了这个讨论,因为volatile是使用内存屏障实现的,至少在x86 / x64上是这样。