C# 多线程 Mutex Monitor Lock
lock关键字
lock 关键字将语句块标记为临界区,方法是获取给定对象的互斥锁,执行语句,然后释放该锁。此语句的形式如下:
Object thisLock = new Object(); lock (thisLock) { // Critical code section }
lock 确保当一个线程位于代码的临界区时,另一个线程不进入临界区。如果其他线程试图进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释放。
lock 调用块开始位置的 Enter 和块结束位置的 Exit。
通常,应避免锁定 public 类型,否则实例将超出代码的控制范围。常见的结构 lock (this)、lock (typeof (MyType)) 和 lock ("myLock") 违反此准则:
-
如果实例可以被公共访问,将出现 lock (this) 问题。
-
如果 MyType 可以被公共访问,将出现 lock (typeof (MyType)) 问题。
-
由于进程中使用同一字符串的任何其他代码将共享同一个锁,所以出现 lock(“myLock”) 问题。
最佳做法是定义 private 对象来锁定, 或 private static 对象变量来保护所有实例所共有的数据。
下例使用线程和 lock。只要 lock 语句存在,语句块就是临界区并且 balance 永远不会是负数。
// statements_lock2.cs using System; using System.Threading; class Account { private Object thisLock = new Object(); int balance; Random r = new Random(); public Account(int initial) { balance = initial; } int Withdraw(int amount) { // This condition will never be true unless the lock statement // is commented out: if (balance < 0) { throw new Exception("Negative Balance"); } // Comment out the next line to see the effect of leaving out // the lock keyword: lock(thisLock) { if (balance >= amount) { Console.WriteLine("Balance before Withdrawal : " + balance); Console.WriteLine("Amount to Withdraw : -" + amount); balance = balance - amount; Console.WriteLine("Balance after Withdrawal : " + balance); return amount; } else { return 0; // transaction rejected } } } public void DoTransactions() { for (int i = 0; i < 100; i++) { Withdraw(r.Next(1, 100)); } } } class Test { static void Main() { Thread[] threads = new Thread[10]; Account acc = new Account(1000); for (int i = 0; i < 10; i++) { Thread t = new Thread(new ThreadStart(acc.DoTransactions)); threads[i] = t; } for (int i = 0; i < 10; i++) { threads[i].Start(); } } }
Mutex 类
当两个或更多线程需要同时访问一个共享资源时,系统需要使用同步机制来确保一次只有一个线程使用该资源。 Mutex 是同步基元,它只向一个线程授予对共享资源的独占访问权。如果一个线程获取了互斥体,则要获取该互斥体的第二个线程将被挂起,直到第一个线程释放该互斥体。
可以使用 WaitHandle.WaitOne 方法请求互斥体的所属权。拥有互斥体的线程可以在对 WaitOne 的重复调用中请求相同的互斥体而不会阻止其执行。但线程必须调用ReleaseMutex 方法同样多的次数以释放互斥体的所属权。 Mutex 类强制线程标识,因此互斥体只能由获得它的线程释放。相反,Semaphore 类不强制线程标识。
如果线程在拥有互斥体时终止,则称此互斥体被放弃。将此 mutex 的状态设置为收到信号,下一个等待线程将获得所有权。从 .NET Framework 2.0 版开始,在获取被放弃 mutex 的下一个线程中将引发 AbandonedMutexException。在 .NET Framework 2.0 版之前,这样不会引发任何异常。
如果出现被放弃的 mutex,通常表明代码中存在严重错误。如果某个线程在未释放互斥体时便退出,受此互斥体保护的数据结构可能处于不一致的状态。如果此数据结构的完整性能得到验证,下一个请求此互斥体所属权的线程就可以处理此异常并继续。因此推荐把Mutex.ReplaceMutex()放在try-catch-finally结构中的finally中。
对于系统范围的 mutex,被放弃的 mutex 可能指示应用程序已突然终止(例如,通过使用 Windows 任务管理器终止)。
Mutex 有两种类型:未命名的局部 mutex 和已命名的系统 mutex。本地 mutex 仅存在于进程当中。您的进程中任何引用表示 mutex 的 Mutex 对象的线程都可以使用它。每个未命名的 Mutex 对象都表示一个单独的局部 mutex。
已命名的系统互斥体在整个操作系统中都可见,可用于同步进程活动。您可以使用接受名称的构造函数创建表示已命名系统 mutex 的 Mutex 对象。同时也可以创建操作系统对象,或者它在创建 Mutex 对象之前就已存在。您可以创建多个 Mutex 对象来表示同一命名系统 mutex,而且您可以使用 OpenExisting 方法打开现有的命名系统 mutex。
在运行终端服务的服务器上,已命名的系统 mutex 可以具有两级可见性。如果名称以前缀“Global\”开头,则 mutex 在所有终端服务器会话中均为可见。如果名称以前缀“Local\”开头,则 mutex 仅在创建它的终端服务器会话中可见。在这种情况下,服务器上各个其他终端服务器会话中都可以拥有一个名称相同的独立 mutex。如果创建已命名 mutex 时不指定前缀,则它将采用前缀“Local\”。在终端服务器会话中,只是名称前缀不同的两个 mutex 是独立的 mutex,这两个 mutex 对于终端服务器会话中的所有进程均为可见。即:前缀名称“Global\”和“Local\”说明 mutex 名称相对于终端服务器会话(而并非相对于进程)的范围。
Monitor 类
Monitor 类通过向单个线程授予对象锁来控制对对象的访问。对象锁提供限制访问代码块(通常称为临界区)的能力。当一个线程拥有对象的锁时,其他任何线程都不能获取该锁。还可以使用 Monitor 来确保不会允许其他任何线程访问正在由锁的所有者执行的应用程序代码节,除非另一个线程正在使用其他的锁定对象执行该代码。
Monitor 具有以下功能:
-
它根据需要与某个对象相关联。
-
它是未绑定的,也就是说可以直接从任何上下文调用它。
-
不能创建 Monitor 类的实例。
将为每个同步对象来维护以下信息:
-
对当前持有锁的线程的引用。
-
对就绪队列的引用,它包含准备获取锁的线程。
-
对等待队列的引用,它包含正在等待锁定对象状态变化通知的线程。
-
下表描述了访问同步对象的线程可以采取的操作:
操作
说明
Enter ,TryEnter
获取对象锁。此操作同样会标记临界区的开头。其他任何线程都不能进入临界区,除非它使用其他锁定对象执行临界区中的指令。
Wait
释放对象上的锁以便允许其他线程锁定和访问该对象。在其他线程访问对象时,调用线程将等待。脉冲信号用于通知等待线程有关对象状态的更改。
Pulse (信号),PulseAll
向一个或多个等待线程发送信号。该信号通知等待线程锁定对象的状态已更改,并且锁的所有者准备释放该锁。等待线程被放置在对象的就绪队列中以便它可以最后接收对象锁。一旦线程拥有了锁,它就可以检查对象的新状态以查看是否达到所需状态。
Exit
释放对象上的锁。此操作还标记受锁定对象保护的临界区的结尾。
使用 Enter 和 Exit 方法标记临界区的开头和结尾。如果临界区是一个连续指令集,则由 Enter 方法获取的锁将保证只有一个线程可以使用锁定对象执行所包含的代码。在这种情况下,建议您将这些指令放在 try 块中,并将 Exit 指令放在 finally 块中。此功能通常用于同步对类的静态或实例方法的访问。 Enter 和 Exit 方法提供的功能与 C# lock语句(在 Visual Basic 中为 SyncLock)提供的功能相同,区别在于 lock 和 SyncLock 将 Enter(Object, Boolean) 方法重载和 Exit 方法包装在 try…finally 块中(Visual Basic 中为 Try…Finally 中)以确保释放监视器。
从 .NET Framework 4 开始,有两组用于 Enter 和 TryEnter 方法的重载。一组重载具有一个 ref(在 Visual Basic 中为 ByRef)Boolean 参数,在获取锁定时自动设置为true,即使在获取锁定时引发了异常。如果释放在所有事例中的锁定非常重要,即使在该锁定保护的资源的状态可能不一致时,也请使用这些重载。
如果临界区跨越整个方法,则可以通过将 System.Runtime.CompilerServices.MethodImplAttribute 放置在方法上并在 MethodImplAttribute 的构造函数中指定Synchronized 值来实现上述锁定功能。使用该属性后就不需要 Enter 和 Exit 语句了。请注意,该属性将使当前线程持有锁,直到方法返回;如果可以更早释放锁,则使用Monitor 类或 C#lock 语句而不是该属性。
尽管锁定和释放给定对象的 Enter 和 Exit 语句可以跨越成员或类的边界或同时跨越两者的边界,但并不推荐这样做。
当选择要同步的对象时,应只锁定私有或内部对象。锁定外部对象可能导致死锁,这是因为不相关的代码可能会出于不同的目的而选择锁定相同的对象。