gpt4 book ai didi

c# - 如何解决这个递归异步/等待问题?

转载 作者:行者123 更新时间:2023-11-30 17:01:09 26 4
gpt4 key购买 nike

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

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 .结果不会生成,因为委托(delegate)类型不匹配。我将不得不返回 Task<int>而不是 int :

...

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

如果我将代表的签名更改为 Func<Node, Task<int>>我将不得不做我的Visit方法 async 这有点奇怪 - 它已经作为 Task 运行而且我不太喜欢使用递归异步方法来获取传入的异步回调。无法解释,但它看起来不对。

问题:

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

最佳答案

重构后,您的新异步 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 一次,只要处理在其中进行。

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

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

请注意,我们仍然不使用 Task.Run里面的任何地方 ProcessAsync ,所以在树的递归遍历过程中不会有多余的线程切换。

另请注意,您不需要再添加一个 async/await像下面这样的 lambda:

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

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

最后,如果里面有东西DoSomeProcessingDoSomethingWithTheNodeAsync更新 UI,它必须在 UI 线程上完成。使用 Monotouch,可以通过 SynchronizationContext.Post 完成/Send UI 线程的 SynchronizationContext .

关于c# - 如何解决这个递归异步/等待问题?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21352654/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com