如何从非异步方法调用异步方法?

25

以下是我的方法:

    public string RetrieveHolidayDatesFromSource() {
        var result = this.RetrieveHolidayDatesFromSourceAsync();
        /** Do stuff **/
        var returnedResult  = this.TransformResults(result.Result); /** Where result gets used **/
        return returnedResult;
    }


    private async Task<string> RetrieveHolidayDatesFromSourceAsync() {
        using (var httpClient = new HttpClient()) {
            var json = await httpClient.GetStringAsync(SourceURI);
            return json;
        }
    }

上述方法不起作用,似乎无法正确返回任何结果。我不确定在哪里漏了一个语句来强制等待结果?我想让RetrieveHolidayDatesFromSource()方法返回一个字符串。

下面的方法可以正常工作,但是它是同步的,我相信它可以改进?请注意,下面是同步的,我希望将其更改为异步,但出于某种原因无法理解。

    public string RetrieveHolidayDatesFromSource() {
        var result = this.RetrieveHolidayDatesFromSourceAsync();
        /** Do Stuff **/

        var returnedResult = this.TransformResults(result); /** This is where Result is actually used**/
        return returnedResult;
    }


    private string RetrieveHolidayDatesFromSourceAsync() {
        using (var httpClient = new HttpClient()) {
            var json = httpClient.GetStringAsync(SourceURI);
            return json.Result;
        }
    }

我是不是漏了什么?

注意:由于某种原因,当我在上述异步方法上下断点时,当它到达var json = await httpClient.GetStringAsync(SourceURI)这一行时,它就会跳出断点,我无法再进入该方法。

2个回答

31

我错过了什么吗?

是的。异步代码本质上意味着在操作正在进行时,当前线程不会被使用。同步代码本质上意味着在操作正在进行时,当前线程被阻塞。这就是为什么从同步代码调用异步代码甚至没有任何意义。事实上,正如我在我的博客中所描述的那样,一种天真的方法(使用Result/Wait)很容易导致死锁

首先要考虑的是:我的API应该是同步还是异步的?如果它涉及I/O(如此示例),那么它应该是异步的。因此,这将是一个更合适的设计:

public async Task<string> RetrieveHolidayDatesFromSourceAsync() {
    var result = await this.DoRetrieveHolidayDatesFromSourceAsync();
    /** Do stuff **/
    var returnedResult  = this.TransformResults(result); /** Where result gets used **/
    return returnedResult;
}

正如我在async最佳实践文章中所描述的那样,你应该"全程异步化"。如果不这样做,你将无法从async中获得任何好处,那么为什么要费心呢?

但是假设你有兴趣最终采用异步方式,但现在你不能改变所有内容,只想更改应用程序的一部分。这是一个非常普遍的情况。

在这种情况下,正确的方法是公开同步和异步API。最终,在所有其他代码升级完成后,可以删除同步API。我在关于旧项目中异步开发的文章中探讨了各种选项;我的个人喜好是"bool参数技巧",它看起来像这样:

public string RetrieveHolidayDatesFromSource() {
  return this.DoRetrieveHolidayDatesFromSourceAsync(sync: true).GetAwaiter().GetResult();
}

public Task<string> RetrieveHolidayDatesFromSourceAsync() {
  return this.DoRetrieveHolidayDatesFromSourceAsync(sync: false);
}

private async Task<string> DoRetrieveHolidayDatesFromSourceAsync(bool sync) {
  var result = await this.GetHolidayDatesAsync(sync);
  /** Do stuff **/
  var returnedResult  = this.TransformResults(result);
  return returnedResult;
}

private async Task<string> GetHolidayDatesAsync(bool sync) {
  using (var client = new WebClient()) {
    return sync
        ? client.DownloadString(SourceURI)
        : await client.DownloadStringTaskAsync(SourceURI);
  }
}

这种方法避免了代码重复,也避免了其他“同步-异步”反模式解决方案中常见的任何死锁或可重入问题。
请注意,我仍然将生成的代码视为通往正确异步API的“中间步骤”。特别是,内部代码必须回退到支持同步和异步的WebClient而不是首选的HttpClient(仅支持异步)。一旦所有调用代码都更改为使用RetrieveHolidayDatesFromSourceAsync而不是RetrieveHolidayDatesFromSource,那么我会重新审视此事,并删除所有技术债务,将其更改为使用HttpClient并且是异步的。

1
public Task<string> RetrieveHolidayDatesFromSourceAsync() 缺少 await 吗? - Nick Weaver
3
@NickWeaver:不是异步函数,因此无法使用await - Stephen Cleary
1
@NickWeaver:由于这是一个微不足道的方法,它只是直接返回任务。它仍然是一个异步API,因此有Async后缀,但其实现不使用async/await - Stephen Cleary
1
感谢您的解释。 - Nick Weaver
@TheRedPea:是的,“异步”(不会阻塞调用线程)和“async”(一种实现技术)之间有区别。例如,在接口中定义的TAP方法只返回任务 - 没有“async”。实现可能使用或不使用“async”。此外,“async”并不能“使任务可等待”。无论是由“async”状态机还是其他方式创建,都可以等待“Task”类型,这是一个可等待类型。 - Stephen Cleary
显示剩余5条评论

7
public string RetrieveHolidayDatesFromSource() {
    var result = this.RetrieveHolidayDatesFromSourceAsync().Result;
    /** Do stuff **/
    var returnedResult  = this.TransformResults(result.Result); /** Where result gets used **/
    return returnedResult;
}

如果您在异步调用中添加.Result,它将执行并等待结果到达,强制它变为同步。
private static string stringTest()
{
    return getStringAsync().Result;
}

private static async Task<string> getStringAsync()
{
    return await Task.FromResult<string>("Hello");
}
static void Main(string[] args)
{
    Console.WriteLine(stringTest());

}

回应评论: 这个可以正常工作,没有任何问题。

1
我认为这不起作用,因为方法中已经有了“result.Result”。使用此.RetrieveHolidayDatesFromSourceAsync().Result将把result更改为非任务对象,这将导致“var returnedResult = this.TransformResults(result.Result)”中出现错误。 - Samuel Tambunan
6
这可以顺利运行,没有任何问题。当在控制台程序之外使用时,可能会出现死锁情况,请说明如何处理结果死锁。 - Alexei Levenkov
2
如果您在 UI 环境或任何只有一个线程的环境中运行,则可能会出现这种情况 SynchronizationContext。在这种情况下,对 .Result 的调用将阻止进一步执行,但是 async 方法中的 await 可能会在被阻塞的线程的运行时期间稍后注册其继续。在 UI 环境中混合异步和同步等待总是非常危险的。 - Nitram
1
它“没有任何问题地工作”的事实有点偶然,仅仅是因为当你等待它时,返回的任务已经完成。这在使用HttpClientWebClient的任何代码中都不会发生。 - Kirill Shlenskiy
我想从一个有很多用户控件的现有 .aspx 页面调用一个异步 API,但这个 API 不受我的控制(它是异步的,我无法更改它)。我可以将 .aspx 页面设置为异步,但那我是否需要将所有用户控件和使用用户控件的其他页面都设置为异步呢? - Jim S
显示剩余2条评论

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