【问题标题】:Async/await, run blocking the UI without obvious reason异步/等待,无明显原因运行阻塞 UI
【发布时间】:2016-10-21 11:50:07
【问题描述】:

我一直在重新研究异步任务。无论我如何设置任务,我的应用程序都会一直遭受 UI 冻结的困扰。我有以下代码用于从网页下载字符串:

internal string DownloadString(string URL)
{
      var result =  LoadCompanyContracts(URL);
      return result.Result;
}

internal async Task<string> LoadCompanyContracts(string URL)
{
Task<string> task2 = Task<string>.Factory.StartNew(() =>
{
     for (int i = 0; i <= 10000000; i++) Console.WriteLine(i);
     WebClient wc = new WebClient();
     string tmp = wc.DownloadString(new Uri(URL));
     return tmp;
});
return task2.Result;

}

当我执行此任务并在 for 循环期间,我的应用程序的 UI 冻结。即使我相信这段代码不应该冻结 UI,我也无法找到解决方案。我已经尝试了许多不同的选项,并且真的想在 webclient 异步中使用任务而不是线程或事件。

信息:我正在为我的项目使用 .net 4.5。我的代码的不同之处在于这些函数在一个类库中(不知道是否重要)。

是否可以通过从我的代码中调用 DownloadString 函数来运行此代码而不用异步等待阻塞用户界面?如果不是,还有什么替代方案(任何好的 nuget 包)?

【问题讨论】:

  • result.Result 这个块
  • 那是使用async和Task.Result的错误方式。
  • 有趣的是,编译器本身会给你一个警告,特别指出LoadCompanyContracts 将同步运行。

标签: c# wpf asynchronous task


【解决方案1】:

async 关键字不会使某些东西异步运行,它使您能够使用await等待一个已经异步的操作。你需要使用 DownloadStringTaskAsync真正异步下载:

internal async Task<string> LoadCompanyContracts(string URL)
{
    ....
    using(var wc = new WebClient())
    {
        string tmp = await wc.DownloadStringTaskAsync(new Uri(URL));
        return tmp;
    }
}

await 本身返回原始执行上下文(即 UI 线程)中的执行。这可能是可取的,也可能不是可取的,这就是为什么库代码通常使用ConfigureAwait(false); 并让库的最终用户决定如何等待:

string tmp = await wc.DownloadStringTaskAsync(new Uri(URL))
                     .ConfigureAwait(false);

最后,如果你要从顶层函数调用.Result,那么等待是没有意义的。如果您不想在代码中使用该方法的结果,那么使用await 毫无意义。 LoadCompanyContracts 可能只是:

internal Task<string> LoadCompanyContracts(string URL)
{
    ....
    using(var wc = new WebClient())
    {
        return wc.DownloadStringTaskAsync(new Uri(URL))
                 .ConfigureAwait(false);
    }
}

糟糕

通常,如果只返回异步操作的结果,则根本不需要使用 await。该方法可能只是return wc.DownloadStringTaskAsync(..);BUT,这将导致该方法返回在下载完成之前处理WebClient。避免 using 块也不是解决方案,因为它会使像 WebClient 这样的昂贵对象的存活时间超过必要的时间。

这就是为什么HttpClient 优于WebClient 的原因:单个实例支持多个并发调用,这意味着您可以只拥有一个实例,例如作为一个字段并重复使用它,例如:

  HttpClient _myClient =new HttpClient();

  internal Task<string> LoadCompanyContractsAsync(string URL)
  {
      ....
      return _myClient.GetStringAsync(new Uri(URL))
                      .ConfigureAwait(false);
  }
}

您可以摆脱您的DownloadString,因为它在LoadCompanyContracts 之上没有任何作用。如果确实使用了LoadCompanyContracts的结果,应该改写为:

internal async Task<string> DownloadString(string URL)
{
  var result =  await LoadCompanyContracts(URL);
  //Do something with the result
  return result;
}

编辑

原始答案使用了DownloadStringAsync,这是一种在下载完成时引发事件的遗留方法。正确的方法是DownloadStringTaskAsync

编辑 2 由于我们正在讨论 UI,因此代码可以通过使用处理程序的async void 语法,例如async void Button1_Click,一直异步到顶级事件处理程序,例如:

async void LoadCustomers_Click(...)
{   
    var contracts=await LoaCompanyContracts(_companyUrls);
    txtContracts>Text=contracts;
}

在这种情况下我们想要回到原来的线程,所以我们不使用ConfigureAwait(false);

【讨论】:

  • @Evangellink 那只会浪费线程。 DownloadStringAsync 确实在后台线程上运行,或者更确切地说,它使用 IO 完成端口,因此在等待结果时不会浪费线程根本。另一方面,Task.Run 在等待响应时浪费了一个线程。
  • @PanagiotisKanellopoulos 哎呀,应该是DownloadStringTaskAsync。修复它
  • @PanagiotisKanellopoulos 过去 3 年有人错过了 DotNetZone 在 TPL 上的聚会......
  • @PanagiotisKanellopoulos 你应该一直使用异步直到你的 UI 层——不要在任何地方使用.Result。否则你的代码会死锁。
  • @PanagiotisKanellopoulos 为什么?对于该方法的其余上下文,var result = await AsyncMethod() 为您提供result 的结果。 await TaskTask.Result 的异步等价物。关键是调用者也必须是异步的(及其调用者等等),这是异步的基本基本原则 - 如果您阻塞异步进程,它就不再是异步的。
猜你喜欢
  • 2016-03-13
  • 1970-01-01
  • 1970-01-01
  • 2022-01-04
  • 1970-01-01
  • 2011-04-03
  • 2019-09-24
  • 2019-02-19
  • 1970-01-01
相关资源
最近更新 更多