如何在使用TaskCompletionSource.SetException时保留await行为?

19

(这是对该问题的新尝试,现在更好地展示了该问题。)

假设我们有一个出现故障的任务 (var faultedTask = Task.Run(() => { throw new Exception("test"); });) 并且我们等待它。 await 将解包 AggregateException 并抛出底层异常。它将抛出 faultedTask.Exception.InnerExceptions.First()

根据 ThrowForNonSuccess 的源代码,它将通过执行任何存储的 ExceptionDispatchInfo 来执行此操作,可能是为了保留良好的堆栈跟踪。如果没有 ExceptionDispatchInfo,它将不会解包 AggregateException

仅此事实就让我感到惊讶,因为文档说明第一个异常总是被抛出:https://msdn.microsoft.com/en-us/library/hh156528.aspx?f=255&MSPPError=-2147217396 但事实证明,await 可能会抛出 AggregateException,这不是已记录的行为。

当我们想要创建代理任务并设置其异常时,这就成为一个问题:

var proxyTcs = new TaskCompletionSource<object>();
proxyTcs.SetException(faultedTask.Exception);
await proxyTcs.Task;

这会抛出 AggregateException 异常,而 await faultedTask; 会抛出测试异常。

我该如何创建一个代理任务,使其可以随意完成并镜像原始任务的异常行为?

原始行为是:

  1. await 将抛出第一个内部异常。
  2. 所有异常仍然可通过 Task.Exception.InnerExceptions 访问。(此问题的早期版本省略了此要求。)

以下是总结发现的测试:

[TestMethod]
public void ExceptionAwait()
{
    ExceptionAwaitAsync().Wait();
}

static async Task ExceptionAwaitAsync()
{
    //Task has multiple exceptions.
    var faultedTask = Task.WhenAll(Task.Run(() => { throw new Exception("test"); }), Task.Run(() => { throw new Exception("test"); }));

    try
    {
        await faultedTask;
        Assert.Fail();
    }
    catch (Exception ex)
    {
        Assert.IsTrue(ex.Message == "test"); //Works.
    }

    Assert.IsTrue(faultedTask.Exception.InnerExceptions.Count == 2); //Works.

    //Both attempts will fail. Uncomment attempt 1 to try the second one.
    await Attempt1(faultedTask);
    await Attempt2(faultedTask);
}

static async Task Attempt1(Task faultedTask)
{
    var proxyTcs = new TaskCompletionSource<object>();
    proxyTcs.SetException(faultedTask.Exception);

    try
    {
        await proxyTcs.Task;
        Assert.Fail();
    }
    catch (Exception ex)
    {
        Assert.IsTrue(ex.Message == "test"); //Fails.
    }
}

static async Task Attempt2(Task faultedTask)
{
    var proxyTcs = new TaskCompletionSource<object>();
    proxyTcs.SetException(faultedTask.Exception.InnerExceptions.First());

    try
    {
        await proxyTcs.Task;
        Assert.Fail();
    }
    catch (Exception ex)
    {
        Assert.IsTrue(ex.Message == "test"); //Works.
    }

    Assert.IsTrue(proxyTcs.Task.Exception.InnerExceptions.Count == 2); //Fails. Should preserve both exceptions.
}

这个问题的动机是我正在尝试构建一个函数,它将把一个任务的结果复制到 TaskCompletionSource 中。这是一个帮助函数,经常在编写任务组合函数时使用。重要的是API客户端不能检测到原始任务和代理任务之间的差异。


1
阅读来自50k+声望用户的新问题总是很有趣,90%的时间我会想:“哦,我也想知道这个”,最终收藏了这个问题。 - Scott Chamberlain
1
如果你想要关于异常的await语义,为什么不直接去源代码(http://referencesource.microsoft.com/#mscorlib/system/runtime/compilerservices/TaskAwaiter.cs,ca9850c71672bd54)并复制`TaskAwaiter`所做的呢? - Kirill Shlenskiy
1
@StephenCleary,我的代码现在也是按照这种精神建模的。同时,我还审查了Jon的公共代码。显然,除了我之外的每个人都知道如何使用正确的重载。用TPL代码获取每个细微差别可能是一门黑魔法。 - usr
1
@Noseratio,那也可以,我刚测试过了。不过我更喜欢更简洁的SetException方法。 - usr
2
@Noseratio,使用这两种方法都可以获得堆栈跟踪信息,但Unwrap方法中有一个无用的部分。 - usr
显示剩余5条评论
1个回答

10
``` await 可能会抛出 AggregateException 异常,但这并未记录在文档中。
不,当第一个嵌套异常是 AggregateException 时,这就是其行为。
基本上,当你调用 TaskCompletionSource.SetException(Exception) 时,它会将该异常包装在 AggregateException 中。
如果你想保留多个异常,只需使用接受 IEnumerable<Exception> 参数的 SetException 重载即可: ```
proxyTcs.SetException(faultedTask.Exception.InnerExceptions);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接