Графические программы отличаются от консольных тем, что в них главный поток занимается многими вещами. В консольной программе у нас есть полный контроль, мы полностью управляем её пробегом. В графических программах мы запускаем приложение, и фреймворк для нас создаёт цикл сообщений. В этом цикле фреймворк обрабатывает передвижение мыши, нажатия на клавиши, изменения размеров окна, колбеки от таймера и тому подобные штуки, а также вызывает наши обработчики событий, по одному на итерацию цикла (окей, это упрощённая картина, но для целей изложения подойдёт). После отработки итерации цикла выполнение переходит к следующей итерации.
Всё это работает в одном и том же потоке, который называется 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>.
lock'ом, а ту же переменную из контрола только черезInvoke. В общем, с потоками всё сложнее. – VladD Jan 18 '17 at 10:11Invoke, нужно объяснить, когда это заклинание нужно, а когда можно и без него, а то ж читатель обернёт весь код потока вInvoke(реальная ситуация, кстати, была в вопросе тут пару месяцев назад). – VladD Jan 18 '17 at 10:14