并发问题:通过检测ExecutionContext分叉识别C#异步不安全代码
基于AsyncLocal的非线程安全对象fail-fast校验方案
核心逻辑
你需要的异步上下文分叉检测本质是状态对象的单异步分支所有权校验,不需要完整追踪异步调用栈树,只要绑定合法访问上下文即可实现,成本极低且可靠性远高于时间窗口重叠检测。
具体实现
1. 基础跨上下文访问检测
基于AsyncLocal<T>实现异步上下文唯一标记,状态对象首次访问时绑定当前上下文标记,后续所有访问校验标记一致性,不一致直接抛出异常:
public class ThreadUnsafeStateObject { // 存储当前拥有访问权限的异步上下文标记 private Guid? _ownedContextId; #if DEBUG // Debug模式下存储访问栈,方便定位问题 private string _lastAccessStack; #endif // 异步上下文唯一标记存储,继承AsyncLocal的流传播特性 private static readonly AsyncLocal<Guid> _executionContextId = new(); private static Guid CurrentContextId { get { if (_executionContextId.Value == Guid.Empty) { _executionContextId.Value = Guid.NewGuid(); } return _executionContextId.Value; } } // 所有对象方法入口统一调用该校验 private void ValidateAccess() { var currentId = CurrentContextId; // 首次访问自动绑定当前上下文 if (!_ownedContextId.HasValue) { _ownedContextId = currentId; #if DEBUG _lastAccessStack = Environment.StackTrace; #endif return; } // 上下文不匹配直接fail-fast if (_ownedContextId.Value != currentId) { #if DEBUG var errorMsg = $"跨异步上下文访问非线程安全对象。上一次合法访问栈:\n{_lastAccessStack}\n本次访问栈:\n{Environment.StackTrace}"; #else var errorMsg = "检测到跨异步上下文访问非线程安全状态对象,该操作会破坏状态一致性。"; #endif throw new InvalidOperationException(errorMsg); } #if DEBUG _lastAccessStack = Environment.StackTrace; #endif } /// <summary> /// 显式释放上下文所有权,允许对象转移到其他异步上下文使用 /// </summary> public void ReleaseOwnership() { ValidateAccess(); _ownedContextId = null; } // 以下为原有对象业务方法,所有方法开头调用ValidateAccess public int GetStateValue() { ValidateAccess(); // 原有业务逻辑 } public void SetStateValue(int value) { ValidateAccess(); // 原有业务逻辑 } }
2. 读写交替的事务场景防护
针对你提到的「读-耗时计算-写」交替执行的状态破坏问题,新增独占会话机制,要求连续状态操作必须显式开启会话,会话期间拒绝所有其他访问:
private bool _isInExclusiveSession; /// <summary> /// 开启独占会话,会话期间仅允许当前上下文访问,用于保护读-改-写类连续操作 /// </summary> public IDisposable OpenExclusiveSession() { ValidateAccess(); if (_isInExclusiveSession) { throw new InvalidOperationException("对象已处于独占会话中,不可重复开启。"); } _isInExclusiveSession = true; return new SessionDisposable(() => _isInExclusiveSession = false); } // 会话释放辅助类 private sealed class SessionDisposable : IDisposable { private readonly Action _releaseAction; public SessionDisposable(Action releaseAction) => _releaseAction = releaseAction; public void Dispose() => _releaseAction?.Invoke(); }
同时需要在ValidateAccess方法中增加会话校验逻辑:
private void ValidateAccess() { // ...原有其他校验逻辑 if (_isInExclusiveSession) { throw new InvalidOperationException("对象处于独占会话中,不可访问。"); } // ...原有后续逻辑 }
开发人员使用时只需包裹连续操作即可:
using (stateObj.OpenExclusiveSession()) { var currentVal = stateObj.GetStateValue(); // 任意耗时计算逻辑 stateObj.SetStateValue(currentVal + 1); }
3. 异步分叉场景适配
当代码中存在Task.WhenAll、Task.Run等异步分叉操作时,需要为每个并行子任务生成独立的上下文标记,避免并行子任务继承父上下文标记导致校验失效,可通过通用隔离上下文方法实现:
public static class AsyncContextIsolator { private static readonly AsyncLocal<Guid> _executionContextId = new(); public static async Task RunIsolated(Func<Task> func) { var originalId = _executionContextId.Value; _executionContextId.Value = Guid.NewGuid(); try { await func(); } finally { _executionContextId.Value = originalId; } } // 重载带返回值的版本 public static async Task<T> RunIsolated<T>(Func<Task<T>> func) { var originalId = _executionContextId.Value; _executionContextId.Value = Guid.NewGuid(); try { return await func(); } finally { _executionContextId.Value = originalId; } } }
并行调用时使用方式如下:
// 两个并行任务会获得不同的上下文标记,访问同一个状态对象时会触发校验失败 await Task.WhenAll( AsyncContextIsolator.RunIsolated(() => DoSomethingAsync(stateObj)), AsyncContextIsolator.RunIsolated(() => DoOtherThingAsync(stateObj)) );
如果团队有代码规范要求,可搭配Roslyn分析器检测未包裹隔离上下文的并行调用,直接在编码阶段给出警告。
内容的提问来源于stack exchange,提问作者Eamon Nerbonne
相关产品推荐
相关产品推荐

