当前位置: 首页 > 知识库问答 >
问题:

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

邹海超
2023-03-14

我已经阅读了许多相互矛盾的信息(msdn,SO等),关于易失性和VoletleRead(ReadAcquireFence)。

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

msdn doc用于挥发性提及:

(…)这样可以确保字段中始终存在最新的值。

挥发性字段的msdn文档提到:

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

。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文档,内存屏障可防止重新排序。然而,这似乎对新鲜度没有任何影响-对吗?

那么怎样才能保证新鲜度呢?标记字段volatile和使用volatieread和VolatileWrite语义访问字段volatile之间有什么区别吗?我目前正在html" target="_blank">执行性能关键型代码的后面部分,这需要保证代码的新鲜度,但是读者有时会得到过时的价值。我想知道标记状态为不稳定是否会使情况有所不同。

编辑1:

我试图实现的是——保证读线程将获得尽可能最新的共享变量值(由多个编写器编写)——理想情况下不超过上下文切换或其他可能推迟状态立即写入的操作的成本。

如果volatile或更高级别的构造(例如lock)具有此保证(是否?)他们是如何做到这一点的?

编辑2:

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

根据我在这里学到的知识,我想知道这是否是解决方案(解决(?)行标记有注释):

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

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

编辑3:

多亏了这里非常有趣的讨论,我学到了很多东西,实际上我能够提炼出我对这个主题的简化明确问题。它与最初的非常不同,所以我宁愿在这里发布一个新的:内存屏障与互锁对内存缓存一致性时间的影响

共有2个答案

经正祥
2023-03-14

内存屏障确实提供了这种保证。我们可以从屏障保证的重新排序属性中派生出您正在寻找的“新鲜度”属性。

新鲜度可能是指读取返回最近写入的值。

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

x = 1
x = 2
print(x)

我们怎么可能打印2以外的值呢?如果没有Volatile,读取可以向上移动一个槽并返回1。但是,Volatile阻止了重新排序。写入不能在时间上向后移动。

简而言之,波动性保证您可以看到最新的值。

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

益富
2023-03-14

我认为这是一个很好的问题。但是,这也很难回答。我不确定我能给你一个明确的答案。这真的不是你的错。只是这个主题很复杂,确实需要知道一些可能无法列举的细节。老实说,你似乎已经在这个问题上很好地自学了。我自己花了很多时间研究这个问题,我仍然没有完全理解所有的事情。尽管如此,我还是会在这里尝试一些表面上的答案。

那么线程读取新值意味着什么呢?这是否意味着读取返回的值保证不超过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;
}

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

我见过很多关于这个原理的无锁代码。也就是说,操作通常被包装成循环,以便不断重试操作,直到成功。它并不假定第一次尝试使用的是最新的值。它也不认为该值的每次使用都是最新的。它仅假设每次读取后该值都较新。

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

如果您可以将“新鲜”的含义简化为仅“较新”,则调用线程。MemoryBarrier每次读取后,使用Volatile。读取,使用Volatile关键字等将绝对保证序列中的一次读取将返回比前一次读取更新的值。

1ABA问题打开了一罐新的蠕虫。

 类似资料:
  • 在阅读了这个问题和这个(尤其是第二个答案)之后,我对volatile及其关于记忆障碍的语义感到非常困惑。 在上面的例子中,我们写入一个易失性变量,这会导致一个mitch,这反过来会将所有挂起的存储缓冲区/加载缓冲区刷新到主缓存,使其他缓存行无效。 然而,非易失性字段可以优化并存储在寄存器中,例如?那么,我们如何才能确保给定一个写入易失性变量之前的所有状态变化都是可见的呢?如果我们有1000件东西呢

  • 所有的中断函数都能正常工作,但是过程函数却让我很生气。 我会感激任何我没注意的把戏。

  • 官方记录说 写入易失性字段与监视器释放具有相同的记忆效果,从易失性字段读取与监视器获取具有相同的记忆效果。 和 有效地,挥发性的语义学得到了实质性的加强,几乎达到了同步的水平。为了可见性的目的,挥发性字段的每次读取或写入都像“半”次同步。 从这里开始。 这是否意味着,对volatile变量的任何写入都会使执行线程将其缓存刷新到主存中,而每次从volatile字段读取都会使线程从主存重新读取其变量?

  • 问题内容: 我尝试了解为什么此示例是正确同步的程序: 由于存在冲突的访问(存在对a的写入和读取),因此在每个顺序一致性中,必须在访问之间的关系之前执行。假设顺序执行之一: 是1发生-在2之前发生,为什么? 问题答案: 不,在相同变量的易失性写入之前(以同步顺序),在易失性写入 之前 不一定 会发生 易失性读取。 这意味着它们可能处于“数据争用”中,因为它们“冲突的访问未按先发生后关系进行排序”。如

  • 我对易变语义几乎没有疑问 假设有三个线程T1、T2和T3,以及给定类的一个实例。 假设发生以下读/写操作序列: 我知道可以保证点9的T1将看到点7的值,点10的T1将看到点6的值(确切地说,至少和这个值一样最新)。 但是,这些说法是真的吗? Java内存模型保证,点11处的T1将看到至少与点5处的T3相同的最新值(来自T3或更实际的本地内存的值,但即使共享内存中有更实际的值,T1也可能看不到) 请

  • a)为什么除了“00”之外还有其他输出? b)如何修改代码以便始终打印“00”。 对于a)我的回答是:在没有任何volatile/同步构造的情况下,编译器可以重新排序一些指令。特别是“this.initialint=val;”和“this.flag=true;”可以切换,这样就可以发生这种情况:线程都启动了,t1充电在前面。给定重新排序的指令,它首先设置flag=true。在它到达“this.in