.NET中可变的新鲜度保证(易失性与易失性读取)如何实现?

内容来源于 Stack Overflow,并遵循CC BY-SA 3.0许可协议进行翻译与使用

  • 回答 (2)
  • 关注 (0)
  • 查看 (10)

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

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

msdn文件的易变的提及:

(...)这确保了最新的值始终存在于现场。

MSDN文件为易变的字段提及:

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

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语义访问它有区别吗?我目前正在做我需要保证新鲜度的性能关键代码中的后者,然而读者有时会得到过时的价值。我想知道如果标记状态变化会使情况不同。

我试图达到的目标 - 保证读者线程将尽可能获得共享变量(由多个编写者编写)的最新值 - 理想情况下不超过上下文切换或其他可能推迟立即执行的操作的开销写入状态。

如果挥发性或更高级别的构造(例如锁定)具有这种保证(是吗?)而不是如何实现这一点?

这个非常浓缩的问题应该是 - 如何在读取过程中获得最新的价值保证?理想情况下不需要锁定(因为不需要独占访问并且存在高争用的可能性)。

从我在这里学到的东西我想知道这是否可能是解决方案(解决(?)行标注了评论):

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 '内存屏障和锁定' 部分)

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

提问于
用户回答回答于

我认为这是一个很好的问题。但是,这也很难回答。我不确定我可以给你一个明确的答案你的问题。这真的不是你的错。只是主题是复杂的,而且确实需要知道可能不可能列举的细节。说实话,这看起来好像你已经很好地教育了你自己。我花了很多时间研究这个主题,但我仍然没有完全理解所有的东西。尽管如此,无论如何,我仍然会在这里尝试一些类似的答案。

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

我认为如果你正在考虑时间间隔的事情,你将很难让你的读者正确地工作。相反,当将阅读连接在一起时,请考虑一些事情。为了说明我的观点,请考虑如何使用任意复杂的逻辑来实现类似互锁的操作。

public static T InterlockedOperation<T>(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;
}

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

我见过的很多无锁代码都适用于这个委托人。也就是说,这些操作通常会封装到循环中,以便操作不断重试直到成功。它不假定第一次尝试使用最新值。它也不会假定每一次使用该值都是最新的。它只假定在每次读取之后该值较新

试着重新思考你的读者应该如何表现。试着让他们对价值的年龄更加不可知论。如果这是不可能的,并且所有的写入操作都必须被捕获和处理,那么你可能会被迫采用更确定的方法,比如将所有的写入操作放入队列中,让读取器一个接一个地将它们逐出队列。我相信,ConcurrentQueue在这种情况下,这个班会有所帮助。

如果你可以的“新鲜”的含义,以减少仅“更新”,然后发出呼叫Thread.MemoryBarrier每次读取后,使用Volatile.Read,使用volatile关键字等,将绝对保证序列中的一个读操作将返回一个新的值比前一读。

用户回答回答于

内存屏障确实提供了这种保证。我们可以从障碍所保证的记录属性中推导出正在寻找的“新鲜”属性。

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

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

x = 1
x = 2
print(x)

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

总之,波动性保证能够看到最近的价值。

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

扫码关注云+社区