C# 锁机制全景与高效实践:从 Monitor 到 .NET 9 全新 Lock
引言:线程安全与锁的基本概念
线程安全
在多线程编程中,保障共享资源的安全访问依赖于有效的线程同步机制。理解并处理好以下两个核心概念至关重要:
- 线程安全:指某个类、方法或数据结构能够在被多个线程同时访问或修改时,依然保持内部状态的一致性,并产生预期的结果。这通常意味着需要对共享状态(如全局变量、静态变量或对象实例字段)的并发访问进行有效管控,防止数据损坏或不一致性。
- 竞态条件 (Race Condition): 是一种典型的并发缺陷。当多个线程在缺乏适当同步机制的情况下,无序地、竞争性地访问或修改共享资源时,程序执行结果变得依赖于无法预测的线程调度时序(即执行顺序)。这种不确定性常常会导致数据错误、程序崩溃或行为异常。竞态条件是线程安全缺失的直接体现。
锁的基本概念
- 锁的本质:锁是一种同步工具,用于确保共享资源的互斥访问(一次只有一个线程使用)。当一个线程获得锁并执行被保护的代码段(临界区)时,其他试图获取同一锁的线程会被阻塞或等待,直到锁被释放。
- 锁的目标:在保证正确性的前提下,最大化并发度和系统吞吐量,最小化延迟。
- 锁的代价:
- 阻塞开销:操作系统调度上下文切换的成本。
- 自旋开销:忙等待消耗CPU周期。
- 死锁风险:线程因相互等待对方释放锁而永久僵持。
- 优先级反转:低优先级线程持有高优先级线程需要的锁。
- 复杂性:使用不当可能导致程序难以理解和调试。
- 选择锁的依据:临界区大小、等待时间长短、竞争激烈程度、读/写比例、进程边界、公平性要求等。
1. Monitor
原理
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语句语法直观。 - 对于短临界区效率较高。
- Monitor 锁是可重入(Reentrancy)的。同一个线程可以多次获得同一个锁对象上的锁(进入嵌套的 lock 块)。计数器会增加,只有等计数器归零时锁才会被释放。
缺点
- 可能导致死锁,如果锁使用不当。Monitor.TryEnter(object obj, int timeoutMilliseconds) 允许设置等待超时,是避免死锁的重要手段。
- 不支持多读单写场景。
- .NET 的 Monitor 锁是非公平的(Windows CLR 实现)。当锁释放时,操作系统从等待队列中选择下一个唤醒的线程是不确定的,不一定是最早等待的那个(这有助于提高吞吐量,但可能导致某些线程“饥饿”)。
2. System.Threading.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语句无缝集成,语法简洁。
缺点
- 需要.NET 9或更高版本。
- 开发者对其熟悉度较低。
3. Mutex
原理
Mutex(互斥锁)是一种支持进程间同步的互斥锁机制,确保只有一个线程或进程访问共享资源。- 可以通过命名互斥锁实现跨进程同步。
- 比 Monitor/lock 重得多(涉及系统调用)。
- 支持安全访问系统资源(如文件、硬件设备句柄)。
操作方式
private static Mutex _mutex = new Mutex();
_mutex.WaitOne();
// 临界区代码
_mutex.ReleaseMutex();
应用场景
- 跨进程同步,如确保应用程序的单一实例运行。
- 保护共享资源,如文件或数据库。
最佳实践
- 使用命名互斥锁(如
new Mutex(false, "MyAppMutex"))进行进程间同步。 - 尽快释放互斥锁,减少阻塞时间。
注意
- 重入性:命名 Mutex 默认是可重入的(同一个线程)。匿名(未命名)Mutex 在 .NET Framework 默认可重入,在 .NET Core+ 中默认为 .NoRecursion 行为。
- 自动释放:如果持有 Mutex 的线程终止(例如崩溃),操作系统会自动释放锁(这可能导致程序逻辑错误),并且下一个等待的线程可能接收到 AbandonedMutexException。
优点
- 支持进程间同步。
- 提供可靠的互斥访问。
缺点
- 由于涉及内核模式转换,性能较低。
- 开销较大,不适合高频短临界区。
4. SpinLock
原理
SpinLock是一种互斥锁,线程在尝试获取锁时会通过自旋(循环检查)等待锁可用,适用于极短的临界区。
操作方式
private SpinLock _spinLock = new SpinLock();
bool lockTaken = false;
try
{
_spinLock.Enter(ref lockTaken);
// 临界区代码
}
finally
{
if (lockTaken)
{
_spinLock.Exit();
}
}
应用场景
- 极短的临界区,锁持有时间短于上下文切换成本。
- 高并发场景,锁竞争频繁但持续时间短。
最佳实践
- 仅用于极短临界区。
- 避免在低竞争或长临界区场景中使用。
优点
- 对于短临界区开销低。
- 无上下文切换。
缺点
- 如果锁持有时间长,会浪费CPU周期。
- 不适合长临界区。
5. ReaderWriterLockSlim
原理
ReaderWriterLockSlim允许多个线程同时读取资源,但写操作互斥,且写时不允许读操作,适合读多写少的场景。
有几种不同的锁定模式:
- 读取锁 (Read Lock):共享模式,允许多个线程同时持有。
- 写入锁 (Write Lock):独占模式,一旦持有,排斥所有读取锁和其他写入锁。
- 可升级读取锁 (Upgradeable Read Lock):一种特殊模式,允许一个读取线程在持有读锁的同时,后续有需要时可以原子性地升级 (Upgrade)为写入锁(避免先释放读锁再尝试拿写锁过程中出现竞态或死锁)
操作方式
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(); // 释放锁
}
}
应用场景
- 读操作频繁、写操作较少的场景,如缓存系统。
最佳实践
- 确保写操作快速,减少读线程阻塞。
- 避免长时间持有写锁,防止写者饥饿。
注意
- ReaderWriterLockSlim 性能更好,语义更清晰,设计更合理。强烈建议总是使用 ReaderWriterLockSlim 而不是 ReaderWriterLock。
- 性能特征:在纯读场景下并发度接近无锁;写操作开销比普通互斥锁略高(需要管理读写状态转换);升级操作开销适中。
- 公平性与策略:提供了构造参数 LockRecursionPolicy.NoRecursion / .SupportsRecursion 和 ReaderWriterLockSlim(lockRecursionPolicy) 来控制递归行为。也涉及公平性问题(如读者优先或写者优先,ReaderWriterLockSlim 有机制防止写者饿死)。
优点
- 允许多个线程同时读取,提高性能。
- 适合读多写少场景。
缺点
- 使用复杂,需管理读写锁状态。不恰当地嵌套获取不同类型的锁(特别是尝试升级锁失败时等待其他锁)会导致死锁。
- 可能导致写者饥饿。
6. Semaphore 和 SemaphoreSlim
原理
Semaphore控制对资源池的并发访问,限制同时访问的线程数。Semaphore:内核模式,支持跨进程、命名。- SemaphoreSlim:轻量级用户模式实现(必要时退化到内核),仅进程内有效,性能开销远小于 Semaphore。绝大多数进程内场景应优先使用 SemaphoreSlim。
- SemaphoreSlim 默认使用公平队列(FIFO),有助于防止饥饿。Semaphore 的公平性由操作系统决定。
操作方式
private Semaphore _semaphore = new Semaphore(3, 3); // 初始和最大计数
//WaitOne/WaitAsync:尝试获取一个令牌(信号)。若无可用令牌则阻塞/异步等待
_semaphore.WaitOne();
// Release:释放一个令牌
_semaphore.Release();
SemaphoreSlim使用方式类似。
应用场景
- 限制并发访问特定资源的数量(API调用限流、连接池控制、异步任务并发度控制)。
最佳实践
- 使用
Semaphore进行进程间同步,SemaphoreSlim用于进程内。 - 设置合理的初始和最大计数。
优点
- 灵活控制并发级别。
SemaphoreSlim性能较高。
缺点
- 使用较复杂。
- 可能导致死锁。
7. EventWaitHandle、AutoResetEvent、ManualResetEvent、ManualResetEventSlim
原理
事件用于线程间信号传递。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。
8. CountdownEvent
原理
初始化一个计数(N)。线程调用 Signal() 来递减计数。当计数达到0时,所有在该对象上 Wait() 的线程被释放。适用于“N个任务完成后继续”的场景。
操作方式
private CountdownEvent _countdown = new CountdownEvent(3);
_countdown.Wait(); // 等待计数归零
// 执行操作
_countdown.Signal(); // 减少计数
应用场景
- 主线程等待一组分散操作的完成,模拟部分 Task.WaitAll 效果但有更多控制(可在操作执行过程中动态调整计数)。
最佳实践
- 设置正确的初始计数。
- 确保所有信号都发送,避免死锁。
优点
- 便于等待多个事件。
缺点
- 仅限于计数场景。
9. Barrier
原理
允许多个线程分阶段执行任务,并确保所有参与线程在一个共同的屏障点(Phase)同步汇合(都到达后)才能继续下一阶段。
操作方式
private Barrier _barrier = new Barrier(3);
_barrier.SignalAndWait(); // 信号并等待其他线程
// 继续执行
应用场景
- 并行算法中协调多个线程的阶段,如分治算法、复杂数据并行流水线处理。
最佳实践
- 确保所有参与者调用
SignalAndWait。
优点
- 协调多线程分阶段执行。
缺点
- 设置复杂,需确保所有线程参与。
10. SpinWait
原理
SpinWait通过自旋等待条件成立,适合短时间等待。
操作方式
SpinWait.SpinUntil(() => someCondition);
应用场景
- 短时间等待条件成立,如检查标志位。
最佳实践
- 用于预期很快满足的条件。
- 避免长时间自旋。
优点
- 避免上下文切换。
缺点
- 长时间等待浪费CPU资源。
11. 无锁替代
不可变性 (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:
- Channel (System.Threading.Channels):.NET Core 2.1+ 引入。高性能、无锁/有界可选的生产者-消费者队列替代方案(取代 BlockingCollection 和无锁队列手动实现)。支持单/多生产者、单/多消费者。是编写异步管道、处理背压 (Backpressure) 的首选。
var channel = Channel.CreateUnbounded<T>();
// 生产者
await channel.Writer.WriteAsync(item);
// 消费者
while (await channel.Reader.WaitToReadAsync())
while (channel.Reader.TryRead(out var item)) { ... }- ValueTask / IValueTaskSource:Task 的轻量级替代(减少了堆分配),尤其在同步完成路径上优化显著。
Immutable Collections (System.Collections.Immutable):提供线程安全的不可变集合,通过原子替换整个集合引用来“修改”数据。读操作非常高效(无需锁),写操作创建新集合,适合读远多于写的共享数据。
专为并发访问设计的内置集合:
- ConcurrentDictionary<TKey, TValue>:高效、低锁竞争、可并行的字典。
- ConcurrentQueue / ConcurrentStack:先进先出(FIFO) / 后进先出(LIFO)队列,基于CAS实现,避免锁争用。
- BlockingCollection:有界/无界生产者-消费者队列(底层使用 ConcurrentQueue 等),提供 Take() 阻塞语义(Channel 通常是更好的异步选择)。支持优雅取消和完成通知。
12. 结语
选择合适的同步原语取决于应用程序需求,如是否需要进程间同步、读写分离或高性能。System.Threading.Lock是C# 13 中的新选择,性能优于Monitor,适合大多数互斥场景。开发者应根据场景权衡性能、复杂性和功能,确保线程安全的同时避免死锁和性能瓶颈。
13. 附件表格对比
| 同步原语 | 互斥性 | 允许多读 | 进程间支持 | 性能 | 示例用例 | 是否支持可重入 |
|---|---|---|---|---|---|---|
| Monitor | 是 | 否 | 否 | 高 | 保护共享变量 | 是 |
| System.Threading.Lock | 是 | 否 | 否 | 极高 | 高性能互斥锁 | 是 |
| Mutex | 是 | 否 | 是 | 低 | 进程间同步 | 是 |
| SpinLock | 是 | 否 | 否 | 极高 | 极短临界区 | 否 |
| ReaderWriterLockSlim | 是(写) | 是 | 否 | 中 | 读多写少资源 | 是 |
| Semaphore | 否 | 无 | 是 | 中 | 限制并发访问 | 否 |
| SemaphoreSlim | 否 | 无 | 否 | 高 | 进程内并发控制 | 否 |
| EventWaitHandle | 否 | 无 | 是 | 中 | 线程/进程间信号传递 | 否 |
| ManualResetEventSlim | 否 | 无 | 否 | 高 | 进程内信号传递 | 否 |
| CountdownEvent | 否 | 无 | 否 | 中 | 等待多个信号 | 否 |
| Barrier | 否 | 无 | 否 | 中 | 分阶段线程执行 | 否 |
| Interlocked | 否 | 无 | 否 | 极高 | 原子操作 | 否 |
| SpinWait | 否 | 无 | 否 | 高 | 短时间自旋等待 | 否 |
❝
由于资料验证范围太广,难免会有遗漏,如果上述表格内的内容有问题,请在评论区告诉我
C# 锁机制全景与高效实践:从 Monitor 到 .NET 9 全新 Lock的更多相关文章
- Java多线程(五) —— 线程并发库之锁机制
参考文献: http://www.blogjava.net/xylz/archive/2010/07/08/325587.html 一.Lock与ReentrantLock 前面的章节主要谈谈原子操作 ...
- java 多线程总结篇4——锁机制
在开发Java多线程应用程序中,各个线程之间由于要共享资源,必须用到锁机制.Java提供了多种多线程锁机制的实现方式,常见的有synchronized.ReentrantLock.Semaphore. ...
- Linux 2.6内核中新的锁机制--RCU
转自:http://www.ibm.com/developerworks/cn/linux/l-rcu/ 一. 引言 众所周知,为了保护共享数据,需要一些同步机制,如自旋锁(spinlock),读写锁 ...
- MYSQL数据库重点:事务与锁机制
一.事务 一组连续的数据库操作,每一次操作都成功,整个事务就成功,只要有一步出错,整个事务就失败: MySQL事务与存储引擎相关 1.MyISAM:不支持事务,用于只读程序提高性能 2.InnoDB: ...
- MySQL 事务与锁机制
下表展示了本人安装的MariaDB(10.1.19,MySQL的分支)所支持的所有存储引擎概况,其中支持事务的有InnoDB.SEQUENCE,另外InnoDB还支持XA事务,MyISAM不支持事务. ...
- Java CAS同步机制 原理详解(为什么并发环境下的COUNT自增操作不安全): Atomic原子类底层用的不是传统意义的锁机制,而是无锁化的CAS机制,通过CAS机制保证多线程修改一个数值的安全性。
精彩理解: https://www.jianshu.com/p/21be831e851e ; https://blog.csdn.net/heyutao007/article/details/19 ...
- JAVA锁机制-可重入锁,可中断锁,公平锁,读写锁,自旋锁,
如果需要查看具体的synchronized和lock的实现原理,请参考:解决多线程安全问题-无非两个方法synchronized和lock 具体原理(百度) 在并发编程中,经常遇到多个线程访问同一个 ...
- 内核中的锁机制--RCU
一. 引言 众所周知,为了保护共享数据,需要一些同步机制,如自旋锁(spinlock),读写锁(rwlock),它们使用起来非常简单,而且是一种很有效的同步机制,在UNIX系统和Linux系统中得到了 ...
- 转 : 深入解析Java锁机制
深入解析Java锁机制 https://mp.weixin.qq.com/s?__biz=MzU0OTE4MzYzMw%3D%3D&mid=2247485524&idx=1&s ...
- linux RCU锁机制分析
openVswitch(OVS)源代码之linux RCU锁机制分析 分类: linux内核 | 标签: 云计算,openVswitch,linux内核,RCU锁机制 | 作者: yuzhih ...
随机推荐
- CompletableFuture API介绍及使用
1. 介绍 CompletableFuture 是 Java 8 引入的一个用于异步编程的类,位于 java.util.concurrent 包中.它是对 Future 的增强,提供了更强大的功能来支 ...
- vue学习一(指令1.v-text,v-html,插值表达式{{msg}})
一.1.v-text,v-html,插值表达式{{msg}} 注:v-text解决差值表达式闪烁问题,因为他是属性不是差值表达式 1.1.v-text: 是没有闪烁问题的,会覆盖标签的元素中原本的内容 ...
- JdbcTemplate 自定义返回的结果集字段和实体类映射
废话不多:抄袭代码 package com.webank.wedatasphere.qualitis.handler; import com.webank.wedatasphere.qualitis. ...
- Linux系统挂载未分配硬盘空间
先查看未挂载之前的磁盘使用情况 发现磁盘使用率已经达到了96%,迫切需要扩容 查看分区情况fdisk –l 首先确保有可分配的磁盘空间 发现/dev/vda下有400多个G 的空间 所以将/dev/v ...
- Java 8的新特性还不了解?快进来!
能坚持别人不能坚持的,才能拥有你想拥有的.关注 编程大道,让我们一起成长
- Linux脚本-自动运维部署脚本
背景 公司正常的业务流程是生产服务器上部署的一个程序去读取数据库,并获取所有ip信息,启动socket连接,发送相关业务指令. 目前有一个需求,需要单独测试一个ip,这个单独的ip需要使用另外的程序测 ...
- 如果在安装32位Oracle客户端组件的情况下64位模式运行, 将出现此问题.
场景重现 在一台Windows 7 32-bit电脑上 安装了Oracle 11gR2 32-bit的客户端 用 VS2010 写的一个基于数据库驱动的项目 操作Oracle数据库都挺正常的 后来.. ...
- 🎀dubbo QOS介绍及命令
简介 在Dubbo中,QoS(Quality of Service)功能是一个非常重要的特性,用于提供对运行时服务的查询和控制能力. QoS的概念源自网络设备中的服务质量保障机制,但在Dubbo中,它 ...
- Asp.net mvc基础(三)View的查找
1.指定转到的视图 View("指定的视图名称"); 优先于寻找Action方法名称可以创建的视图的文件夹,如果没有,就去View文件夹下的Shared文件夹寻找指定的视图名称. ...
- Docker光速入门
1.docker是什么,能干什么 Docker 是一个开源的应用容器引擎,基于Go语言并遵从Apache2.0协议开源. Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级.可移植的容器中 ...