什么是可重入(Reentrant)和线程安全?先分清这两个概念
可重入 ≠ 线程安全,但二者常被混用。可重入指:**同一函数/方法在未执行完时,能被同一线程再次调用且不破坏内部状态**;典型场景是信号处理、递归调用或回调嵌套。
lock会阻塞同一线程再次进入(除非用
RecursiveLock或
Monitor.TryEnter配超时),所以普通
lock块默认不可重入。
线程安全则关注**多线程并发访问共享资源时不出现数据竞争或状态不一致**。它不要求可重入——比如一个只读的静态缓存可能线程安全但无需支持重入。
在 C# 中,真正同时满足“可重入 + 线程安全”的常见做法是:用
Monitor手动控制重入计数,或改用
AsyncLocal<t></t>/ 不可变对象 / 纯函数式设计来规避共享状态。
用 Monitor
实现可重入锁(ReentrantLock)
C# 的
lock语句底层就是
Monitor.Enter/Exit,但它不暴露重入计数。要自己实现可重入行为,必须用
Monitor.TryEnter(obj, timeout)并手动维护线程 ID 与进入次数映射。
注意:
Monitor本身是可重入的(.NET 运行时保证同一线程多次
Enter不死锁),但你得确保每次
Exit次数匹配,否则其他线程永远等不到释放。
Monitor.Enter和
Monitor.Exit必须成对出现在同一个线程中;异常时需
try/finally保障
Exit不要跨线程调用
Monitor.Exit,会抛
SynchronizationLockException避免在锁内调用未知第三方代码(可能引发重入或死锁)
public class ReentrantLock
{
private readonly object _syncRoot = new object();
private Thread? _owner;
private int _entryCount;
<pre class='brush:php;toolbar:false;'>public void Enter()
{
var current = Thread.CurrentThread;
lock (_syncRoot)
{
if (_owner == current)
{
_entryCount++;
return;
}
Monitor.Enter(_syncRoot); // 等待获取锁
_owner = current;
_entryCount = 1;
}
}
public void Exit()
{
lock (_syncRoot)
{
if (Thread.CurrentThread != _owner)
throw new InvalidOperationException("Exit called from non-owner thread");
if (--_entryCount == 0)
{
_owner = null;
Monitor.Exit(_syncRoot);
}
}
}}
更轻量、更现代的替代方案:避免锁,用 AsyncLocal<t></t>
或不可变状态
如果你的“可重入”需求本质是想让递归调用或异步嵌套保持上下文隔离(比如日志追踪 ID、事务作用域),
AsyncLocal<t></t>是比手写可重入锁更安全、更符合 .NET 设计哲学的选择。
它自动随 async/await 流动,且每个逻辑执行路径拥有独立副本,天然线程安全、天然可重入——因为根本不共享状态。
AsyncLocal<t></t>不适用于跨线程同步共享数据,只用于“逻辑上下文传播” 若需共享可变状态,优先考虑
ImmutableArray<t></t>+
Interlocked更新引用,而非加锁 纯函数式风格(无副作用、输入决定输出)天然可重入且线程安全,适合配置解析、DTO 转换等场景
private static readonly AsyncLocal<string> _traceId = new AsyncLocal<string>();
<p>public void DoWork(string id)
{
_traceId.Value = id; // 当前逻辑流独享
NestedCall();
}</p><p>private void NestedCall()
{
Console.WriteLine($"Current trace: {_traceId.Value}"); // 自动继承,不污染其他分支
}容易踩的坑:别把 lock(this)
或 lock(typeof(T))
当可重入锁用
这些写法不仅不可重入,还极易引发死锁或意外锁竞争:
lock(this)暴露了实例锁,外部代码也能锁它,导致协作失控
lock(typeof(T))是全 AppDomain/Assembly 级别锁,多个类库可能无意中锁住同一 Type 对象
Monitor.Enter(obj)后没配对
Monitor.Exit(obj)—— 尤其在异常分支遗漏
finally,会导致永久挂起 在
async方法里用
lock:await 会切出线程,再回来时可能已不是原线程,
Monitor不允许跨线程 Exit
真正的难点不在“怎么写锁”,而在于判断“是否真的需要锁”。多数业务逻辑的可重入需求,其实源于状态管理混乱——把上下文塞进静态字段、单例属性,才被迫去解决线程安全问题。重构掉共享可变状态,往往比写一个完美的
ReentrantLock更有效。
