C#中理解async/await与Wait的区别及其"ContinueWith"行为

9

一种方法是标准的异步方法,就像这个:

private static async Task AutoRetryHandlerAsync_Worker(Func<Task<bool>> taskToRun,...)

我已经测试了两个实现方式,一个使用 await,另一个使用 .Wait()
这两种实现方式完全不同,因为相同的测试在 await 版本中失败,但在 Wait() 版本中没有失败。
该方法的目标是“执行由输入函数返回的任务,并通过执行相同的函数重试直到成功”(在达到一定尝试次数后自动停止限制)。
以下代码可以运行:
private static async Task AutoRetryHandlerAsync_Worker(Func<Task<bool>> taskToRun,...)
{
    try {
       await taskToRun();
    }
    catch(Exception) 
   {
       // Execute later, and wait the result to complete
       await Task.Delay(currentDelayMs).ContinueWith(t =>
       {
            // Wait for the recursive call to complete
            AutoRetryHandlerAsync_Worker(taskToRun).Wait();
       });

       // Stop
       return;
    }    
}

使用 async t => 以及使用 await 而不是 t =>.Wait() 的方式是行不通的,因为在最后执行 return; 前,递归调用的结果没有被等待:

private static async Task AutoRetryHandlerAsync_Worker(Func<Task<bool>> taskToRun,...)
{
    try {
       await taskToRun();
    }
    catch(Exception) 
   {
       // Execute later, and wait the result to complete
       await Task.Delay(currentDelayMs).ContinueWith(async t =>
       {
            // Wait for the recursive call to complete
            await AutoRetryHandlerAsync_Worker(taskToRun);
       });

       // Stop
       return;
    }    
}

我试图理解为什么这个简单的变化会改变一切,当它应该做相同的事情:等待ContinueWith完成。

如果我提取由ContinueWith方法运行的任务,我会看到ContinueWith函数的状态在内部等待返回之前就传递到了" ranToCompletion "。

为什么?难道它不应该被等待吗?


具体可测试的行为

public static void Main(string[] args)
{
    long o = 0;
    Task.Run(async () =>
    {
        // #1 await
        await Task.Delay(1000).ContinueWith(async t =>
        {
            // #2 await
            await Task.Delay(1000).ContinueWith(t2 => {
                o = 10;
            });
        });
        var hello = o;
    });


    Task.Delay(10000).Wait();
}

为什么在 o=10 之前就已经执行了 var hello = o;

#1 await 不是应该等待执行才能继续吗?


1
不,它并不会暂停线程。它将控制权交回给调用者。这些机制的目的是让线程执行有用的工作,而不是闲置等待其他线程/IO执行工作。 - Damien_The_Unbeliever
8
ContinueWith 方法无法处理“异步 Lambda”并且不会等待 Lambda 返回的任务完成,而是仅将该任务通过 ContinueWith 返回,并让其他人来 await。如果要处理此类情况,则需要使用 await await - Lasse V. Karlsen
1
@LasseV.Karlsen 给出完整的答案,评论很少被完全阅读。 - BRAHIM Kamel
1
虽然这不是对问题的回答,但你可以使用简单的 while 循环来实现更简单、更高效的代码,而不是手动使用 continuation 和递归。代码如下:while (true) { try { await taskToRun(); return; } catch (Exception) { await Task.Delay(currentDelayMs); } } - Matthias247
1
请注意,.Wait()方法会在线程池上阻塞一个线程(Task.Delay()在这里结束),直到taskToRun()完成。根据并发级别和taskToRun的持续时间,这可能不是理想的解决方案。 - Matthias247
显示剩余11条评论
1个回答

6
Lambda表达式语法会让你忽略了你在 ContinueWith(async void ...) 中。
async void 方法不会被等待,它们抛出的任何错误都将无法被观察到。
对于你的基本问题,从 catch 中重试本来就不是一种推荐的做法。太多事情要处理了,catch 块应该简单明了。而且,对于所有异常类型进行重试也是非常可疑的。你应该知道哪些错误可能需要重试,让其余的通过。
追求简单和易读:
while (count++ < N)
{
   try
   {          
      MainAction();
      break;      
   }
   catch(MoreSpecificException ex) { /* Log or Ignore */ }

   Delay();
}

1
我同意在我的情况下递归并不是必要的。我会采用你的解决方案。 - Micaël Félix

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