Асинхронный / ожидающий запуск, блокирующий пользовательский интерфейс без очевидной причины

#c# #wpf #asynchronous #task

#c# #wpf #асинхронный #задача

Вопрос:

Я еще раз повторно просматривал асинхронные задачи. Независимо от того, как я устанавливаю задачи, мое приложение постоянно страдает от зависания пользовательского интерфейса. У меня есть следующий код для загрузки строки с веб-страницы:

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

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.Resu<

}
  

Когда я выполняю эту задачу и во время цикла for, пользовательский интерфейс моего приложения зависает. Хотя я считаю, что этот код не должен замораживать пользовательский интерфейс, я не могу найти решение. Я перепробовал много разных вариантов и действительно хочу использовать задачи вместо потоков или событий с webclient async.

Информация: Я использую .net 4.5 для своего проекта. Разница в моем коде в том, что эти функции находятся внутри библиотеки классов (не знаю, имеет ли это значение).

Возможно ли запустить этот код без блокировки пользовательского интерфейса с помощью async await, вызвав функцию DownloadString из моего кода? Если нет, то каковы альтернативы (любые хорошие пакеты nuget)?

Комментарии:

1. result.Result это блокирует

2. Это неправильный способ использования async и Task.Result.

3. Интересно, что сам компилятор выдаст вам предупреждение, в котором конкретно говорится, что LoadCompanyContracts будет выполняться синхронно.

Ответ №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 само по себе возвращает выполнение в исходный контекст выполнения (т. Е. поток пользовательского интерфейса). Это может быть или не быть желательным, вот почему библиотечный код обычно использует 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(..); НО это заставило бы метод вернуть и удалить 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 resu<
}
  

Редактировать

В исходном ответе использовался DownloadStringAsync, который является устаревшим методом, который вызывает событие при завершении загрузки. Правильный метод — DownloadStringTaskAsync

РЕДАКТИРОВАТЬ 2 Поскольку мы говорим об пользовательском интерфейсе, код можно сделать асинхронным вплоть до верхнего обработчика событий, используя async void синтаксис для обработчика, eg async void Button1_Click , eg:

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

В этом случае мы хотим вернуться к исходному потоку, поэтому мы не используем ConfigureAwait(false);

Комментарии:

1.@Evangelink Это просто привело бы к потере потока. DownloadStringAsync выполняется в фоновом потоке или, скорее, использует порты завершения ввода-вывода, поэтому вообще не тратит поток впустую в ожидании результата. Task.Run с другой стороны, поток тратится впустую в ожидании ответа.

2. @PanagiotisKanellopoulos упс, это должна быть строка DownloadStringTaskAsync . Исправлено

3. @PanagiotisKanellopoulos кто-то пропустил собрания DotNetZone на TPL за последние 3 года ….

4. @PanagiotisKanellopoulos вы должны использовать async вплоть до вашего уровня пользовательского интерфейса — не используйте .Result нигде. В противном случае ваш код зайдет в тупик.

5. @PanagiotisKanellopoulos почему? var result = await AsyncMethod() выдает результат, как result для остальной части контекста этого метода. await Task является асинхронным эквивалентом Task.Result . Дело в том, что вызывающий объект также должен быть асинхронным (и его вызывающий объект, и так далее), Что является основным фундаментальным принципом асинхронности — если вы блокируете асинхронный процесс, он перестает быть асинхронным.