锁定语句之后的读取指令是否可以在锁定之前移动?

时间:2016-12-06 08:57:09

标签: c# .net multithreading locking clr

此问题是对this主题中的评论的跟进。

我们假设我们有以下代码:

// (1)
lock (padlock)
{
    // (2)
}
var value = nonVolatileField; // (3)

此外,我们假设(2)中的任何指令都不会对nonVolatileField产生任何影响,反之亦然。

读取指令(3)能否以锁定声明(1)之前或(2)内部结束的方式重新排序?

据我所知,C#规范(§3.10)和CLI规范(§I.12.6.5)中没有任何内容禁止此类重新排序。

请注意,这与this一个问题不同。在这里,我特别询问阅读说明,因为据我了解,它们不被认为是副作用,并且保证较弱。

2 个答案:

答案 0 :(得分:3)

相信这一点得到了CLI规范的部分保证,尽管它并不像它可能的那样清晰。从I.12.6.5开始:

  

获取锁(System.Threading.Monitor.Enter或输入同步方法)应隐式执行易失性读操作并释放锁   (System.Threading.Monitor.Exit或保留同步方法)将隐式执行易失性写操作。见§I.12.6.7。

然后从I.12.6.7:

  

易失性读取具有“获取语义”,这意味着保证在CIL指令序列中的读取指令之后发生的对存储器的任何引用之前发生读取。易失性写操作具有“释放语义”,这意味着写保证在CIL指令序列中的写指令之前的任何存储器引用之后发生。

因此,进入锁定应防止(3)移动到(1)。我相信,从nonVolatileField读书仍然算作“记忆的参考”。但是,当锁定退出时仍然可以在易失性写入之前执行读取,因此它仍然可以移动到(2)。

C#/ CLI内存模型目前还有很多不足之处。我希望整个事情能够得到明显的澄清(并且可能会收紧,使一些“理论上有效但实际上可怕的”优化无效)。

答案 1 :(得分:2)

就.NET而言,进入监视器(lock语句)具有获取语义,因为它隐式执行易失性读取并退出监视器(lock块的末尾)具有释放语义,因为它隐式执行易失性写入(参见Common Language Infrastructure (CLI) Partition I中的第12.6.5节“锁和线程”)。

volatile bool areWeThereYet = false;

// In thread 1
// Accesses, usually writes: create objects, initialize them
areWeThereYet = true;

// In thread 2
if (areWeThereYet)
{
    // Accesses, usually reads: use created and initialized objects
}

当您向areWeThereYet写入一个值时,执行之前的所有访问都会执行,并且在volatile写入之后不会重新排序。

当您从areWeThereYet读取时,后续访问不会在易失性读取之前重新排序。

在这种情况下,当线程2观察到areWeThereYet已更改时,它保证以下访问(通常是读取)将观察其他线程的访问,通常是写入。假设没有其他代码搞乱受影响的变量。

对于.NET中的其他同步原语,例如SemaphoreSlim,虽然没有明确记录,但如果它们没有类似的语义,那么它将毫无用处。事实上,基于它们的程序在内存模型较弱的平台或硬件架构中甚至无法正常工作。

许多人都认为微软应该在这样的架构上强制执行强大的内存模型,类似于x86 / amd64,以保持当前的代码库(微软自己和客户的代码库)兼容。

我无法验证自己,因为我没有使用Microsoft Windows的ARM设备,更不用说.NET ARM for ARM,但至少有一篇MSDN杂志文章安德鲁·帕多({2}}指出:< / p>

  

允许CLR公开比ECMA CLI规范要求更强的内存模型。例如,在x86上,CLR的内存模型很强大,因为处理器的内存模型很强大。 .NET团队可以使ARM上的内存模型与x86上的模型一样强大,但是尽可能确保完美排序会对代码执行性能产生显着影响。我们已经做了有针对性的工作来加强ARM上的内存模型 - 具体来说,我们在写入托管堆时在关键点插入了内存屏障以保证类型安全 - 但我们确保只在影响最小的情况下执行此操作在表现上。该团队与专家进行了多次设计评审,以确保ARM CLR中应用的技术是正确的。此外,性能基准测试表明,在x86,x64和ARM之间进行比较时,.NET代码执行性能与本机C ++代码相同。