如何从非异步方法调用异步方法? [复制]

Posted

技术标签:

【中文标题】如何从非异步方法调用异步方法? [复制]【英文标题】:How Do I Call an Async Method from a Non-Async Method? [duplicate] 【发布时间】:2016-07-20 19:31:39 【问题描述】:

我有以下方法:

    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) 行时,它刚刚超出断点,我无法返回到该方法。

【问题讨论】:

【参考方案1】:

我错过了什么吗?

是的。异步代码 - 就其性质而言 - 意味着在操作进行时不使用当前线程。同步代码 - 就其性质而言 - 意味着当前线程在操作进行时被阻塞。这就是为什么从同步代码中调用异步代码实际上甚至没有意义。事实上,正如我在博客中所描述的那样,a naive approach (using Result/Wait) can easily result in deadlocks。

首先要考虑的是:应该我的 API 是同步的还是异步的?如果它处理 I/O(如本例所示),则为 should be asynchronous。所以,这将是一个更合适的设计:

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

正如我在async best practices article 中所描述的,您应该“一直异步”。如果你不这样做,无论如何你都不会从异步中获得任何好处,那何必呢?

但是假设您对最终异步化感兴趣,但现在您无法更改所有内容,您只想更改部分 您的应用程序。这是很常见的情况。

在这种情况下,正确的方法是公开同步和异步 API。最终,在所有其他代码都升级后,可以删除同步 API。我在article on brownfield async development 中探索了这种场景的各种选择;我个人最喜欢的是“bool parameter hack”,它看起来像这样:

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 并仅使用异步。

【讨论】:

public Task&lt;string&gt; RetrieveHolidayDatesFromSourceAsync() 是否缺少await @NickWeaver:不,不是async,所以不能使用await 我明白了。我想知道,因为该方法的名称最后是“异步”。为什么不设置为异步? @NickWeaver:因为它是一个简单的方法,它只是直接返回任务。它仍然是一个异步 API,因此有 Async 后缀,但不使用 async/await 来实现它。 感谢您的解释。【参考方案2】:
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());


解决评论:这没有任何问题。

【讨论】:

我认为这行不通,因为我已经在方法中有“result.Result”。拥有 this.RetrieveHolidayDatesFromSourceAsync().Result 会将结果更改为非任务对象,这将导致“var returnedResult = this.TransformResults(result.Result)”中的错误。 我更新了我的答案以向您展示工作代码。您可以看到 getStringAsync 是在同步方法(stringTest())中调用的异步方法,我得到所需的输出而没有错误。您遇到了什么错误? “这工作没有任何问题”:)...您可能会更好一点,并解释如何在实际使用外部控制台应用程序时处理导致的死锁。 如果您在 UI 环境或任何具有单线程 SynchronizationContext 的环境中运行,可能会有一个。在这种情况下,对.Result 的调用将阻止进一步的执行,但async 方法中的await 可能会在稍后被阻止的线程运行时注册它的继续。混合异步和同步等待总是很危险的,尤其是在 UI 环境中。 它确实“没有任何问题地工作”这一事实有点侥幸,这仅仅是因为当您等待它时返回的任务已经完成。任何使用HttpClientWebClient 的代码都不会出现这种情况。

以上是关于如何从非异步方法调用异步方法? [复制]的主要内容,如果未能解决你的问题,请参考以下文章

如何从非异步方法发送 websocket 消息?

我们可以从构造函数中调用异步方法吗? [复制]

从同步代码调用异步方法并阻塞直到任务完成的正确方法是啥? [复制]

C# 中有没有办法在不使调用者也异步的情况下调用异步方法? [复制]

如果我不等待它,是不是保证从 Mvc.Controller 操作方法调用的异步方法完成? [复制]

同步调用异步方法