Async / await,在没有明显原因的情况下运行阻止UI

时间:2016-10-21 11:50:07

标签: c# wpf asynchronous task

我再一次重新研究了异步任务。无论我如何设置任务,我的应用程序一直受到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函数,是否可以在不使用async await阻止用户界面的情况下运行此代码?如果不是什么替代品(任何好的nuget包)?

1 个答案:

答案 0 :(得分:5)

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,那么等待是没有意义的。如果您不希望在您的代码中使用该方法的结果,则根本不使用awaitLoadCompanyContracts可能只是:

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的原因:单个实例支持多个并发调用,这意味着您可以只有一个实例,例如作为字段并重复使用它,例如:< / p>

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