C# lock 和 Monitor
lock
lock 关键字是 C# 中最常用的线程同步工具之一,它使得一段代码在同一时间只能被一个线程执行,以确保对共享资源的访问不会被其他线程干扰。
lock 关键字的语法如下:
lock (lockObject) { // 要同步的代码块 }
- lockObject:是一个对象实例,用于定义临界区域。多个线程在执行 lock 代码块时,如果它们尝试获取相同的 lockObject 锁,则只有一个线程能够成功获取锁,其他线程将被阻塞,直到锁被释放。
- lock 关键字的优点是简单易用,编译器会自动处理锁的获取和释放,使得代码更加清晰和简洁。
- lock 关键字的缺点是在锁的范围内不能发生阻塞,否则会导致死锁。此外,由于 lock 是语言级别的关键字,所以无法对它进行更细粒度的控制。
在 C# 中,lock 关键字实际上是对 Monitor 类的一种简化用法,它会被编译器转换成 Monitor 类的方法调用。当你使用 lock 关键字时,编译器会自动创建一个 Monitor 对象,并在代码块的开始处调用 Monitor.Enter 方法获取锁,在代码块的结束处调用 Monitor.Exit 方法释放锁。
Monitor
Monitor 类提供了细粒度的线程同步控制,它允许程序员手动管理同步块的进入和退出,并且支持更复杂的线程同步操作。
Monitor 类的基本用法如下:
// 进入同步代码块 Monitor.Enter(lockObject); try { // 同步代码块 } finally { // 退出同步代码块 Monitor.Exit(lockObject); }
- Monitor 类需要使用 Enter 和 Exit 方法手动管理同步块的进入和退出。Enter 方法用于进入同步代码块,Exit 方法用于退出同步代码块。通常会在 try-finally 块中使用,以确保锁的释放。
- Monitor 类还提供了 Wait、Pulse 和 PulseAll 方法,用于线程之间的通信和同步。Wait 方法用于让线程等待,Pulse 方法用于唤醒等待的线程,PulseAll 方法用于唤醒所有等待的线程。
- Monitor 类的优点是可以手动控制同步块的进入和退出,以及支持更复杂的线程同步操作,比如等待和唤醒线程。
- Monitor 类的缺点是使用起来相对复杂,需要手动管理锁的获取和释放,容易出现遗漏或错误,导致死锁或其他线程同步问题。
案例
分享一个简单的火车票购票系统的例子,来说明 lock 关键字的使用方式以及与Monitor 类的对比。
假设有一个火车票购票系统,多个用户同时在线购票。我们希望在多线程环境中正确管理火车票的分配,避免出现超卖或者座位重复分配的情况。
使用lock
创建一个 TicketSystem 类来管理座位的状态和购票操作:
using System; using System.Threading; public class TicketSystem { private object lockObject = new object(); // 用于同步的锁对象 private int availableSeats; // 可用座位数量 public TicketSystem(int totalSeats) { availableSeats = totalSeats; } public void BuyTicket(string customerName, int numSeats) { lock (lockObject) { if (availableSeats >= numSeats) { // 模拟购票操作 Console.WriteLine($"{customerName} 购买了 {numSeats} 张票。"); availableSeats -= numSeats; } else { Console.WriteLine($"对不起,{customerName},没有足够的座位。"); } } } } class Program { static void Main(string[] args) { TicketSystem ticketSystem = new TicketSystem(15); // 总共15个座位 Thread[] threads = new Thread[20]; // 20个线程模拟20个用户 for (int i = 0; i < 20; i++) { threads[i] = new Thread(() => { string customerName = $"User {Thread.CurrentThread.ManagedThreadId}"; ticketSystem.BuyTicket(customerName, 1); }); } // 启动所有线程 foreach (var thread in threads) { thread.Start(); } // 等待所有线程执行完成 foreach (var thread in threads) { thread.Join(); } Console.WriteLine("所有用户购票结束。"); Console.ReadKey(); } }
使用Monitor
using System; using System.Threading; public class TicketSystem { private object lockObject = new object(); // 用于同步的锁对象 private int availableSeats; // 可用座位数量 public TicketSystem(int totalSeats) { availableSeats = totalSeats; } public void BuyTicket(string customerName, int numSeats) { Monitor.Enter(lockObject); try { if (availableSeats >= numSeats) { // 模拟购票操作 Console.WriteLine($"{customerName} 购买了 {numSeats} 张票。"); availableSeats -= numSeats; } else { Console.WriteLine($"对不起,{customerName},没有足够的座位。"); } } finally { Monitor.Exit(lockObject); } } } class Program { static void Main(string[] args) { TicketSystem ticketSystem = new TicketSystem(15); // 总共15个座位 Thread[] threads = new Thread[20]; // 20个线程模拟20个用户 for (int i = 0; i < 20; i++) { threads[i] = new Thread(() => { string customerName = $"User {Thread.CurrentThread.ManagedThreadId}"; ticketSystem.BuyTicket(customerName, 1); }); } // 启动所有线程 foreach (var thread in threads) { thread.Start(); } // 等待所有线程执行完成 foreach (var thread in threads) { thread.Join(); } Console.WriteLine("所有用户购票结束。"); Console.ReadKey(); } }
上面的场景中使用lock和Moniter效果都是相同的,接下来描述lock 没有的功能。Monitor的Wait和Pulse 方法
Monitor的Wait和Pulse
我们知道春节火车票不是一次性放票的而是分段开放的,同时也有退票的情况。这将导致余票的数量动态变化。多个用户线程同时等待抢票,抢票系统一旦检测到有新的余票释放,将通知所有等待的线程进行抢票。这种情况下,我们需要使用线程同步机制确保多个线程之间的正确操作,并及时通知用户线程有新票可供抢购。
using System; using System.Threading; public class TicketSystem { private int availableTickets = 0; private object lockObject = new object(); public TicketSystem(int initialTickets) { this.availableTickets = initialTickets; } public void SellTicket() { Monitor.Enter(lockObject); try { while (availableTickets == 0) { Console.WriteLine($"{Thread.CurrentThread.Name} is waiting for available tickets..."); Monitor.Wait(lockObject); } // Simulate selling ticket availableTickets--; Console.WriteLine($"{Thread.CurrentThread.Name} sold a ticket. Available tickets: {availableTickets}"); } finally { Monitor.Exit(lockObject); } } public void AddTickets(int numTicketsToAdd) { Monitor.Enter(lockObject); try { // Simulate adding new tickets availableTickets += numTicketsToAdd; Console.WriteLine($"Added {numTicketsToAdd} new tickets. Available tickets: {availableTickets}"); // Notify waiting threads Monitor.PulseAll(lockObject); } finally { Monitor.Exit(lockObject); } } } public class Program { public static void Main(string[] args) { TicketSystem ticketSystem = new TicketSystem(0); // 初始没有票 // 创建并启动5个抢票线程 for (int i = 0; i < 5; i++) { Thread thread = new Thread(() => { while (true) { ticketSystem.SellTicket(); Thread.Sleep(1000); // 模拟抢票过程 } }); thread.Name = $"TicketBuyer {i + 1}"; thread.Start(); } // 模拟分段放票 for (int i = 0; i < 3; i++) { Thread.Sleep(3000); // 等待3秒模拟间隔放票 ticketSystem.AddTickets(5); // 每次放5张票 } Console.WriteLine("Press any key to exit..."); Console.ReadKey(); } }
这段代码实现了一个简单的抢票功能,具体功能和实现方式如下:
- 使用
TicketSystem
类管理票务信息,包括余票数量和售票方法。 - 在
Main
方法中创建多个线程模拟不同的抢票者,它们会不断尝试购买票。 - 使用
Monitor.Enter
和Monitor.Exit
实现线程同步,确保多线程访问临界区时的安全。 - 使用
Monitor.Wait
和Monitor.PulseAll
实现线程间的通信,唤醒等待的线程。 - 程序模拟分段开放票务,定时增加新的票。
由于 lock
示例本质上是实现了Monitor
,所以也可以使用lock
+ Monitor.Wait
和 Monitor.PulseAll
方式使用。
public class TicketSystem { private int availableTickets = 0; public TicketSystem(int initialTickets) { this.availableTickets = initialTickets; } public void SellTicket() { lock (this) { while (availableTickets == 0) { Console.WriteLine($"{Thread.CurrentThread.Name} is waiting for available tickets..."); Monitor.Wait(this); } // Simulate selling ticket availableTickets--; Console.WriteLine($"{Thread.CurrentThread.Name} sold a ticket. Available tickets: {availableTickets}"); } } public void AddTickets(int numTicketsToAdd) { lock (this) { // Simulate adding new tickets availableTickets += numTicketsToAdd; Console.WriteLine($"Added {numTicketsToAdd} new tickets. Available tickets: {availableTickets}"); // Notify waiting threads Monitor.PulseAll(this); } } }
缺点
- 性能开销:
- Monitor 是一种内核模式构造,它提供了互斥锁功能,用于保护共享资源免受并发访问的影响。
- 但是,每次在 Monitor 上调用 Enter 或 Exit 方法时,都需要进行用户模式到内核模式的切换,这会导致性能开销。
- 如果应用程序中频繁使用 Monitor,这些切换可能会显著影响整体性能。
- 死锁风险:
- Monitor 的使用需要谨慎,因为不正确的使用可能导致死锁。
- 如果线程在持有锁的情况下等待其他资源,而其他资源又在等待相同的锁,就会发生死锁。
- 死锁是一种严重的问题,会导致应用程序停滞不前。