我有时会使用一组任务,为了确保他们都在等待我使用这种方法:
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方法是否存在问题。我设法将其缩小到这个简单的演示,如果你包含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
}
我的问题是,为什么显示的示例没有正确地抛出异常(它从不出现在任何地方)。
答案 0 :(得分:10)
Task
不是awaited
或未使用Wait()
或Result()
方法,默认情况下会吞下该例外。在Task
是GC后,崩溃正在运行的流程可以将此行为修改回.NET 4.0中的行为方式。您可以在app.config
中进行设置,如下所示:
<configuration>
<runtime>
<ThrowUnobservedTaskExceptions enabled="true"/>
</runtime>
</configuration>
来自Microsoft并行编程团队的this博客文章引用:
熟悉.NET 4中的任务的人都会知道TPL具有“未观察到的”异常的概念。这是TPL中两个相互竞争的设计目标之间的折衷:支持将未处理的异常从异步操作封送到消耗其完成/输出的代码,并遵循标准的.NET异常升级策略来处理应用程序代码未处理的异常。从.NET 2.0开始,在新创建的线程,ThreadPool工作项等中未处理的异常都会导致默认的异常升级行为,这会导致进程崩溃。这通常是可取的,因为异常表明出现了问题,并且崩溃有助于开发人员立即识别应用程序已进入不可靠状态。理想情况下,任务将遵循相同的行为。但是,任务用于表示稍后加入的异步操作,如果这些异步操作产生异常,则应将这些异常封送到加入代码的运行位置并消耗异步操作的结果。这固有地意味着TPL需要支持这些异常并保持它们,直到消耗代码访问任务时它们可以被再次抛出。由于这会阻止默认升级策略,因此.NET 4应用了“未观察到的”异常的概念来补充“未处理”异常的概念。 “未观察到的”异常是存储在任务中但从未以消费代码以任何方式查看的异常。有很多方法可以观察异常,包括Wait()on the Task,访问Task的Result,查看Task的Exception属性,等等。如果代码从未观察到Task的异常,那么当Task消失时,会引发TaskScheduler.UnobservedTaskException,为应用程序提供一个“观察”异常的机会。如果异常仍未被观察到,则异常升级策略将由终结器线程上未处理的异常启用。
答案 1 :(得分:10)
TL; DR :run()
会抛出异常,但您正在等待WhenAny()
,它本身不会引发异常。
WhenAny
州的MSDN文档:
当任何提供的任务完成时,返回的任务将完成。返回的任务将始终以 RanToCompletion 状态结束,并将其Result设置为要完成的第一个任务。即使完成的第一项任务以已取消或故障状态结束,也是如此。
基本上发生的事情是WhenAny
返回的任务只是吞下了故障任务。它只关心任务完成的事实,而不是它已成功完成。当你等待任务时,它只是完成而没有错误,因为它是内部任务出错,而不是你正在等待的任务。
答案 2 :(得分:4)
来自评论:
这些[任务]与托管资源相关联,我想在何时发布它们 它们变得可用而不是等待所有它们完成 然后释放。
使用帮助器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
)处理此类“带外”异常。
我在这里详细描述了这个: