监视器与锁定的区别

110

何时在C#中使用Monitor类或lock关键字保证线程安全?

编辑: 从目前为止的答案来看,lock是一系列调用Monitor类的简写。lock调用到底是什么简写?或者更明确地说,

class LockVsMonitor
{
    private readonly object LockObject = new object();
    public void DoThreadSafeSomethingWithLock(Action action)
    {
        lock (LockObject)
        {
            action.Invoke();
        }
    }
    public void DoThreadSafeSomethingWithMonitor(Action action)
    {
        // What goes here ?
    }
}

更新

非常感谢大家的帮助:我已经发了另一个问题,以跟进你们提供的一些信息。由于你们似乎在这个领域很熟练,所以我将链接放在这里:这个锁定和管理锁定异常的解决方案有什么问题?

9个回答

98

Eric Lippert在他的博客中谈到了这个问题: 锁和异常不可搭配

C# 4.0版本及之前版本的等效代码有所不同。


C# 4.0中的代码为:

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

它依赖于Monitor.Enter在获取锁时原子性地设置标志位。


之前的内容:

var temp = obj;
Monitor.Enter(temp);
try
{
   body
}
finally
{
    Monitor.Exit(temp);
}

这取决于在 Monitor.Entertry 之间没有抛出任何异常。我认为在调试代码中,由于编译器在它们之间插入了 NOP,因此可能导致线程在这两者之间被中止。


5
在我看来,恢复共享状态与锁定/多线程无关。因此,应该在lock块内使用try-catch/finally来完成。 - CodesInChaos
@kizzx2: 我希望.NET能够更容易地实现我认为正确的锁定模式,这样一来,意外异常既不会释放锁,也不会保持锁定状态,而是失效它,使得任何等待或未来尝试进入锁定资源的代码都会立即抛出异常。这样,关键需要锁定资源的代码会快速失败,而不是无限期地等待,而那些可以受益于资源但并不真正需要它的代码则可以继续工作。 - supercat
2
@kizzx2:这种模式在使用读写锁时特别好。如果在持有读取锁的代码中发生异常,就没有理由期望受保护的资源可能会被损坏,因此也没有理由使其失效。如果在编写锁定中发生异常并且异常处理代码没有明确指示受保护对象的状态已得到修复,则表明该对象可能已经受损并应该无效。 依我之见,意外异常不应该使程序崩溃,但应该使任何可能存在损坏的内容无效化。 - supercat
好的。监视器进入/退出我似乎明白了。但是还有一个脉冲函数,它似乎会唤醒某些线程。有人能解释一下这是否必要吗? - Arsen Zahray
2
@ArsenZahray,对于简单的锁定,您不需要使用Pulse。它在一些高级多线程场景中非常重要。我从未直接使用过Pulse - CodesInChaos
显示剩余2条评论

53

lock 只是使用 try + finallyMonitor.ExitMonitor.Enter 快捷方式。只要足够使用 lock 语句 - 如果需要类似于 TryEnter 的东西,则必须使用 Monitor。


26

一个lock语句等效于:

Monitor.Enter(object);
try
{
   // Your code here...
}
finally
{
   Monitor.Exit(object);
}

请注意,Monitor还可以使用Wait()Pulse(),这在复杂的多线程情况下通常很有用。

更新

但是在C# 4中,它的实现方式不同:

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

感谢CodeInChaos的评论和链接


在C#4中,锁语句的实现方式有所不同。http://blogs.msdn.com/b/ericlippert/archive/2009/03/06/locks-and-exceptions-do-not-mix.aspx - CodesInChaos

22

Monitor更加灵活。我最喜欢使用它的情况是:

当你不想等待轮到自己,直接跳过时:

//already executing? forget it, lets move on
if (Monitor.TryEnter(_lockObject))
{
    try
    {
        //do stuff;
    }
    finally
    {
        Monitor.Exit(_lockObject);
    }
}

7

正如其他人所说,lock相当于

Monitor.Enter(object);
try
{
   // Your code here...
}
finally
{
   Monitor.Exit(object);
}

仅出于好奇,lock 会保留您传递给它的第一个引用,并且如果您更改它,它不会抛出异常。 我知道不建议更改已锁定的对象,并且您也不想这样做。

但同样地,从技术角度来看,这种方法是有效的:

var lockObject = "";
var tasks = new List<Task>();
for (var i = 0; i < 10; i++)
    tasks.Add(Task.Run(() =>
    {
        Thread.Sleep(250);
        lock (lockObject)
        {
            lockObject += "x";
        }
    }));
Task.WaitAll(tasks.ToArray());

这个可以,而这个不行:

var lockObject = "";
var tasks = new List<Task>();
for (var i = 0; i < 10; i++)
    tasks.Add(Task.Run(() =>
    {
        Thread.Sleep(250);
        Monitor.Enter(lockObject);
        try
        {
            lockObject += "x";
        }
        finally
        {
            Monitor.Exit(lockObject);
        }
    }));
Task.WaitAll(tasks.ToArray());

错误:

在70783sTUDIES.exe中发生了'System.Threading.SynchronizationLockException'类型的异常,但未在用户代码中处理

额外信息:从未同步的代码块中调用了对象同步方法。

这是因为Monitor.Exit(lockObject);会对已更改的lockObject进行操作,因为strings是不可变的,所以您正在从未同步的代码块中调用它。但无论如何,这只是一个有趣的事实。


这是因为Monitor.Exit(lockObject);将作用于lockObject。那么锁对对象不起作用吗?锁是如何工作的? - Yugo Amaryl
@YugoAmaryl,我想这是因为锁定语句会记住第一个传递的引用,然后使用它而不是使用更改后的引用,例如:object temp = lockObject; Monitor.Enter(temp); <...locked code...> Monitor.Exit(temp); - Zhuravlev A.

3

3
实际上,lock关键字是使用Monitor类实现的。例如,参见http://msdn.microsoft.com/en-us/library/ms173179(v=vs.80).aspx。 - RobertoBr
1
底层的锁实现使用了 Monitor,但它们并不是同一回事。请考虑 Monitor 提供的方法,在锁中不存在的方法,以及您可以在代码块中分别锁定和解锁的方式。 - eran otzap

3

锁和监视器的基本行为(进入+退出)大多相同,但监视器有更多选项,可以让您拥有更多的同步可能性。

锁是一种快捷方式,它是基本用法的选项。

如果您需要更多控制,则监视器是更好的选择。您可以使用等待、尝试进入和脉冲等高级用法(如屏障、信号量等)。


2

锁定 锁定关键字确保一次只有一个线程在执行代码。

lock(lockObject)

        {
        //   Body
        }

锁定关键字通过获取给定对象的互斥锁、执行语句,然后释放锁来将语句块标记为临界区。

如果另一个线程尝试进入锁定代码,则会等待,阻塞,直到该对象被释放。

监视器 监视器是一个静态类,属于System.Threading命名空间。

它提供了对对象的独占锁,以便在任何给定时间点只有一个线程可以进入临界区。

C#中Monitor和lock之间的区别

锁是使用try和finally的Monitor.Enter的快捷方式。 Lock在内部处理try和finally块 Lock = Monitor + try finally.

如果您想使用TryEnter()、Wait()、Pulse()和PulseAll()方法实现高级多线程解决方案,则Monitor类是您的选择。

C# Monitor.wait():一个线程等待其他线程通知。

Monitor.pulse(): 一个线程通知另一个线程。

Monitor.pulseAll(): 一个线程通知进程中的所有其他线程。


0
除了以上所有的解释,锁定是C#语句,而Monitor是位于System.Threading命名空间中的.NET类。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接