Почему мой асинхронный код работает синхронно при отладке?
Я пытаюсь реализовать метод под названием ReadAllLinesAsync
с помощью функции async. Я создал следующий код:
private static async Task<IEnumerable<string>> FileReadAllLinesAsync(string path)
{
using (var reader = new StreamReader(path))
{
while ((await reader.ReadLineAsync()) != null)
{
}
}
return null;
}
private static void Main()
{
Button buttonLoad = new Button { Text = "Load File" };
buttonLoad.Click += async delegate
{
await FileReadAllLinesAsync("test.txt"); //100mb file!
MessageBox.Show("Complete!");
};
Form mainForm = new Form();
mainForm.Controls.Add(buttonLoad);
Application.Run(mainForm);
}
Я ожидаю, что указанный код будет выполняться асинхронно, а на самом деле это так! Но только когда я запускаю код без Visual Studio Debugger.
Когда я запускаю код с приложением Visual Studio Debugger, код запускается синхронно, блокируя основной поток, заставляя пользовательский интерфейс зависать.
Я попытался воспроизвести проблему на трех машинах и преуспел. Каждый тест проводился на 64-битной машине (Windows 8 или Windows 7) с использованием Visual Studio 2012.
Я хотел бы знать, почему эта проблема возникает и как ее решить (поскольку работа без отладчика, вероятно, будет препятствовать развитию).
Ответы
Ответ 1
Я вижу ту же проблему, что и вы, - но только в какой-то степени. Для меня пользовательский интерфейс очень отрывочен в отладчике и иногда отрывается в отладчике. (Мой файл состоит из множества строк из 10 символов, кстати - форма данных изменит поведение здесь.) Часто в отладчике хорошо начинать, потом плохо в течение длительного времени, а затем он иногда восстанавливается.
I подозреваемый проблема может заключаться в том, что ваш диск слишком быстрый, а ваши строки слишком короткие. Я знаю, что это звучит безумно, поэтому позвольте мне объяснить...
Когда вы используете выражение await
, это будет проходить через путь "присоединить продолжение", если это необходимо. Если результаты уже присутствуют, код просто извлекает значение и продолжается в том же потоке.
Это означает, что если ReadLineAsync
всегда возвращает задание, которое завершается к моменту его возвращения, вы эффективно увидите синхронное поведение. Вполне возможно, что ReadLineAsync
смотрит, какие данные он уже получил, и пытается синхронно найти в нем строку. После этого операционная система может считывать больше данных с диска, чтобы она была готова для использования вашим приложением... что означает, что нить пользовательского интерфейса никогда не сможет передавать свои обычные сообщения, поэтому пользовательский интерфейс замерзает.
Я ожидал, что запуск одного и того же кода через сеть "исправит" проблему, но это не похоже. (Он точно меняет, как проявляется рывкость, заметьте.) Однако, используя:
await Task.Delay(1);
Разблокирует пользовательский интерфейс. (Task.Yield
, однако, это не слишком меня смущает. Я подозреваю, что это может быть вопросом приоритетности между продолжением и другими событиями пользовательского интерфейса.)
Теперь о том, почему вы видите это только в отладчике - это все еще меня смущает. Возможно, это как-то связано с тем, как прерывания обрабатываются в отладчике, изменяя время.
Это только догадки, но они, по крайней мере, несколько образованные.
EDIT: Хорошо, я разработал способ указать, что это по крайней мере частично связано с этим. Измените свой метод следующим образом:
private static async Task<IEnumerable<string>>
FileReadAllLinesAsync(string path, Label label)
{
int completeCount = 0;
int incompleteCount = 0;
using (var reader = new StreamReader(path))
{
while (true)
{
var task = reader.ReadLineAsync();
if (task.IsCompleted)
{
completeCount++;
}
else
{
incompleteCount++;
}
if (await task == null)
{
break;
}
label.Text = string.Format("{0} / {1}",
completeCount,
incompleteCount);
}
}
return null;
}
... и создайте и добавьте подходящую метку в пользовательский интерфейс. На моей машине, как в отладочной, так и не отладочной, я вижу гораздо более "полные" хиты, чем "неполные" - как ни странно, отношение полного к неполному равно 84: 1 последовательно, как под отладчиком, так и нет. Таким образом, только после прочтения одной из 85 строк пользовательский интерфейс может получить возможность обновить. Вы должны попробовать то же самое на своей машине.
В качестве другого теста я добавил счетчик, увеличивающий значение в событии label.Paint
- в отладчике он выполнял только 1/10, столько раз, сколько не в отладчике, для того же количества строк.
Ответ 2
Проблема заключается в том, что вы вызываете await reader.ReadLineAsync()
в замкнутом цикле, который ничего не делает, кроме выполнения возврата к потоку пользовательского интерфейса после каждого ожидания, прежде чем начинать все заново. Ваш поток пользовательского интерфейса может обрабатывать события Windows ТОЛЬКО, а ReadLineAsync()
пытается прочитать строку.
Чтобы исправить это, вы можете изменить вызов на await reader.ReadLineAsync().ConfigureAwait(false)
.
await
ожидает завершения асинхронного вызова и возвращает выполнение в контексте Syncrhonization, который вызвал await
в первую очередь. В настольном приложении это поток пользовательского интерфейса. Это хорошо, потому что он позволяет вам напрямую обновлять интерфейс, но может вызвать блокировку, если вы обрабатываете результаты асинхронного вызова сразу после await
.
Вы можете изменить это поведение, указав ConfigureAwait(false)
, и в этом случае выполнение продолжается в другом потоке, а не в исходном контексте синхронизации.
Ваш исходный код будет блокироваться, даже если это был не просто жесткий цикл, так как любой код в цикле, который обрабатывал данные, все равно выполнялся в потоке пользовательского интерфейса. Чтобы обрабатывать данные асинхронно без добавления ConfigureAwait
, вы должны обработать данные в taks, созданных с использованием, например. Task.Factory.StartNew и ждите эту задачу.
Следующий код не будет блокироваться, потому что обработка выполняется в другом потоке, позволяя потоку пользовательского интерфейса обрабатывать события:
while ((line= await reader.ReadLineAsync()) != null)
{
await Task.Factory.StartNew(ln =>
{
var lower = (ln as string).ToLowerInvariant();
Console.WriteLine(lower);
},line);
}
Ответ 3
Visual Studio не выполняет синхронный асинхронный обратный вызов. Однако ваш код структурирован таким образом, что он "наводняет" поток пользовательского интерфейса сообщениями, которые вам, возможно, не нужно выполнять в потоке пользовательского интерфейса. В частности, когда FileReadAllLinesAsync
возобновляет выполнение в теле цикла while
, он делает это на SynchronizationContext
, который был записан в строке await
тем же методом. Это означает, что для каждой строки вашего файла сообщение отправляется обратно в поток пользовательского интерфейса для выполнения 1 копии тела этого цикла while.
Вы можете решить эту проблему, используя ConfigureAwait(false)
тщательно.
-
В FileReadAllLinesAsync
тело цикла while не чувствительно к тому, к какому потоку он работает, поэтому вы можете использовать следующее:
while ((await reader.ReadLineAsync().ConfigureAwait(false)) != null)
-
В Main
предположим, что вы хотите, чтобы строка MessageBox.Show
выполнялась в потоке пользовательского интерфейса (возможно, у вас также есть инструкция buttonLoad.Enabled = true
). Вы можете (и будете!) По-прежнему получать это поведение без каких-либо изменений в Main
, так как вы не использовали ConfigureAwait(false)
там.
Я подозреваю, что задержки, которые вы наблюдаете в отладчике, связаны с медленной производительностью .NET в управляемом/неуправляемом коде при подключении отладчика, поэтому отправка каждого из этих миллионов сообщений в поток пользовательского интерфейса до 100 раз медленнее, если у вас есть отладчик прилагается. Вместо того, чтобы пытаться ускорить эту диспетчеризацию, отключив функции, я подозреваю, что пункт № 1 выше решит основную часть ваших проблем немедленно.
Ответ 4
От Асинхронный шаблон на основе задач в Центре загрузки Microsoft:
По соображениям производительности, если задача уже завершена к моменту времени задача ожидается, управление не будет выполнено, а функция вместо этого продолжит выполнение.
и
В некоторых случаях объем работы, необходимый для завершения операции меньше объема работы, необходимой для запуска операции асинхронно (например, чтение из потока, в котором чтение может быть удовлетворяются данными, уже буферизованными в памяти). В таких случаях операция может завершиться синхронно, возвращая Задачу, которая уже завершено.
Таким образом, мой последний ответ был неправильным (синхронный по времени для асинхронной работы с синтаксисом ).