Async/await,运行阻塞UI没有明显的原因

本文关键字:UI await 运行 Async | 更新日期: 2023-09-27 18:14:15

我再次研究了异步任务。无论我如何设置任务,我的应用程序总是受到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软件包)?

Async/await,运行阻塞UI没有明显的原因

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);