C# 并发编程核心机制:深入解析 lock、Monitor 与 Interlocked
一、lock 关键字与排他锁
在 C# 中,lock 关键字用于确保同一时刻只有一个线程能够访问被保护的代码块。从底层实现来看,lock 实际上是 System.Threading.Monitor 类的语法糖。
使用 lock 时,锁定的目标对象应当是一个私有的、只读的引用类型(强烈不建议使用字符串或 this 关键字,以避免潜在的死锁或外部干扰)。
基础使用示例
以下代码展示了如何使用 lock 来保护共享资源的并发更新:
class ConcurrentProcessor
{
// 推荐做法:使用私有且只读的对象作为锁
private static readonly object _syncRoot = new object();
private static int _totalScore = 0;
public static void Execute()
{
var workerA = new Thread(ProcessTaskA);
var workerB = new Thread(ProcessTaskB);
workerA.Start();
workerB.Start();
}
private static void ProcessTaskA()
{
lock (_syncRoot)
{
for (int i = 0; i < 5; i++)
{
_totalScore += 10;
Console.WriteLine($"Task A updated score: {_totalScore}");
Thread.Sleep(100);
}
}
}
private static void ProcessTaskB()
{
lock (_syncRoot)
{
for (int i = 0; i < 5; i++)
{
_totalScore += 20;
Console.WriteLine($"Task B updated score: {_totalScore}");
Thread.Sleep(100);
}
}
}
}
性能与适用场景
当线程获取锁后,其他尝试获取同一把锁的线程将被阻塞,直到锁被释放。这种机制会带来一定的上下文切换开销。因此,lock 不适合用于保护长时间运行的操作、繁重的计算或 I/O 密集型任务(如文件读写、网络请求),否则会导致严重的线程饥饿和性能下降。
二、Monitor 类的高级控制
Monitor 是一个静态类,提供了比 lock 更细粒度的线程同步控制。除了基本的互斥锁功能外,它还支持线程间的等待与通知机制。
核心方法解析
- Enter / Exit:获取和释放对象上的排他锁。
- Wait:释放当前对象上的锁,并阻塞当前线程,使其进入等待队列。
- Pulse / PulseAll:唤醒等待队列中的一个或所有线程,使其进入就绪队列(遵循先进先出原则)。
生产者-消费者模式示例
以下代码演示了如何利用 Monitor 的等待和脉冲机制实现线程间的协调:
class MonitorCoordination
{
private static readonly object _lockObj = new object();
private static bool _dataReady = false;
public static void Run()
{
var consumer = new Thread(Consume);
var producer = new Thread(Produce);
consumer.Start();
Thread.Sleep(200); // 确保消费者先进入等待状态
producer.Start();
}
private static void Consume()
{
lock (_lockObj)
{
while (!_dataReady)
{
Console.WriteLine("Consumer: Waiting for data...");
Monitor.Wait(_lockObj); // 释放锁并阻塞
}
Console.WriteLine("Consumer: Data processed successfully.");
}
}
private static void Produce()
{
lock (_lockObj)
{
_dataReady = true;
Console.WriteLine("Producer: Data generated.");
Monitor.Pulse(_lockObj); // 唤醒等待的线程
}
}
}
安全的锁获取模式
在 .NET Framework 4.0 及以上版本中,推荐使用带有 ref bool lockTaken 参数的 Monitor.Enter 重载,以确保在发生异常时能够安全地释放锁:
private static readonly object _safeLock = new object();
public static void SafeMonitorUsage()
{
bool lockTaken = false;
try
{
Monitor.Enter(_safeLock, ref lockTaken);
// 执行临界区代码
}
finally
{
if (lockTaken)
{
Monitor.Exit(_safeLock);
}
}
}
三、Interlocked 与原子操作
Interlocked 类提供了针对简单数据类型(如 int、long、float 等)的轻量级线程同步机制。它通过硬件级别的原子指令来实现操作,避免了重量级锁的开销。
适用场景
当只需要对单个变量进行简单的递增、递减、赋值或比较交换时,Interlocked 的执行效率远高于 lock。但它无法保护包含多个步骤的复杂复合操作。
常用原子操作示例
以下是 Interlocked 类中几个核心方法的应用演示:
class AtomicOperations
{
private static int _requestCount = 0;
private static int _systemState = 0; // 0: 停止, 1: 运行
public static void PerformOperations()
{
// 1. 原子递增:等同于 _requestCount++,但线程安全
Interlocked.Increment(ref _requestCount);
// 2. 原子赋值:将新值赋予变量,并返回变量的原始值
int previousCount = Interlocked.Exchange(ref _requestCount, 1000);
Console.WriteLine($"Count reset. Previous value was: {previousCount}");
// 3. 比较并交换 (CAS 操作)
// 逻辑:如果 _systemState 等于 0,则将其更新为 1;否则不更新。
// 返回值是 _systemState 的原始值。
int originalState = Interlocked.CompareExchange(ref _systemState, 1, 0);
if (originalState == 0)
{
Console.WriteLine("System state successfully transitioned to Running.");
}
else
{
Console.WriteLine("System state transition failed or already running.");
}
}
}
