.NET TPL疑问:为何非异步委托执行效果类似阻塞Wait调用?
先明确几个核心底层规则,再对应你的四个场景逐一解释:
核心规则
- Task异常传播逻辑:Task完成时若携带异常,仅当该Task被
await/Wait()/Result等待,或主动访问其Exception属性时,异常才会被抛出。未被观察的异常(既不等待也不访问)在.NET Core+中会被静默处理,.NET Framework中会触发全局TaskScheduler.UnobservedTaskException事件,但不会在当前代码路径抛出。 - Task.Run重载差异:
- 传入
Action(同步无返回委托):Task.Run会创建独立Task执行同步代码,同步代码的异常会被包装到返回的Task中;若委托内部启动子Task(如Task.Delay)却不等待,子Task的异常与Task.Run返回的Task无关,属于未观察异常。 - 传入
Func<Task>(异步委托,无论是否带async):Task.Run会直接返回委托内部Task的“解包”版本,外部等待Task.Run的结果等同于等待内部Task的结果,异常会直接传播。
- 传入
- Wait()的阻塞特性:
Task.Wait()是同步阻塞调用,Task完成后若有异常,会立即抛出包含原始异常的AggregateException。 - async委托的异常处理:async方法内部的异常会被自动捕获并包装到返回的Task中,不会同步抛出。
场景对应解析
传入非异步委托(内部执行Task.Delay)并await时捕获取消异常,效果如Wait
这里的“非异步委托”实际是Func<Task>类型(比如() => Task.Delay(1000, cts.Token)),而非Action。若为Action,内部Task.Delay是“启动即遗忘”,Task.Run返回的Task会立即完成,不会携带异常。当传入Func<Task>时,Task.Run直接返回Task.Delay的Task,await该Task等同于直接await Task.Delay,取消异常会正常抛出——和Task.Delay(...).Wait()的区别仅在于前者是异步抛出,后者是同步阻塞抛出,但最终都会触发异常捕获。直接执行未等待的Task.Delay时,不会捕获异常
直接调用Task.Delay(...)仅启动延迟任务,既不等待也不观察其状态。该Task的取消异常属于未观察异常,在.NET Core+中会被静默处理,不会在当前代码路径的try/catch中触发;即使在.NET Framework中,也只会触发全局事件,不会直接在当前逻辑中抛出。给Task.Delay添加Wait调用时,会捕获异常
Task.Delay(...).Wait()是同步阻塞等待,当Task.Delay因取消进入Canceled状态时,Wait()会立即将取消异常包装为AggregateException抛出,因此会被外部的try/catch捕获到。给Task.Run的委托添加async关键字后,不再捕获异常
这种情况通常是因为你在async委托内部未等待Task.Delay,比如代码是async () => { Task.Delay(1000, cts.Token); }。此时async方法会立即返回已完成的Task,而内部的Task.Delay是“启动即遗忘”,其异常属于未观察异常,不会传播到Task.Run返回的Task中。外部await Task.Run等待的是async方法返回的已完成Task,自然不会捕获到Task.Delay的取消异常。若你在async委托内部加上await Task.Delay(...),取消异常仍会正常传播到外部,被try/catch捕获。
内容的提问来源于stack exchange,提问作者Stanislau Viaroukin

