【问题标题】:Does a lock statement around multiple statements ensure that all the changes are visible to other threads (provided they enter the same mutex)?围绕多个语句的锁定语句是否确保所有更改对其他线程可见(假设它们输入相同的互斥锁)?
【发布时间】:2012-07-21 07:22:55
【问题描述】:

如果您在一个锁代码块内有多个共享变量赋值,这是否一定意味着所有这些更改对其他线程立即可见,一旦进入锁语句就可能在其他处理器上运行在同一个对象上 - 或者没有这样的保证?

那里的许多示例显示了一个单个“设置”或“获取”一个公共变量,并详细介绍了内存屏障,但是如果里面有一组更复杂的语句会发生什么?甚至可能执行其他操作的函数调用?

类似这样的:

lock(sharedObject)
{
  x = 10;
  y = 20;
  z = a + 10;
}

如果此代码在另一个线程上运行,而该线程可能在另一个处理器上执行,它是否对更改的“可见性”做出任何保证?

lock (sharedObject)
{
  if (y == 10)
  {
     // Do something. 
  }
}

如果答案是 - 也许并说明何时这些变化可能会变得可见?

【问题讨论】:

  • 是的,锁意味着内存屏障。纯粹是因为没有一个就无法实现可靠的锁。

标签: c# synchronization thread-safety locking


【解决方案1】:

锁块在开始和结束(块的开始和结束)处包括内存栅栏。这确保了对内存的任何更改对其他内核(例如,在其他内核上运行的其他线程)都是可见的。在您的示例中,任何其他线程都可以看到第一个锁定块中对 x、y、z 的更改。 “可见”意味着缓存到寄存器中的任何值都将被刷新到内存中,并且任何缓存在 CPU 缓存中的内存都将被刷新到物理内存中。 ECMA 334 详细说明锁块是由 Monitor.Enter 和 Monitor.Exit 包围的块。此外,ECMA 335 详细说明 Monitor.Enter “应隐式执行易失性读取操作......”和 Monitor.Exit “隐式执行易失性写入操作。这确实意味着修改对其他内核/线程不可见,直到锁定块的末尾(在 Monitor.Exit 之后),但是如果您对这些变量的所有访问都受到锁定的保护,那么无论如何都不能跨不同的内核/线程同时访问所述变量。

这实际上意味着任何由 lock 语句保护的变量都不需要声明为 volatile 以使其修改对其他线程可见。

由于示例代码仅包含一个依赖于单个共享原子操作的操作(读取和写入单个值到 y),因此您可以通过以下方式获得相同的结果:

try
{
  x = 10;
  y = 20;
  Thread.VolatileWrite(ref z, a + 10);
}

if(y == 10)
{
// ...
}

第一个块保证对 x 的写入在对 y 的写入之前是可见的,对 y 的写入在对 z 的写入之前是可见的。它还保证,如果对 x 或 y 的写入缓存在 CPU 缓存中,则该缓存将在调用 VolatileWrite 后立即刷新到物理内存(因此对任何其他线程可见)。

如果在if(y == 10) 块内您使用xy 执行某些操作,您应该返回使用lock 关键字。

此外,以下内容相同:

try
{
  x = 10;
  y = 20;
  Thread.MemoryBarrier();
  z = a + 10;
}

【讨论】:

  • @neil。阅读ecma 355,术语“可见性”还指其他线程/内核是否可以看到对内存的更改。我的回答详细说明了发生更改时其他线程/内核如何看不到内存更改(寄存器缓存和 CPU 内存缓存)另请参阅msdn.microsoft.com/en-us/library/windows/hardware/…(它使用“查看”而不是“可见性”,但你明白了)。另请阅读 ECMA 334 第 17.4.3 节易失性字段,其中详细说明了“在存储完成后,将允许存储对主线程可见”等。
  • 这不是看到或没有看到变化的问题。只要更改在同一个实例上,其他进程将始终看到更改。这是何时可以看到更改的问题,而不是是否可以看到更改。锁定块本身并不能保证其他线程会看到对 x、y 和 z 的原子更改,也不能保证 x、y 或 z 按照操作发生的顺序进行修改 - 唯一可以保证的该操作被视为原子操作是其他代码查看它尊重互斥锁并锁定同一锁实例。
  • @Neil 不,如果没有易失性写入,他们总是不会。这才是重点。如果变量不是易失性的,则写入可以缓存到寄存器,或者写入内存可以在 CPU 缓存中。即写入的结果不能被其他核心/线程“看到”。此外,锁确实保证指令不会被重新排序,否则可以从其他线程“看到”(避免“可见”)的更改将以与它们相同的顺序被看到是用代码编写的。此保证继承自 try/catch 保证,因为锁定块中的代码被包装在 try 块中
  • 这就是为什么我说这是时间问题。寄存器或 cpu 缓存中的值最终将被写回共享内存。锁块确实做了你需要做的事情来实现这一点。执行方式的实现是无关紧要的——锁块将被其他线程视为原子的,但前提是它们也尊重相同的互斥锁。基本上这就是我要说的。锁定块本身并不能特别保护 x、y 和 z 不受任何影响,只有与互斥锁相关的整个应用程序代码才会这样做。
  • @Neil 你根本没有提到时间。是的,锁确实做了它需要做的事情以使内存更改可见,但这不是由于同步;它来自隐式内存屏障 try/catch 保证。您可以创建没有锁(或互斥锁等)的同步代码,但是您不会获得这些可见性保证。
【解决方案2】:

如果我误解了你的问题,请原谅我(很有可能);但我认为您正在混淆同步可见性的概念。

互斥锁(“互斥”)的全部意义在于确保两个代码块不会同时运行。所以在你的例子中,第一个块:

lock(sharedObject)
{
  x = 10;
  y = 20;
  z = a + 10;
}

...和第二块:

lock (sharedObject)
{
  if (y == 10)
  {
     // Do something. 
  }
}

...永远不会同时执行。这就是 lock 关键字为您提供的保证。

因此,任何您的代码进入第二个块时,变量 xyz 应该处于与完全执行第一的。 (这是假设您在任何地方访问这些变量时,您在 sharedObject 上的 lock 与在这些 sn-ps 中的方式相同。)

这意味着第一个块中的中间更改的“可见性”与第二个块的角度无关,因为永远不会发生例如值x 的更改,但是不要yz

【讨论】:

  • “可见性”是相关的。防止两个线程同时运行特定代码位与获取和释放语义不同。仅仅因为两段代码不能同时在不同的线程(并且可能是不同的内核)上运行,与对变量的更改是否对其他线程“可见”是完全不同的。 “可见性”必须处理潜在的缓存值。有关更多详细信息,请参阅我的答案。 “锁定”还提供可见性保证。
  • @PeterRitchie:我想我明白你的意思。我发布此答案是因为我将 OP 的问题解释为:“是否有可能另一个线程将看到 x 等于 10y 等于 20?” ——即,如果有可能来自同步代码块中的中间更改以某种方式在其他线程中变得“可见”。我想指出,只要所有有问题的代码都是同步的,就不可能发生这种情况。可能是 OP 实际上是在问较低级别的问题,在这种情况下,我会说您的答案更有用。
  • 对我来说,使用“内存屏障”意味着 OP 询问的是“可见性”而不仅仅是同步。是的,这两段代码不可能同时运行。它是指在不同内核上运行的另一个代码块是否可以在运行时看到另一个内核上的其他代码块中发生的情况,总是
  • 是的,问题是关于可见性的。同步理解——多线程不能进入lock语句所包围的代码块。然而,问题的要点是——如果线程 A 退出块并且另一个线程 B 进入同一对象的锁,是否保证立即看到新值?如果是这样 - 这意味着所有可能在寄存器中缓存变量的 CPU 或内核或其缓存都需要被丢弃并强制从内存中重新加载新副本。
  • @Dodgyrabbit:那我确实误解了你的问题,我很抱歉告诉你你已经知道的事情。对于它的价值——我完全承认这纯粹是轶事——我对 .NET 的体验通常是,在内存模型方面,该框架为开发人员提供了一个相当强大的保护,确保了人们直观/天真的期望的行为。例如,众所周知的 Java 中双重检查锁定的缺点在 .NET 中不存在(我上次还是检查了)。我认为在这种情况下也是如此(幼稚的期望被证明是准确的)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-03-19
  • 1970-01-01
  • 2015-07-08
  • 2018-10-19
  • 2018-06-10
  • 2021-02-19
  • 1970-01-01
相关资源
最近更新 更多