没有从异步委托提供的故障 CountinueWith 中捕获异常

No exceptions caught from faulted CountinueWith supplied with async delegate

以下程序我想捕获我认为在访问时应该发生的异常 t.Result

   class Program
{
    static void Main(string[] args)
    {
        Task.Run(async () => await Test()).Wait();
        Console.ReadKey();
    }

    private static async Task Test()
    {
        var t1 = Task.Run<int>(() => { throw new Exception("1"); return 1; });
        var tasks = new[]
            {
                //t1.ContinueWith(t => {
                //    var y = t.Result + 1;
                //    }),
                //t1.ContinueWith(t => {
                //    var y = t.Result + 1;
                //    })
                t1.ContinueWith(async t =>
                {
                    var y = t.Result + 1;
                    await Task.Delay(100);
                }),
                t1.ContinueWith(async t =>
                {
                    var y = t.Result + 1;
                    await Task.Delay(100);
                }),
            };
        try
        {
            await Task.WhenAll(tasks);
        }
        catch (Exception ex)
        {
            Console.WriteLine("Exception caught");
        }
    }
}    

但是如果我使用异步委托,则不会捕获异常。两个任务都出错,但它们的异常为空。

问题

  1. 有什么窍门,我在哪里可以阅读更多详细信息?
  2. 如果我以某种方式失去了等待,我可以确定在 Task.WhenAll returns 之后继续完成吗?

造成混淆的原因是 Task.RunTask.Factory.StartNewTask.ContinueWith 的预期用法不同。

Task.Run,通过其重载,可以接受同步委托(ActionFunc<TResult>)或异步委托(Func<Task>Func<Task<TResult>>).如果指定了异步委托,Task.Run 负责解包表示异步操作的内部任务,并返回 that 作为结果。

Task.ContinueWith, on the other hand, does not recognize asynchronous delegates. If you pass a Func<Task, Task> delegate to ContinueWith, it will simply wrap the asynchronous operation within its own outer task, returning Task<Task> as the result. (Same applies to TaskFactory.StartNew.) 因此,您的 Task.WhenAll 只会等待外部任务,而不是异步操作。这不是你想要的。

要展开内部异步操作,您只需调用 Unwrap:

t1.ContinueWith(async t =>
{
    var y = t.Result + 1;
    await Task.Delay(100);
}).Unwrap(),

有关此问题的讨论,请参阅 Task.Run vs Task.Factory.StartNew. (The handling of asynchronous delegates in ContinueWith is similar to StartNew.) For the rationale behind unwrapping, see How to: Unwrap a Nested Task

此外,我建议您避免混合使用 awaitContinueWith。在您的情况下,您可以在异步委托中等待 t1 的结果,而不是向其注册延续。您将需要一个 self-invoking 匿名函数以单一方法执行此操作……

((Func<Task>)(async () => 
{
    var y = await t1 + 1;
    await Task.Delay(100);
}))(),

…但是如果你把它拆分成一个单独的方法,阅读起来会更清楚:

private static async Task Test()
{
    var t1 = Task.Run<int>(() => { throw new Exception("1"); return 1; });
    var tasks = new[]
    {
        IncrementAndDelayAsync(t1),
        IncrementAndDelayAsync(t1),
    };
    // ...
}

private static async Task IncrementAndDelayAsync(Task<int> t1)
{
    var y = await t1 + 1;
    await Task.Delay(100);
}