
C#中的
Monitor类和
lock语句实际上是同一概念的不同表达方式。
lock语句是
Monitor类的语法糖,提供了一种更简洁、易用的方式来获取和释放对象的互斥锁。
lock语句本质上是Monitor.Enter和Monitor.Exit的封装。
// lock 语句
lock (obj)
{
// 受保护的代码
}
// 等价于
Monitor.Enter(obj);
try
{
// 受保护的代码
}
finally
{
Monitor.Exit(obj);
}lock语句保证了即使在受保护的代码块中发生异常,锁也会被正确释放,避免死锁。
C# Monitor类的核心功能是什么?
Monitor类提供了一系列静态方法,用于实现线程同步,它允许线程获取对象的独占锁,并提供了线程等待和通知机制。主要功能包括:
Enter(object obj): 获取指定对象的独占锁。如果锁已经被其他线程持有,当前线程会阻塞,直到锁被释放。 TryEnter(object obj): 尝试获取指定对象的独占锁。如果锁可用,则获取锁并返回true;否则,立即返回
false,不会阻塞。
TryEnter还有带超时时间的重载版本,允许线程等待一段时间。 Exit(object obj): 释放指定对象的独占锁。必须与
Enter配对使用。 Wait(object obj): 释放对象的锁,并阻塞当前线程,直到其他线程调用
Pulse或
PulseAll来唤醒它。必须在持有锁的情况下调用。 Pulse(object obj): 通知等待队列中的一个线程,使其变为就绪状态。必须在持有锁的情况下调用。 PulseAll(object obj): 通知等待队列中的所有线程,使其变为就绪状态。必须在持有锁的情况下调用。
使用Monitor类时,需要特别注意锁的正确释放。如果忘记调用
Exit,或者在
Enter和
Exit之间发生未处理的异常,可能会导致死锁。这也是为什么
lock语句更安全的原因,因为它使用
try-finally块来确保锁总是被释放。
什么时候应该使用Monitor类而不是lock语句?
虽然
lock语句在大多数情况下更方便、更安全,但
Monitor类在某些高级场景下提供了更大的灵活性。例如: 需要尝试获取锁,而不是无限期阻塞:可以使用
Monitor.TryEnter方法。这在某些需要避免长时间阻塞的场景中很有用。 需要更精细的控制线程同步:
Monitor类的
Wait、
Pulse和
PulseAll方法提供了线程等待和通知机制,可以实现更复杂的线程同步逻辑,例如生产者-消费者模式。 在某些特殊情况下,需要手动管理锁的生命周期:虽然不推荐,但在某些特定的性能优化场景下,可能需要手动控制锁的获取和释放。
一个使用Monitor类实现简单线程同步的例子:
class Example
{
private static readonly object _locker = new object();
private static int _counter = 0;
public static void IncrementCounter()
{
Monitor.Enter(_locker);
try
{
_counter++;
Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId}: Counter = {_counter}");
}
finally
{
Monitor.Exit(_locker);
}
}
public static void Main(string[] args)
{
Thread[] threads = new Thread[5];
for (int i = 0; i < 5; i++)
{
threads[i] = new Thread(IncrementCounter);
threads[i].Start();
}
foreach (Thread thread in threads)
{
thread.Join();
}
Console.WriteLine($"Final Counter Value: {_counter}");
}
}在这个例子中,多个线程同时访问和修改
_counter变量。使用
Monitor.Enter和
Monitor.Exit确保了对
_counter的访问是线程安全的。
try-finally块保证了即使在
IncrementCounter方法中发生异常,锁也会被释放。
Monitor类的Wait(), Pulse(), PulseAll()方法具体如何使用?
Wait、
Pulse和
PulseAll是
Monitor类中用于线程间通信的关键方法,它们允许线程在特定条件下挂起自身,等待其他线程发出信号。以下是它们的具体用法:
Wait(object obj):
作用:释放指定对象的锁,并阻塞当前线程。线程会进入对象的等待队列,等待其他线程调用Pulse或
PulseAll来唤醒它。 使用场景:当线程需要等待某个条件满足时,可以使用
Wait方法。例如,在生产者-消费者模式中,当缓冲区为空时,消费者线程可以调用
Wait方法挂起自身,等待生产者线程向缓冲区添加数据。 注意事项:
Wait方法必须在持有锁的情况下调用。调用
Wait方法会自动释放锁,允许其他线程访问共享资源。当线程被唤醒后,它会尝试重新获取锁。
Pulse(object obj):
作用:通知等待队列中的一个线程,使其变为就绪状态。被通知的线程会从等待队列中移除,并尝试重新获取锁。 使用场景:当某个条件变为真时,可以使用Pulse方法通知等待该条件的线程。例如,在生产者-消费者模式中,当生产者线程向缓冲区添加数据后,可以调用
Pulse方法唤醒一个等待的消费者线程。 注意事项:
Pulse方法必须在持有锁的情况下调用。
Pulse方法只会唤醒一个线程,如果有多个线程在等待,只有其中一个会被唤醒。
PulseAll(object obj):
作用:通知等待队列中的所有线程,使其变为就绪状态。所有被通知的线程都会从等待队列中移除,并尝试重新获取锁。 使用场景:当某个条件发生变化,可能影响到所有等待的线程时,可以使用PulseAll方法。例如,在某些复杂的并发场景中,可能需要一次性唤醒所有等待的线程。 注意事项:
PulseAll方法必须在持有锁的情况下调用。
PulseAll方法会唤醒所有等待的线程,这可能会导致竞争,因此需要谨慎使用。
一个使用
Wait、
Pulse实现生产者-消费者模式的例子:
class ProducerConsumer
{
private static readonly object _locker = new object();
private static Queue<int> _queue = new Queue<int>();
private static int _capacity = 5;
public static void Produce()
{
Random random = new Random();
while (true)
{
lock (_locker)
{
while (_queue.Count == _capacity)
{
Console.WriteLine("Producer is waiting, queue is full.");
Monitor.Wait(_locker);
}
int item = random.Next(100);
_queue.Enqueue(item);
Console.WriteLine($"Producer produced: {item}");
Monitor.Pulse(_locker); // 通知一个消费者
}
Thread.Sleep(random.Next(500));
}
}
public static void Consume()
{
Random random = new Random();
while (true)
{
lock (_locker)
{
while (_queue.Count == 0)
{
Console.WriteLine("Consumer is waiting, queue is empty.");
Monitor.Wait(_locker);
}
int item = _queue.Dequeue();
Console.WriteLine($"Consumer consumed: {item}");
Monitor.Pulse(_locker); // 通知一个生产者
}
Thread.Sleep(random.Next(500));
}
}
public static void Main(string[] args)
{
Thread producerThread = new Thread(Produce);
Thread consumerThread = new Thread(Consume);
producerThread.Start();
consumerThread.Start();
Console.ReadKey();
}
}在这个例子中,生产者线程负责向队列中添加数据,消费者线程负责从队列中取出数据。当队列满时,生产者线程会调用
Monitor.Wait挂起自身,等待消费者线程消费数据。当队列为空时,消费者线程会调用
Monitor.Wait挂起自身,等待生产者线程生产数据。
Monitor.Pulse用于通知等待的线程。
