锁到底如何工作?


526

我看到对于使用不是线程安全的对象,我们使用如下锁将代码包装起来:

private static readonly Object obj = new Object();

lock (obj)
{
    // thread unsafe code
}

因此,当多个线程访问同一代码时会发生什么(假设它在ASP.NET Web应用程序中运行)。他们排队了吗?如果是这样,他们将等待多长时间?

使用锁会对性能产生什么影响?


Answers:


447

lock语句由C#3.0转换为以下内容:

var temp = obj;

Monitor.Enter(temp);

try
{
    // body
}
finally
{
    Monitor.Exit(temp);
}

在C#4.0中,此更改已更改,现在生成如下:

bool lockWasTaken = false;
var temp = obj;
try
{
    Monitor.Enter(temp, ref lockWasTaken);
    // body
}
finally
{
    if (lockWasTaken)
    {
        Monitor.Exit(temp); 
    }
}

你可以找到什么更多信息Monitor.Enter确实在这里。引用MSDN:

使用Enter以获取作为参数传递的对象上的监视器。如果另一个线程Enter 在对象上执行,但尚未执行Exit,则当前线程将阻塞,直到另一个线程释放对象。同一线程在Enter不阻塞的情况下多次调用是合法的 。但是,Exit必须等待相等数量的 调用,然后等待该对象的其他线程才能解除阻塞。

Monitor.Enter方法将无限期等待;它不会超时。


15
根据MSDN,“与直接使用Monitor类相比,相比于直接使用Monitor类,通常更喜欢使用lock(C#)或SyncLock(Visual Basic)关键字,这是因为lock或SyncLock更简洁,并且因为lock或SyncLock可以确保释放基础监视器,甚至如果受保护的代码引发异常,则可以通过finally关键字完成此操作,无论是否引发异常,该关键字都会执行其关联的代码块。” msdn.microsoft.com/zh-CN/library/ms173179.aspx
Aiden Strydom

10
var temp = obj的意义是什么?线。既然这只是一个开始,那么再做一个有什么用呢?
priehl

11
@priehl它允许用户进行更改obj而不会导致整个系统陷入僵局。
史蒂文

7
@Joymon最终,每种语言功能都是语法糖。语言功能与锁定功能一样,是关于提高开发人员的生产力和使应用程序更具可维护性。
史蒂文·史蒂芬(Steven)

2
正确。这是lock-statement和Monitor 的全部目的:这样,您可以在一个线程中执行操作,而不必担心另一个线程会将其破坏。
Dizzy H. Muffin

285

它比您想象的要简单。

根据Microsoft的说法:lock关键字确保一个线程不会输入代码的关键部分,而另一个线程在关键部分。如果另一个线程试图输入锁定的代码,它将等待阻塞,直到对象被释放为止。

lock关键字要求Enter在该块的开始和Exit在块的结尾。lock关键字实际上Monitor在后端处理类。

例如:

private static readonly Object obj = new Object();

lock (obj)
{
    // critical section
}

在上面的代码中,首先线程进入关键部分,然后将其锁定obj。当另一个线程尝试进入时,它还将尝试锁定obj已被第一个线程锁定的。第二个线程将必须等待第一个线程释放obj。当第一个线程离开时,然后另一个线程将锁定obj并进入关键部分。


9
我们应该创建一个虚拟对象来锁定还是可以在上下文中锁定现有变量?
batmaci

9
@batmaci-锁定在单独的私有虚拟对象上可以确保没有其他人锁定该对象。如果您锁定数据,并且外部仍然可以看到同一数据,那么您将失去保证。
Umar Abbas

8
如果多个进程正在等待释放锁,将会发生什么情况?等待进程是否已排队,以便它们将按FIFO顺序锁定关键部分?
jstuardo

@jstuardo-它们已排队,但不能保证顺序为FIFO。看看这个链接:albahari.com/threading/part2.aspx
Umar Abbas


47

不,他们没有排队,他们正在睡觉

形式的锁语句

lock (x) ... 

其中x是引用类型的表达式,精确等于

var temp = x;
System.Threading.Monitor.Enter(temp); 
try { ... } 
finally { System.Threading.Monitor.Exit(temp); }

您只需要知道它们正在互相等待,只有一个线程进入锁定块,其他线程将等待...

Monitor是完全用.net编写的,因此它足够快,还可以查看 带有反射器的Monitor类以了解更多详细信息


6
注意,对于发出的代码lock声明小幅C#4变:blogs.msdn.com/b/ericlippert/archive/2009/03/06/...
LukeH

@ArsenMkrt,它们不处于“已阻塞”状态“队列?”。我认为睡眠和阻塞状态之间有些区别,不是吗?
Mohanavel 2014年

@Mohanavel有什么区别?
2014年

1
那不是问题。问题是关于“锁定”关键字的。假设进程进入“锁定”部分。这意味着进程会阻塞该代码段,并且在释放该锁之前,其他进程可能无法进入该部分。好吧....现在,还有2个进程尝试输入同一块。根据此论坛上的说法,由于它受到“锁定”关键字的保护,因此他们将等待。当第一个进程释放锁时。哪个过程进入该块?尝试输入的第一个还是最后一个?
jstuardo

1
我猜你的意思是线程,而不是过程......如果是这样,比答案是没有,也不能保证哪一个会进入更多...这里stackoverflow.com/questions/4228864/...
阿尔森Mkrtchyan

29

锁将阻止其他线程执行锁块中包含的代码。线程将必须等待,直到锁块内的线程完成并且释放了锁。这确实会对多线程环境中的性能产生负面影响。如果确实需要执行此操作,则应确保lock块中的代码可以非常快速地处理。您应尽量避免进行昂贵的活动,例如访问数据库等。


11

对性能的影响取决于锁定方式。您可以在此处找到良好的优化列表:http : //www.thinkingparallel.com/2007/07/31/10-ways-to-reduce-lock-contention-in-threaded-programs/

基本上,您应该尝试尽可能少地锁定,因为这会使您的等待代码进入休眠状态。如果您在锁定中进行了繁重的计算或持久的代码(例如文件上传),则会导致巨大的性能损失。


1
但是,即使您是该领域的专家,尝试编写低锁定的代码也常常会导致难以发现和修复的细微错误。通常,使用锁有两个弊端。您应该完全根据需要锁定,不多也不少!
路加福音

1
@LukeH:在某些使用模式下,低锁代码非常简单易行[ do { oldValue = thing; newValue = updated(oldValue); } while (CompareExchange(ref thing, newValue, oldValue) != oldValue]。最大的危险是,如果需求超出了此类技术可以处理的范围,则可能很难修改代码来处理该技术。
2013年

链接已损坏。
CarenRose19年

8

lock语句中的部分只能由一个线程执行,因此所有其他线程将无限期地等待持有该锁的线程完成操作。这可能导致所谓的死锁。



By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.