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

时间:2014-01-25 15:48:02

标签: c# recursion async-await

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

void Visit(Node node, Func<Node, int> 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<Node, int> callback)
{
  var rootNode = await someWebService.GetNodes();
  await Task.Run( () => Visit(rootNode, callback) );
}

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

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

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

...

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

如果我将代理人的签名更改为Func<Node, Task<int>>,我将不得不使我的Visit方法异步,这有点奇怪 - 它已经作为Task运行而我感觉不到非常好的使用递归异步方法来获取传入的异步回调。无法解释,但看起来不对。

问题:

  • 这种方法是否不适合异步世界?
  • 如何正确完成?
  • 我原本不想使用回调方法而是使用IEnumerable<Node>。但是,使用异步方法似乎是不可能的。
  • 这个问题对于codereview.stackexchange.com来说是否更合适?

2 个答案:

答案 0 :(得分:2)

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

async Task Visit(Node node, Func<Node, Task<int>> 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<Node, Task<int>> callback, token)
{
  var rootNode = await someWebService.GetNodes();
  await Visit(rootNode, callback, token);
}

它可以简单地这样调用:

await ProcessAsync(DoSomethingWithTheNodeAsync, token);

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

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

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

Visit内,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));
}

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

另请注意,需要向lambda添加另一个async/await,如下所示:

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

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

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

答案 1 :(得分:0)

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

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

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

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

这是异步同步。让Visit异步并可疑地处理Task.Run

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

  

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

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