ECMA-335规范声明如下:
* 获取锁(System.Threading.Monitor.Enter或输入同步方法)应隐式执行易失性读操作,并释放锁(System.Threading.Monitor.Exit或保留同步方法)应隐式执行易失性写操作。 (...)
易失性读取具有获取语义,这意味着保证在CIL指令序列中的读取指令之后发生的对存储器的任何引用之前发生读取。易失性写入具有释放语义,这意味着写入保证在CIL指令序列中的写入指令之前的任何存储器引用之后发生。 *
这意味着编译器无法将语句移出Monitor.Enter / Monitor.Exit块,但不禁止其他语句移入块中。也许,甚至可以将另一个Monitor.Enter移动到块中(因为可以交换易失性写入,然后是易失性读取)。 那么,可以使用以下代码:
class SomeClass
{
object _locker1 = new object();
object _locker2 = new object();
public void A()
{
Monitor.Enter(_locker1);
//Do something
Monitor.Exit(_locker1);
Monitor.Enter(_locker2);
//Do something
Monitor.Exit(_locker2);
}
public void B()
{
Monitor.Enter(_locker2);
//Do something
Monitor.Exit(_locker2);
Monitor.Enter(_locker1);
//Do something
Monitor.Exit(_locker1);
}
}
,变成以下的等同物:
class SomeClass
{
object _locker1 = new object();
object _locker2 = new object();
public void A()
{
Monitor.Enter(_locker1);
//Do something
Monitor.Enter(_locker2);
Monitor.Exit(_locker1);
//Do something
Monitor.Exit(_locker2);
}
public void B()
{
Monitor.Enter(_locker2);
//Do something
Monitor.Enter(_locker1);
Monitor.Exit(_locker2);
//Do something
Monitor.Exit(_locker1);
}
}
,可能导致死锁?或者我错过了什么?
答案 0 :(得分:2)
ECMA-335规范弱于what the CLR (and every other implementation) uses。
我记得读过(heresay)关于微软首次尝试使用较弱的内存模型移植到IA-64。他们有很多自己的代码,取决于双重检查的锁定习惯用法(在较弱的内存模型下is broken),他们只是在该平台上实现了更强大的模型。
Joe Duffy a great post总结了(实际的)CLR记忆模型,仅供我们凡人使用。还有一个MSDN文章的链接,该文章更详细地解释了CLR与ECMA-335的不同之处。
我认为这不是实践中的问题;只是假设CLR内存模型,因为其他人都这样做。此时没有人会创建一个弱实现,因为大多数代码都会破坏。
答案 1 :(得分:1)
当您使用lock
或Monitor.Enter
和Monitor.Exit
这是完整栅栏时,这意味着它会在内存中创建“屏障”{{1} }在beggening或锁定“Thread.MemoryBarrier()
”之前以及锁定“Monitor.Enter
”之前。所以锁定之前和之后都不会有任何操作,但请注意,锁本身内的操作可以从其他线程的角度进行交换,但这从来不是一个问题,因为锁将保证互斥,所以只有一个线程会同时执行锁中的代码。无论如何,重新排序不会在单个线程中发生,也就是说,当多线程进入相同的代码区域时,它们可能看到不按相同顺序的指令。
我强烈建议您在this文章中详细了解Monitor.Exit
以及全围和半围栏。
编辑请注意,我在这里描述了MemoryBarrier
是完全围栏的事实,但没有谈到你所知道的“死锁”,你所描述的场景将永远不会发生,因为像@Hans一样提到方法调用永远不会发生重新排序,即:
lock
将始终按顺序执行,但其中的指令可能会重新排序,例如当多线程执行Method1();
Method2();
Method3();
内的代码时。