如何使用 TaskCompletionSource.SetException 保留等待行为?

How to preserve await behavior with TaskCompletionSource.SetException?

(这是对这个问题的新尝试,现在更好地证明了这个问题。)

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

根据 ThrowForNonSuccess 的源代码,它将通过执行任何存储的 ExceptionDispatchInfo 来执行此操作,大概是为了保留良好的堆栈跟踪。如果没有 ExceptionDispatchInfo.

,它不会解压 AggregateException

这个事实本身就让我感到惊讶,因为文档指出第一个异常是 always 抛出: 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;

这会抛出 AggregateExceptionawait 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 客户端无法检测到原始任务和代理任务之间的差异。

It turns out that await can throw AggregateException, though, which is not documented behavior.

不,这是第一个嵌套异常 AggregateException.

时的行为

基本上,当您调用 TaskCompletionSource.SetException(Exception) 时,它会将异常包装在 AggregateException.

如果你想保留 多个 异常,只需使用 SetException 的重载,它接受 IEnumerable<Exception>:

proxyTcs.SetException(faultedTask.Exception.InnerExceptions);