我希望等待抛出AggregateException,而不仅仅是第一个Exception
本文关键字:不仅仅是 第一个 Exception AggregateException 等待 我希望 | 更新日期: 2023-09-27 18:28:16
在等待出现故障的任务(已设置异常的任务)时,await
将重新引发存储的异常。如果存储的异常是AggregateException
,它将重新抛出第一个并丢弃其余的。
我们如何使用await
,同时抛出原始的AggregateException
,这样我们就不会意外丢失错误信息?
请注意,当然可以为此想出一些巧妙的解决方案(例如,尝试在await
周围捕获,然后调用Task.Wait
)。我真的希望找到一个干净的解决方案这里的最佳做法是什么
我想过使用一个自定义的awaiter,但内置的TaskAwaiter
包含了很多魔法,我不知道如何完全复制。它在TPL类型上调用内部API。我也不想复制所有这些。
如果你想玩的话,这里有一个简短的演示:
static void Main()
{
Run().Wait();
}
static async Task Run()
{
Task[] tasks = new[] { CreateTask("ex1"), CreateTask("ex2") };
await Task.WhenAll(tasks);
}
static Task CreateTask(string message)
{
return Task.Factory.StartNew(() => { throw new Exception(message); });
}
在Run
中只抛出两个异常中的一个。
请注意,关于堆栈溢出的其他问题并没有解决这个特定的问题。建议重复时请小心。
我不同意你的问题标题中暗示await
的行为是不受欢迎的。这在绝大多数情况下都是有意义的。在WhenAll
的情况下,您真正需要了解所有错误详细信息的频率是多少,而不是只有一个?
AggregateException
的主要困难是异常处理,即您失去了捕获特定类型的能力。
也就是说,你可以通过一个扩展方法得到你想要的行为:
public static async Task WithAggregateException(this Task source)
{
try
{
await source.ConfigureAwait(false);
}
catch
{
// source.Exception may be null if the task was canceled.
if (source.Exception == null)
throw;
// EDI preserves the original exception's stack trace, if any.
ExceptionDispatchInfo.Capture(source.Exception).Throw();
}
}
我知道我迟到了,但我发现了这个巧妙的小技巧,可以随心所欲。由于完整的异常集可用于等待中的Task,因此调用此Task的Wait或.Result将引发聚合异常。
static void Main(string[] args)
{
var task = Run();
task.Wait();
}
public static async Task Run()
{
Task[] tasks = new[] { CreateTask("ex1"), CreateTask("ex2") };
var compositeTask = Task.WhenAll(tasks);
try
{
await compositeTask.ContinueWith((antecedant) => { }, TaskContinuationOptions.ExecuteSynchronously);
compositeTask.Wait();
}
catch (AggregateException aex)
{
foreach (var ex in aex.InnerExceptions)
{
Console.WriteLine(ex.Message);
}
}
}
static Task CreateTask(string message)
{
return Task.Factory.StartNew(() => { throw new Exception(message); });
}
以下是Stephen Cleary的WithAggregateException
扩展方法的一个较短实现:
public static async Task WithAggregateException(this Task source)
{
try { await source.ConfigureAwait(false); }
catch when (source.IsCanceled) { throw; }
catch { source.Wait(); }
}
public static async Task<T> WithAggregateException<T>(this Task<T> source)
{
try { return await source.ConfigureAwait(false); }
catch when (source.IsCanceled) { throw; }
catch { return source.Result; }
}
这种方法基于Stephen Toub在GitHub的API提案中的建议。
更新:我添加了对取消情况的特殊处理,以防止传播包含OperationCanceledException
的AggregateException
时的尴尬。现在直接传播OperationCanceledException
,并保留Task.IsCanceled
状态。感谢@nosegratio在这个答案的评论中指出了这个缺陷。当然,现在这个实现并不比Stephen Cleary的方法短多少!
异常处理(任务并行库)
我可以说更多,但这只是填充。玩它,它确实像他们说的那样有效。你只需要小心。
也许你想要这个
God(Jon Skeet)解释等待异常处理
(就我个人而言,我不愿等待,但这只是我的偏好)
回复评论(评论回复太长)
然后使用线程作为类似论点的起点,作为最佳实践,这里将有线程的来源。
除非您实现代码来传递异常,否则异常很容易被吞噬(例如,等待正在预先包装的异步模式……当您引发事件时,将它们添加到事件args对象中)。当您有一个场景,启动任意数量的线程并在它们上执行时,您无法控制顺序或终止每个线程的点。此外,如果一个错误与另一个错误相关,则永远不会使用此模式。因此,你强烈暗示其余部分的执行是完全独立的——IE你强烈暗示这些线程上的异常已经作为异常处理。如果你想做一些事情,而不是在这些线程中处理它们所在的线程中的异常(这很奇怪),你应该将它们添加到一个通过引用传递的锁定集合中——你不再将异常视为异常,而是将其视为一条信息——使用并发包,将异常包装在您需要识别其来源的上下文的信息中,该上下文将被传递给它。
不要混淆你的用例。
我不想放弃实践,只捕捉我期望的异常。这让我想到了以下扩展方法:
public static async Task NoSwallow<TException>(this Task task) where TException : Exception {
try {
await task;
} catch (TException) {
var unexpectedEx = task.Exception
.Flatten()
.InnerExceptions
.FirstOrDefault(ex => !(ex is TException));
if (unexpectedEx != null) {
throw new NotImplementedException(null, unexpectedEx);
} else {
throw task.Exception;
}
}
}
消费代码可能是这样的:
try {
await Task.WhenAll(tasks).NoSwallow<MyException>();
catch (AggregateException ex) {
HandleExceptions(ex);
}
骨头头异常将具有与同步世界中相同的效果,即使它与MyException
同时抛出也是偶然的。NotImplementedException
的包装有助于不松动原始堆栈轨迹。
封装原始聚合异常并且不更改返回类型的扩展,因此它仍然可以与Task<T>
一起使用
public static Task<T> UnswallowExceptions<T>(this Task<T> t)
=> t.ContinueWith(t => t.IsFaulted ? throw new AggregateException("whatever", t.Exception) : t.Result);
示例:
Task<T[]> RunTasks(Task<T>[] tasks) =>
Task.WhenAll(CreateSometasks()).UnswallowExceptions();
try
{ var result = await CreateTasks(); }
catch(AggregateException ex) { } //ex is original aggregation exception here
注意如果任务被取消,此方法将抛出,如果取消对您的很重要,则使用另一种方法