使用CancelAfter时如何避免任务取消后调用SetResult报错
问题确认
你猜测的竞态条件确实存在:IsCanceled的读取和SetResult的调用不是原子操作,两者的执行间隙完全可能触发超时逻辑执行SetCanceled,导致SetResult抛出InvalidOperationException。
关于TaskCompletionSource的线程安全
文档中说的线程安全指的是TaskCompletionSource<T>的所有公开方法都不会出现内部状态损坏,但Set*系列方法(SetResult/SetCanceled/SetException)的语义是「必须成功设置状态」,如果TCS已经进入终态(已完成/已取消/已出错)就会直接抛异常。
而Try*系列方法(TrySetResult/TrySetCanceled/TrySetException)就是你要的原子操作版本:它会原子检查TCS当前状态,只有未进入终态时才会修改状态并返回true,否则直接返回false,不会抛出任何异常。
最优解决方案
直接把所有Set*调用改为Try*即可,完全不需要外部的if(!req.IsCanceled)判断,修改后的代码如下:
class Request<T> { // 注意原简化代码漏了初始化,实际使用需要创建实例 TaskCompletionSource<T> tcs = new TaskCompletionSource<T>(); CancellationTokenSource cts = new CancellationTokenSource(); public int Timeout { get; } public bool IsCanceled => tcs.Task.IsCanceled; public Request(int timeout = System.Threading.Timeout.Infinite) => Timeout = timeout; public Task<T> StartRequestAsync() { // 取消回调也改用TrySetCanceled,避免和OnEvent竞争时抛异常 cts.Token.Register(() => tcs.TrySetCanceled()); cts.CancelAfter(Timeout); return tcs.Task; } public void OnEvent(T result) { // 原子设置结果,成功后主动停止超时,释放不必要的资源 if (tcs.TrySetResult(result)) { cts.CancelAfter(System.Threading.Timeout.Infinite); } } }
外部调用直接简化为:
var req = new Request<int>(); req.OnEvent(5);
其他可选方案
也可以通过加锁把IsCanceled判断和SetResult包成原子操作,但这种方案完全没必要:Try*系列方法底层已经做了原子同步,性能比手动加锁更好,代码也更简洁,没有额外的维护成本。
内容的提问来源于stack exchange,提问作者Sorashi
相关产品推荐
相关产品推荐

