为什么自定义异步方法构建器仅作为类或Debug模式下正常运行?
问题根因
你遇到的问题本质是值类型(struct)的拷贝语义和C#异步状态机的优化逻辑共同作用的结果:
- 你直接把
_exception、_hasResult、_lock、_result、_source这些可变状态存在了struct类型的构建器本身的字段里。而C#编译器在Release模式优化异步状态机的过程中,会在装箱、状态机上下文传递时对struct类型的构建器执行值拷贝,导致你后续的SetResult/SetException操作是在拷贝后的新struct实例上执行的,和之前访问Task属性的struct实例是完全独立的两个对象,各自的字段状态完全不互通,自然就出现了修改不可见、SpinLock不生效的竞态问题。 - Debug模式下编译器会关闭异步状态机的相关优化,所有操作都指向同一个struct实例,所以不会出现该问题;你把构建器改成class(引用类型)后,拷贝的只是对象引用,所有操作都指向同一个实例,也不会有问题。
- 微软官方的
AsyncTaskMethodBuilder<TResult>虽然也是struct,但它本身不存储可变状态,所有的结果、异常、完成状态都存在内部持有的引用类型Task<TResult>实例上,struct就算被拷贝,内部的引用还是指向同一个Task对象,所以不会出现状态不共享的问题。
修复方案
有两种可行的修复方式:
- 直接将
CustomAwaitableAsyncMethodBuilder<T>定义为class,你已经验证过该方案可以正常运行。 - 保持构建器为struct,将所有可变状态封装到一个独立的引用类型中,构建器仅持有该引用类型的实例:
// 封装所有可变状态的引用类型 internal sealed class BuilderState<T> { public Exception? Exception; public bool HasResult; public SpinLock Lock = new SpinLock(Debugger.IsAttached); public T? Result; public TaskCompletionSource<T>? Source; } public struct CustomAwaitableAsyncMethodBuilder<T> { private readonly BuilderState<T> _state = new BuilderState<T>(); public CustomAwaitable<T> Task { get { var lockTaken = false; try { _state.Lock.Enter(ref lockTaken); if (_state.Exception is not null) return new CustomAwaitable<T>(ValueTask.FromException<T>(_state.Exception)); if (_state.HasResult) return new CustomAwaitable<T>(ValueTask.FromResult(_state.Result!)); return new CustomAwaitable<T>( new ValueTask<T>( (_state.Source ??= new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously)) .Task ) ); } finally { if (lockTaken) _state.Lock.Exit(); } } } public void AwaitOnCompleted<TAwaiter, TStateMachine>( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine => awaiter.OnCompleted(stateMachine.MoveNext); public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion where TStateMachine : IAsyncStateMachine => awaiter.UnsafeOnCompleted(stateMachine.MoveNext); public static CustomAwaitableAsyncMethodBuilder<T> Create() => new(); public void SetException(Exception exception) { var lockTaken = false; try { _state.Lock.Enter(ref lockTaken); if (Volatile.Read(ref _state.Source) is {} source) { source.TrySetException(exception); } else { _state.Exception = exception; } } finally { if (lockTaken) _state.Lock.Exit(); } } public void SetResult(T result) { var lockTaken = false; try { _state.Lock.Enter(ref lockTaken); if (Volatile.Read(ref _state.Source) is {} source) { source.TrySetResult(result); } else { _state.Result = result; _state.HasResult = true; } } finally { if (lockTaken) _state.Lock.Exit(); } } public void SetStateMachine(IAsyncStateMachine stateMachine) {} public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine => stateMachine.MoveNext(); }
这样就算struct构建器被拷贝,所有副本持有的都是同一个BuilderState<T>实例,状态完全共享,就不会出现竞态和状态不一致的问题。
内容的提问来源于stack exchange,提问作者Matt Thomas
相关产品推荐
相关产品推荐

