在多线程编程中,保障共享资源的安全访问依赖于有效的线程同步机制。理解并处理好以下两个核心概念至关重要:
Monitor
类提供了一种互斥锁机制,确保同一时间只有一个线程可以访问临界区。它是C#中lock
语句的基础,通过Monitor.Enter
和Monitor.Exit
实现锁的获取和释放。
基于对象的内部 SyncBlock 索引关联的一个系统锁对象。每个.NET对象在堆上分配时,都有一个关联的 Sync Block Index (SBI)。当首次对这个对象使用 lock 时,SBI 被分配并指向操作系统内核中的一个真正的锁对象(比如 Windows 的 CRITICAL_SECTION)。
当锁已被占用时,后续请求的线程会进入内核等待状态,发生上下文切换。
Monitor.Wait(object obj), Monitor.Pulse(object obj), Monitor.PulseAll(object obj) 提供了在锁内等待特定条件成立的能力(类似 ConditionVariable),可用于构建生产者-消费者模式等。
lock
语句是使用Monitor
的简便方式:
private readonly object _lock = new object();
lock (_lock)
{
// 临界区代码
}
等价于:
Monitor.Enter(_lock);
try
{
// 临界区代码
}
finally
{
Monitor.Exit(_lock);
}
private readonly object _lock = new object();
)进行锁定,避免死锁。typeof(MyClass)
),因为其他代码可能也会锁定它们。lock
语句语法直观。System.Threading.Lock
是.NET 9(C# 13)引入的新同步原语,旨在提供比Monitor
更高效的互斥锁机制。它通过EnterScope
方法支持using
语句,确保锁自动释放,降低死锁风险。
直接使用:
private readonly Lock _lock = new Lock();
using (_lock.EnterScope())
{
// 临界区代码
}
或在C# 13及以上版本中使用lock
语句:
lock (_lock)
{
// 临界区代码
}
Monitor
类似,用于保护共享资源。Lock
实例。using
语句确保锁自动释放。Lock
对象转换为object
或其他类型,以防止编译器警告。Monitor
高约25%。| Method | Mean | Error | StdDev | Ratio | Gen0 | Allocated | Alloc Ratio |
|------------------------- |----------:|---------:|---------:|------:|-------:|----------:|------------:|
| CountTo1000WithLock | 107.22 us | 1.561 us | 1.460 us | 1.00 | 0.1221 | 1.06 KB | 1.00 |
| CountTo1000WithLockClass | 75.73 us | 0.884 us | 0.827 us | 0.71 | 0.1221 | 1.05 KB | 0.99 |
Dispose
模式自动释放锁,降低死锁风险。lock
语句无缝集成,语法简洁。Mutex
(互斥锁)是一种支持进程间同步的互斥锁机制,确保只有一个线程或进程访问共享资源。private static Mutex _mutex = new Mutex();
_mutex.WaitOne();
// 临界区代码
_mutex.ReleaseMutex();
new Mutex(false, "MyAppMutex")
)进行进程间同步。SpinLock
是一种互斥锁,线程在尝试获取锁时会通过自旋(循环检查)等待锁可用,适用于极短的临界区。
private SpinLock _spinLock = new SpinLock();
bool lockTaken = false;
try
{
_spinLock.Enter(ref lockTaken);
// 临界区代码
}
finally
{
if (lockTaken)
{
_spinLock.Exit();
}
}
ReaderWriterLockSlim
允许多个线程同时读取资源,但写操作互斥,且写时不允许读操作,适合读多写少的场景。
有几种不同的锁定模式:
private readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();
public string ReadData()
{
_rwLock.EnterReadLock(); // 获取读锁
try
{
// 安全读取共享数据
return _cachedData;
}
finally
{
_rwLock.ExitReadLock(); // 释放读锁
}
}
public void UpdateData(string newData)
{
_rwLock.EnterWriteLock(); // 获取写锁
try
{
// 安全更新共享数据
_cachedData = newData;
}
finally
{
_rwLock.ExitWriteLock(); // 释放写锁
}
}
// 使用可升级锁 (避免“写者饥饿”风险):
public void UpdateIfCondition(string newData, Func<bool> condition)
{
_rwLock.EnterUpgradeableReadLock(); // 获取可升级读锁
try
{
if (condition())
{
_rwLock.EnterWriteLock(); // 升级为写锁
try
{
// 安全更新共享数据
_cachedData = newData;
}
finally
{
_rwLock.ExitWriteLock(); // 降级回可升级读锁
}
}
}
finally
{
_rwLock.ExitUpgradeableReadLock(); // 释放锁
}
}
Semaphore
控制对资源池的并发访问,限制同时访问的线程数。Semaphore
:内核模式,支持跨进程、命名。private Semaphore _semaphore = new Semaphore(3, 3); // 初始和最大计数
//WaitOne/WaitAsync:尝试获取一个令牌(信号)。若无可用令牌则阻塞/异步等待
_semaphore.WaitOne();
// Release:释放一个令牌
_semaphore.Release();
SemaphoreSlim
使用方式类似。
Semaphore
进行进程间同步,SemaphoreSlim
用于进程内。SemaphoreSlim
性能较高。事件用于线程间信号传递。AutoResetEvent
在信号一个等待线程后自动重置;ManualResetEvent
保持信号状态直到手动重置;ManualResetEventSlim
是轻量级版本。
AutoResetEvent
示例:
private AutoResetEvent _event = new AutoResetEvent(false);
_event.WaitOne(); // 等待信号
// 执行操作
_event.Set(); // 发送信号
ManualResetEvent
示例:
private ManualResetEvent _event = new ManualResetEvent(false);
_event.WaitOne(); // 等待信号
// 执行操作
_event.Set(); // 发送信号
_event.Reset(); // 重置事件
AutoResetEvent
进行一对一信号传递。ManualResetEvent
广播信号给多个线程。ManualResetEvent
。初始化一个计数(N)。线程调用 Signal() 来递减计数。当计数达到0时,所有在该对象上 Wait() 的线程被释放。适用于“N个任务完成后继续”的场景。
private CountdownEvent _countdown = new CountdownEvent(3);
_countdown.Wait(); // 等待计数归零
// 执行操作
_countdown.Signal(); // 减少计数
允许多个线程分阶段执行任务,并确保所有参与线程在一个共同的屏障点(Phase)同步汇合(都到达后)才能继续下一阶段。
private Barrier _barrier = new Barrier(3);
_barrier.SignalAndWait(); // 信号并等待其他线程
// 继续执行
SignalAndWait
。SpinWait
通过自旋等待条件成立,适合短时间等待。
SpinWait.SpinUntil(() => someCondition);
不可变性 (Immutability):一旦创建对象就不可修改。避免了修改引起的同步需求(readonly 字段,记录类型 record)。
线程本地存储 (Thread-Local Storage - TLS):ThreadStaticAttribute, AsyncLocal变量,ThreadLocal。每个线程使用自己独立的数据副本(适用性有限)。
Interlocked 类:提供对简单类型(int, long, IntPtr, float, double, object 引用)执行原子操作的静态方法(Increment, Decrement, Add, Exchange, CompareExchange)。是最轻量级的“锁”,基于 CPU 的原子指令实现,性能极高,无锁开销。
private int _counter = 0;
public void IncrementSafely()
{
Interlocked.Increment(ref _counter); // 原子+1
}
public void SetIfEqual(int newValue, int expected)
{
Interlocked.CompareExchange(ref _counter, newValue, expected); // CAS
}
基于任务的异步模式 (TAP) 与 Task:
var channel = Channel.CreateUnbounded<T>();
// 生产者
await channel.Writer.WriteAsync(item);
// 消费者
while (await channel.Reader.WaitToReadAsync())
while (channel.Reader.TryRead(out var item)) { ... }
Immutable Collections (System.Collections.Immutable):提供线程安全的不可变集合,通过原子替换整个集合引用来“修改”数据。读操作非常高效(无需锁),写操作创建新集合,适合读远多于写的共享数据。
专为并发访问设计的内置集合:
选择合适的同步原语取决于应用程序需求,如是否需要进程间同步、读写分离或高性能。System.Threading.Lock
是C# 13 中的新选择,性能优于Monitor
,适合大多数互斥场景。开发者应根据场景权衡性能、复杂性和功能,确保线程安全的同时避免死锁和性能瓶颈。
同步原语 | 互斥性 | 允许多读 | 进程间支持 | 性能 | 示例用例 | 是否支持可重入 |
---|---|---|---|---|---|---|
Monitor | 是 | 否 | 否 | 高 | 保护共享变量 | 是 |
System.Threading.Lock | 是 | 否 | 否 | 极高 | 高性能互斥锁 | 是 |
Mutex | 是 | 否 | 是 | 低 | 进程间同步 | 是 |
SpinLock | 是 | 否 | 否 | 极高 | 极短临界区 | 否 |
ReaderWriterLockSlim | 是(写) | 是 | 否 | 中 | 读多写少资源 | 是 |
Semaphore | 否 | 无 | 是 | 中 | 限制并发访问 | 否 |
SemaphoreSlim | 否 | 无 | 否 | 高 | 进程内并发控制 | 否 |
EventWaitHandle | 否 | 无 | 是 | 中 | 线程/进程间信号传递 | 否 |
ManualResetEventSlim | 否 | 无 | 否 | 高 | 进程内信号传递 | 否 |
CountdownEvent | 否 | 无 | 否 | 中 | 等待多个信号 | 否 |
Barrier | 否 | 无 | 否 | 中 | 分阶段线程执行 | 否 |
Interlocked | 否 | 无 | 否 | 极高 | 原子操作 | 否 |
SpinWait | 否 | 无 | 否 | 高 | 短时间自旋等待 | 否 |