如何让这个递归异步/等待问题正确?

我有一个递归方法访问树层次结构中的每个节点并触发每个节点的回调,如(下面的代码未经过测试和示例):

void Visit(Node node, Func callback, CancellationToken ct) { if(ct.IsCancellationRequested) { return; } var processedNode = DoSomeProcessing(node); int result = callback(processedNode); // Do something important with the returned int. ... // Recursion. foreach(var childNode in node.Children) { Visit(childNode, callback); } } 

上面的方法是从传递回调的异步方法调用的。 由于它长时间运行,我将调用包装成Task.Run()

 async Task ProcessAsync(Func callback) { var rootNode = await someWebService.GetNodes(); await Task.Run( () => Visit(rootNode, callback) ); } 

现在问题是:一些异步代码正在等待ProcessAsync():

 ... await ProcessAsync( node => { return DoSomethingWithTheNode(); }); ... 

这很有效。 但是,我们没有重构, DoSomethingWithTheNode变为异步: DoSomethingWithTheNodeAsync 。 结果将无法生成,因为委托类型不匹配。 我必须返回Task而不是int

 ... await ProcessAsync( async node => { return await DoSomethingWithTheNodeAsync(); }); ... 

如果我将委托的签名更改为Func<Node, Task>我将不得不使我的Visit方法异步,这有点奇怪 – 它已经作为一个Task运行,我觉得使用递归异步并不是很好获取异步回调的方法传入。无法解释,但看起来不对。

题:

  • 这种方法不适合异步世界吗?
  • 如何做到这一点正确?
  • 我最初不想使用回调方法,而是使用IEnumerable 。 然而,对于异步方法,这似乎是不可能的。
  • 这个问题对于codereview.stackexchange.com来说可能更合适吗?

重新分解后,您的新异步Visit可能如下所示:

 async Task Visit(Node node, Func> callback, CancellationToken ct) { if(ct.IsCancellationRequested) { return; } var processedNode = DoSomeProcessing(node); int result = await callback(processedNode).ConfigureAwait(false); // Do something important with the returned int. ... // Recursion. foreach(var childNode in node.Children) { await Visit(childNode, callback, token); } } 

然后ProcessAsync看起来像这样:

 async Task ProcessAsync(Func> callback, token) { var rootNode = await someWebService.GetNodes(); await Visit(rootNode, callback, token); } 

它可以简单地像这样调用:

 await ProcessAsync(DoSomethingWithTheNodeAsync, token); 

因为您在回调中引入了异步,所以很可能您不再需要将ProcessAsync卸载到单独的线程中。 下面我将尝试解释原因。

让我们考虑一下你的DoSomethingWithTheNodeAsync看起来像这样:

 async Task DoSomethingWithTheNodeAsync(Node node) { Debug.Print(node.ToString()); await Task.Delay(10); // simulate an IO-bound operation return 42; } 

Visit内部, await callback(processedNode).ConfigureAwait(false)之后的执行await callback(processedNode).ConfigureAwait(false)将在随机池线程(正好为异步Task.Delay操作的完成服务的线程)上继续。 因此,将不再阻止UI线程。

对于你可能在DoSomethingWithTheNodeAsync使用的任何其他纯异步API也是如此(我认为这是重新分解的最初原因)。

现在,我唯一担心的是:

 var processedNode = DoSomeProcessing(node); 

一旦调用了ProcessAsync(DoSomethingWithTheNodeAsync) ,上述DoSomeProcessing第一次调用将在与原始调用相同的线程上进行。 如果这是一个UI线程, DoSomeProcessing可能会阻止UI一次,因为只要处理进入它。

如果这是一个问题,那么无论你从UI线程调用ProcessAsync ,用Task.Run包装它 ,例如:

 void async button_click(object s, EventArgs e) { await Task.Run(() => ProcessAsync(DoSomethingWithTheNodeAsync)); } 

注意,我们仍然不在ProcessAsync内的任何地方使用Task.Run ,因此在树的递归遍历期间不会有冗余的线程切换。

另请注意,您不需要像下面那样向lambda添加另一个async/await

 await Task.Run(async () => await ProcessAsync(DoSomethingWithTheNodeAsync)); 

这将添加一些冗余的编译器生成的状态机代码。 Task.Run有一个覆盖处理lambda返回TaskTask ,它使用Task.Unwrap解包嵌套任务。 更多关于这里 。

最后,如果DoSomeProcessingDoSomethingWithTheNodeAsync某些内容更新了UI,则必须在UI线程上完成。 使用Monotouch,可以通过UI线程的SynchronizationContext SynchronizationContext.Post / Send来完成。

如果要使用async取消阻塞调用线程,则整个调用树会被感染,直到需要释放线程为止。 这是异步的本质,这是您应该仔细考虑是否要将其带入代码库的主要原因。

从这个意义上说, Visit应该是异步的,因为它调用的东西是异步的。 不要陷入实现众所周知的异步同步反模式(或同步异步)的陷阱。

特别是,这可能是错误的:

 await Task.Run(() => Visit(rootNode, callback) ); 

这是异步同步。 使Visit异步并怀疑地对待Task.Run

关于你关于IEnumerable子问题:我不清楚你想要完成什么。 根据这一点,您可以使用Task>IAsyncEnumerable 。 取决于用例。

…我将不得不使我的访问方法异步,这有点奇怪 – 它已经作为一个任务运行,我不觉得使用递归异步回调传递的递归异步方法。

你应该对使用Task.Run感到不Task.Run这是错误,而不是Visit异步。