作者:会说话de狗尾草 | 来源:互联网 | 2022-12-03 13:34
https://msdn.microsoft.com/en-us/magazine/jj883956.aspx
考虑轮询循环模式:
private bool _flag = true;
public void Run()
{
// Set _flag to false on another thread
new Thread(() => { _flag = false; }).Start();
// Poll the _flag field until it is set to false
while (_flag) ;
// The loop might never terminate!
}
在这种情况下,.NET 4.5 JIT编译器可能会像这样重写循环:
if (_flag) { while (true); }
在单线程的情况下,这种转换是完全合法的,一般来说,提升循环读取是一个很好的优化.但是,如果_flag在另一个线程上设置为false,则优化可能会导致挂起.
请注意,如果_flag字段是volatile,则JIT编译器不会提升循环读取.(有关此模式的更详细说明,请参阅12月文章中的"轮询循环"部分.)
如果我锁定_flag
或仅使其volatile
停止优化,JIT编译器是否仍会如上所示优化代码?
Eric Lippert对于volatile有如下说法:
坦率地说,我不鼓励你做一个不稳定的领域.易失性字段表明你正在做一些彻头彻尾的疯狂:你试图在两个不同的线程上读取和写入相同的值,而不是锁定到位.锁定保证锁内部读取或修改的内存一致,锁定保证一次只有一个线程访问给定的内存块,依此类推.锁定速度太慢的情况非常少,并且由于您不了解确切的内存模型而导致代码错误的可能性非常大.除了Interlocked操作最琐碎的用法之外,我不会尝试编写任何低锁代码.我将"挥发性"的用法留给了真正的专家.
总结一下:谁确保上面提到的优化不会破坏我的代码?只有volatile
?另外,lock
声明?或者是其他东西?
由于Eric Lippert不鼓励你使用volatile
,必须有别的东西?
Downvoters:我很感激每个问题的反馈.特别是如果你贬低它,我想听听你为什么认为这是一个糟糕的问题.
bool变量不是线程同步原语:这个问题是一个基本问题.编译器什么时候不进行优化?
Dupilcate:这个问题明确是关于优化的.您链接的那个没有提到优化.
1> Eric Lippert..:
让我们回答一下被问到的问题:
如果我锁定_flag或者只是使它挥发性停止优化,JIT编译器是否仍会如上所示优化代码?
好吧,我们不回答被问到的问题,因为这个问题太复杂了.让我们把它分解成一系列不太复杂的问题.
如果我锁定_flag,JIT编译器是否仍会如上所示优化代码?
简短回答:lock
提供更强有力的保证volatile
,因此,如果读取周围存在锁定,则不允许抖动将读出输出_flag
.当然锁也必须在写.锁只有在你到处使用时才有效.
private bool _flag = true;
private object _flagLock = new object();
public void Run()
{
new Thread(() => { lock(_flaglock) _flag = false; }).Start();
while (true)
lock (_flaglock)
if (!_flag)
break;
}
(当然,我注意到这是等待一个线程向另一个线程发出信号的一种非常糟糕的方式.不要坐在一个紧密的循环中轮询一个标志!像一个明智的人一样使用等待句柄.)
你说锁比挥发物强; 那是什么意思?
读取挥发物会阻止某些操作及时移动.写入易挥发物会阻止某些操作及时移动.锁可防止更多操作及时移动.这些预防语义被称为"记忆围栏" - 基本上,挥发物引入半围栏,锁引入完整围栏.
有关详细信息,请阅读有关特殊副作用的C#规范部分.
一如既往,我会提醒您,挥发物不会给您全球新鲜度的保证.在多线程C#编程中没有这样的东西作为变量的"最新"值,因此易失性读取不会给你"最新"值,因为它不存在.存在"最新"值的想法意味着始终观察到读取和写入在时间上具有全局一致的排序,这是错误的.线程仍然可以在易失性读写顺序上不一致.
锁可以防止这种优化; 挥发物阻止了这种优化.这些是阻止优化的唯一因素吗?
不可以.您也可以使用Interlocked
操作,或者可以明确地引入内存屏障.
我volatile
是否能够正确理解这一点?
不.
我该怎么办?
不要首先编写多线程程序.一个程序中的多个控制线程是个坏主意.
如果必须,请不要跨线程共享内存.将线程用作低成本进程,并且只有在具有可执行CPU密集型任务的空闲CPU时才使用它们.对所有I/O操作使用单线程异步.
如果必须跨线程共享内存,请使用可用的最高级编程结构,而不是最低级别.使用a CancellationToken
表示在异步工作流中的其他位置取消的操作.