我有一个递归方法,它访问树层次结构中的每个节点并触发每个节点的回调,例如(下面的代码未经过测试和示例):
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>
。但是,使用异步方法似乎是不可能的。答案 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的更多信息。
最后,如果DoSomeProcessing
或DoSomethingWithTheNodeAsync
内的内容更新了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
异步。