Мне нужно выводить информацию пользователю с задержкой. К примеру, менять содержимое текстовой метки каждую секунду. (Или выводить промежуточные результаты длинных вычислений.) В программах командной строки я делал так:
Console.WriteLine("значение 1");
Thread.Sleep(1000);
Console.WriteLine("значение 2");
Thread.Sleep(1000);
Console.WriteLine("значение 3");
Это работало. Теперь мне нужно сделать то же самое в графической программе. Я написал метод
void OnClick(object sender, EventArgs args)
{
label.Text = "значение 1";
Thread.Sleep(1000);
label.Text = "значение 2";
Thread.Sleep(1000);
label.Text = "значение 3";
}
но он работает как-то не так. Промежуточные значения не показываются, а программа надолго перестаёт реагировать. А когда она отвисает, сразу показывает последнее значение.
Что происходит? Почему программа ведёт себя неправильно, и как же сделать правильно?
Графические программы отличаются от консольных тем, что в них главный поток занимается многими вещами. В консольной программе у вас есть полный контроль, вы полностью управляете её пробегом. В графических программах вы запускаете приложение, и фреймворк для вас создаёт цикл сообщений. В этом цикле фреймворк обрабатывает передвижение мыши, нажатия на клавиши, изменения размеров окна, колбеки от таймера и тому подобные штуки, а также вызывает ваши обработчики событий, по одному на итерацию цикла (окей, это упрощённая картина, но для целей изложения подойдёт). После отработки итерации цикла выполнение переходит к следующей итерации.
Всё это работает в одном и том же потоке, который называется UI-потоком.
Теперь, что происходит, если вы в UI-потоке выполняете Thread.Sleep(1000)
? А вот что: поток блокируется и ничего не делает целую секунду. Эту самую секунду ваш цикл сообщений простаивает, потому что поток выполнения заблокирован вами! Эту секунду не обрабатываются оконные сообщения, не происходит реакция на мышь, не вызываются колбеки, и даже не перерисовывается содержимое окна — ведь всё это делается в том же самом цикле сообщений, который мы заблокировали!
Чтобы программа работала нормально, ваши обработчики событий (наподобие OnClick
), конструкторы объектов, и вообще весь код, бегущий в UI-потоке, должны пробегать максимально быстро, без задержек.
Как же сделать паузу в одну секунду? К счастью, в современной версии языка (начиная с C# 5) есть простое решение. Это async/await. Сделаем наш обработчик асинхронным (ключевое слово async
), и заменим Thread.Sleep
на await Task.Delay
:
async void OnClick(object sender, EventArgs args)
{
label.Text = "значение 1";
await Task.Delay(1000);
label.Text = "значение 2";
await Task.Delay(1000);
label.Text = "значение 3";
}
Этот метод работает правильно!¹
Что же произошло? Дело в том, что await Task.Delay
на время ожидания не блокирует поток. На время ожидания метод как бы прекращает своё выполнение, и цикл сообщений больше не блокируется. [Будьте внимательны, он может быть заблокирован ещё где-то.] Когда ожидание оканчивается, цикл сообщений возобновляет выполнение метода с прерванной точки, до следующего await
или до конца метода.²
Таким образом, наш код больше не блокирует UI-поток, и фреймворк может и дальше отрисовывать окно и заниматься прочими служебными заданиями.
А что делать, если вместо задержки нужно выполнить какие-то вычисления? Их так просто не вырезать из хода выполнения функции, они всё равно должны быть выполнены. Для этих целей их можно выгрузить в другой поток. Не пугайтесь, это очень просто. Вместо кода
label.Text = "парсим большой файл";
size = ParseBigFile();
label.Text = "закончили, результат = " + size;
вы пишете вот что:
label.Text = "парсим большой файл";
size = await Task.Run(() => ParseBigFile());
label.Text = "закончили, результат = " + size;
Task.Run
выполняет ваш код в фоновом потоке, а на время этого выполнения функция опять-таки не блокирует UI-поток.³ Профит! Обратите только внимание на то, что из фонового потока нельзя считывать значения из контролов, поэтому их нужно считать заранее:
Было:
label.Text = "парсим большой файл";
size = ParseBigFileFromPath(textbox.Text);
label.Text = "закончили, результат = " + size;
Стало:
label.Text = "парсим большой файл";
string path = textbox.Text; // читаем из контрола в UI-потоке
size = await Task.Run(() => ParseBigFileFromPath(path)); // обращается к переменной
label.Text = "закончили, результат = " + size;
В более старых версиях языка, без async/await, приходилось достигать того же самого более сложным образом. Например, заводить таймер, подписываться на его тики, и на них менять значения в контролах. При этом локальные переменные приходилось выносить в поля класса (или в специальную структуру-контекст). Или можно было делать грязные трюки с DoEvents
. К счастью, те старые недобрые времена давно прошли.
Связанные вопросы:
¹ Но для остальных асинхронных методов, не обработчиков событий, вы должны возвращать не void
, а Task
или какой-нибудь Task<string>
, чтобы вызывающий код мог дождаться их окончания и получить результат.
² Изложение грешит упрощениями, так что не принимайте его за истину в последней инстанции. Это примерная картина, а если вы хотите знать точную, лучше всего почитать книги или документацию. Или задать вопрос, если что-то ведёт себя непонятно.
³ Если вам нужно делать большую и длительную работу в фоновом потоке, то, возможно, имеет смысл выгрузить эту работу целиком и сообщать о результатах в UI через Progress<T>
.
using System.Threading;
...
new Thread(new ThreadStart(() => {
this.Invoke((MethodInvoker)delegate
{
label.Text = "значение 1";
});
Thread.Sleep(1000);
this.Invoke((MethodInvoker)delegate
{
label.Text = "значение 2";
});
Thread.Sleep(1000);
this.Invoke((MethodInvoker)delegate
{
label.Text = "C# 3.0, привет из 2017!";
});
})).Start();
Кофе для программистов: как напиток влияет на продуктивность кодеров?
Рекламные вывески: как привлечь внимание и увеличить продажи
Стратегії та тренди в SMM - Технології, що формують майбутнє сьогодні
Выделенный сервер, что это, для чего нужен и какие характеристики важны?
Современные решения для бизнеса: как облачные и виртуальные технологии меняют рынок
Подскажите, пожалуйста, реально каким-то образом вернуть строку из программы? Скажем, из одной программы я вызываю другую, которая должна...
Изучаю c# поэтому возник вопрос про полиморфизмСам пробовал писать но успеха не добился поэтому спрашиваю у специалистов