为什么不抛出此异常
本文关键字:异常 为什么不 | 更新日期: 2023-09-27 18:04:12
我有时会使用一组任务,为了确保它们都在等待中,我使用这种方法:
public async Task ReleaseAsync(params Task[] TaskArray)
{
var tasks = new HashSet<Task>(TaskArray);
while (tasks.Any()) tasks.Remove(await Task.WhenAny(tasks));
}
然后像这样称呼它:
await ReleaseAsync(task1, task2, task3);
//or
await ReleaseAsync(tasks.ToArray());
但是,最近我注意到一些奇怪的行为,并设置查看ReleaseAsync方法是否存在问题。我设法将其缩小到这个简单的演示,如果您包含 linqpad System.Threading.Tasks
,它会在 linqpad 中运行。它还可以在控制台应用程序或 asp.net mvc 控制器中稍作修改。
async void Main()
{
Task[] TaskArray = new Task[]{run()};
var tasks = new HashSet<Task>(TaskArray);
while (tasks.Any<Task>()) tasks.Remove(await Task.WhenAny(tasks));
}
public async Task<int> run()
{
return await Task.Run(() => {
Console.WriteLine("started");
throw new Exception("broke");
Console.WriteLine("complete");
return 5;
});
}
我不明白的是为什么异常从未出现在任何地方。我会认为,如果等待异常任务,它会抛出。我能够通过将 while 循环替换为每个简单的循环来确认这一点,如下所示:
foreach( var task in TaskArray )
{
await task;//this will throw the exception properly
}
我的问题是,为什么显示的示例没有正确抛出异常(它永远不会出现在任何地方(。
TL;DR:run()
抛出异常,但您正在等待WhenAny()
,它本身不会抛出异常。
WhenAny
的 MSDN 文档指出:
当提供的任何任务完成时,返回的任务将完成。返回的任务将始终以 RanToComplete(RanToComplete(状态结束,其结果设置为要完成的第一个任务。即使要完成的第一个任务以">已取消"或"故障"状态结束,也是如此。
本质上,正在发生的事情是WhenAny
返回的任务只是吞下错误的任务。它只关心任务已完成的事实,而不是它已经成功完成的事实。当您等待任务时,它只会顺利完成,因为出错的是内部任务,而不是您正在等待的任务。
未awaited
或未使用其Wait()
或Result()
方法的Task
将吞噬异常。此行为可以修改回在 .NET 4.0 中执行的方式,方法是在 GC Task
后使正在运行的进程崩溃。您可以在app.config
中按如下方式设置它:
<configuration>
<runtime>
<ThrowUnobservedTaskExceptions enabled="true"/>
</runtime>
</configuration>
并行编程团队在Microsoft的这篇博文中引用了一句话:
熟悉 .NET 4 中任务的人应该知道 TPL 具有"未观察到的"异常的概念。 这是 TPL 中两个相互竞争的设计目标之间的折衷:支持将异步操作中未经处理的异常封送到使用其完成/输出的代码,并对应用程序代码未处理的异常遵循标准 .NET 异常升级策略。 自 .NET 2.0 以来,在新创建的线程、ThreadPool 工作项等中未处理的异常都会导致默认的异常升级行为,从而导致进程崩溃。 这通常是可取的,因为异常表明出了问题,崩溃有助于开发人员立即识别应用程序已进入不可靠状态。 理想情况下,任务将遵循相同的行为。 但是,任务用于表示代码稍后联接的异步操作,如果这些异步操作引起异常,则应将这些异常封送到联接代码运行的位置并使用异步操作的结果。 这本质上意味着 TPL 需要支持这些异常并保留它们,直到在使用代码访问任务时可以再次抛出它们为止。 由于这会阻止默认升级策略,因此 .NET 4 应用了"未观察到的"异常的概念来补充"未处理"异常的概念。 "未观察到的"异常是存储在任务中但使用代码从未以任何方式查看的异常。 有许多方法可以观察异常,包括对任务进行 Wait((、访问任务的结果、查看任务的异常属性等。 如果代码从未观察到任务的异常,那么当任务消失时,将引发 TaskScheduler.UnobservedTaskException,从而为应用程序提供更多"观察"异常的机会。 如果异常仍未观察到,则异常升级策略将由终结器线程上未处理的异常启用。
从评论:
这些[任务]与托管资源相关联,我想在以下情况下释放它们 它们变得可用,而不是等待所有完成 然后释放。
使用帮助程序async void
方法可能会为您提供所需的行为,以便从列表中删除已完成的任务并立即引发未观察到的异常:
public static class TaskExt
{
public static async void Observe<TResult>(Task<TResult> task)
{
await task;
}
public static async Task<TResult> WithObservation(Task<TResult> task)
{
try
{
return await task;
}
catch (Exception ex)
{
// Handle ex
// ...
// Or, observe and re-throw
task.Observe(); // do this if you want to throw immediately
throw;
}
}
}
然后,您的代码可能如下所示(未经测试(:
async void Main()
{
Task[] TaskArray = new Task[] { run().WithObservation() };
var tasks = new HashSet<Task>(TaskArray);
while (tasks.Any<Task>()) tasks.Remove(await Task.WhenAny(tasks));
}
.Observe()
将立即"带外"重新引发任务的异常,如果调用线程具有同步上下文,则使用 SynchronizationContext.Post
,否则使用 ThreadPool.QueueUserWorkItem
。您可以使用 AppDomain.CurrentDomain.UnhandledException
处理此类"带外"异常。
我在这里更详细地描述了这一点:
TAP 全局异常处理程序