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

时间:2014-07-10 13:25:45

标签: c# .net multithreading volatile memory-barriers

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

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

msdn doc for volatile提及:

  

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

msdn doc for volatile fields提及:

  

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

.NET code for VolatileRead是:

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&#39;内存障碍和锁定部分)< / p>

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

EDIT3:

感谢非常有趣的讨论,我学到了很多东西,实际上我能够提炼出关于这个主题的简单明确的问题。它与原版的完全不同,所以我宁愿在这里发布一个新的: Memory barrier vs Interlocked impact on memory caches coherency timing

2 个答案:

答案 0 :(得分:7)

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

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

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

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

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

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

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

如果您可以将“新鲜”的含义简化为“更新”,则在每次阅读后使用Thread.MemoryBarrier,使用Volatile.Read关键字等拨打volatile。绝对保证序列中的一个读取将返回较新的值,而不是之前的读取。


1 ABA problem开辟了新的蠕虫病毒。

答案 1 :(得分:2)

内存屏障确实提供了这种保证。我们可以得出&#34;新鲜度&#34;您正在寻找屏障保证的重新标记属性的属性。

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

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

x = 1
x = 2
print(x)

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

简而言之,volatile可以保证您看到最近的价值。

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