将基于回调的异步方法转换为可等待任务的最佳方式

94

如何将使用回调函数的“传统”异步方法转换/包装为返回(可等待的)任务的最佳方法?

例如,给定以下方法:

public void GetStringFromUrl(string url, Action<string> onCompleted);

我所知道的将此内容封装成返回任务的方法的唯一方法是:
public Task<string> GetStringFromUrl(string url)
{
     var t = new TaskCompletionSource<string>();

     GetStringFromUrl(url, s => t.TrySetResult(s));

     return t.Task;
}

这是实现这个的唯一方法吗?

而且,是否有一种方法可以在任务本身中包装对GetStringFromUrl(url,callback)的调用(即调用本身将在任务内部异步运行而不是同步地运行)?


2
顺便说一句,这不是“经典”的 .net 异步方法。那些是 BeginXxx()EndXxx() 对。另外,你为什么要寻找其他做法呢?你希望得到什么? - svick
9
我只是想确保我没有错过其他明显的完成同样事情的替代方法。 - Philippe Leybaert
1
这里有使用TrySetResult而不是SetResult的原因吗? - Ben Huber
2个回答

51
你的代码简短、易读和高效,所以我不明白为什么你要寻找替代方案,但我想不出别的方法。我认为你的方法是合理的。
我也不确定为什么你认为原始版本中同步部分没问题,但是你希望在基于任务的版本中避免它。如果你认为同步部分可能需要太长时间,请为方法的两个版本都进行修复。
但如果你只想在任务版本中异步运行它(即在线程池中),你可以使用Task.Run():
public Task<string> GetStringFromUrl(string url)
{
    return Task.Run(() =>
    {
        var t = new TaskCompletionSource<string>();

        GetStringFromUrl(url, s => t.TrySetResult(s));

        return t.Task;
    });
}

2
我并不总是能控制现有的基于回调的方法,因此如果该方法的同步部分执行某些耗时操作,我希望也有将其移动到任务中的选项。您的解决方案正好解决了这个问题。 - Philippe Leybaert
1
嗯...我本来以为原来带回调函数的GetStringFromUrl已经是异步的了(因此有回调函数)。如果是这样的话,这个建议将会浪费资源,因为要再开启另一个任务才能调用该调用。 - Drew Marsh
@DrewMarsh 即使是异步方法,通常也有一些同步部分,尽管大多数情况下可以忽略不计。但这个问题特别要求了这一点,否则我不会建议任何类似的做法。 - svick
1
@svick 当然,通常会有一些启动成本,但除非有人创建了一个非常糟糕的实现,否则我敢打赌,将其传递到另一个线程中使用Task.Run会产生更多的开销,而不是在当前线程上执行该部分。我认为你必须在这方面信任调用异步API,直到你能够证明它有罪,我个人永远不会建议默认这样做。只是我的两分钱。 - Drew Marsh
如果这个方法真的是异步的,那么启动一个新任务将失去所有异步运行作业的优势。如果您希望,在调用方法之前可以调用Task.Yield()将其发布到任务计划程序中。问题的代码将更好。 - Alex Lyalka
@AlexLyalka,你能分享一下你所说的代码示例吗?我正在尝试理解OPs代码、这段代码以及你所描述的区别。 - user3010678

6

如果回调函数只处理成功的情况,那么您假定的实现方式是完全正确的。但是,如果在GetStringFromUrl实现的异步基础中出现异常,当前会发生什么情况?他们没有真正的方法将其传播到Action回调函数...他们只是将其吞下并返回null或其他内容吗?

我唯一建议的是遵循新约定,使用XXXAsync后缀命名此类异步方法。


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