Содержание
Асинхронное программирование позволяет избежать появления узких мест в приложении и увеличить общую скорость реагирования на действия пользователя. Суть асинхронного программирования заключается в том, что отдельные операции в вашем коде выносятся в специальные асинхронные методы и выполняются отдельно таким образом, чтобы ресурсы вашего приложения использовались максимально эффективно.
Синхронность vs Асинхронность
Синхронное выполнение задач
Чтобы продемонстрировать всю прелесть использования приемов асинхронного программирования в C# рассмотрим бытовой пример, с которым мы с вами могли встречаться ни один раз, будучи студентами ВУЗов и колледжей — работу частной мелкой типографии. Ситуация: в офисе один работник (Вы). У вас есть в распоряжении плоттер, черно-белый и цветной принтер, ну и по мелочи всякие типографские штуки. На дворе, допустим, конец мая (время сессии). Забегает к вам студент с целой стопкой распечатанных листов формата А4 и флэшкой и выдает вам такой заказ:
- На флэшке три файла — (1) чертеж на формате А1. Его надо распечатать на плоттере
- (2) Документ Word — его надо распечатать в черно-белом цвете на А4
- (3) Документ PDF с кучей картинок — его надо распечатать в цвете на формате А3
- Распечатанные листы сложить вместе с теми, которые принес студент
- Сброшюровать все листы A4
Итого, получаем пять задач. Как будет действовать в этом случае «однозадачный» человек? Он начнет выполнять задачи синхронно — сначала распечатает одно, потом другое, третье, потом сложит все листы А4 в кучку и, сброшюрует и, наконец-то, отдаст заказ. Посмотрим как эти задачи мы бы смоделировали в C#:
using System;
using System.Threading.Tasks;
using System.Diagnostics;
namespace Typography
{
//Классы для примера. Они не несут в себе никакой ценной идеи и сделаны просто для демонстрации процесса
internal class ListsA1 { } //распечатаные листы А1
internal class ListsA3 { } //распечатаные листы А3
internal class ListsA4 { } //распечатаные листы А4
internal class ListsHeap { } //собранная стопка бумаги
internal class Booklet { } //готовая брошюра формата A4
class Program
{
//методы (рабочие операции работника типографии)
//Распечатка листов формата А1
private static ListsA1 PrintListA1(int count)
{
Console.WriteLine("Включаем плоттер");
Console.WriteLine("Ждем несколько секунд...");
Task.Delay(3000).Wait();
Console.WriteLine("Печатаем формат А1 на плоттере");
for (int i = 0; i < count; i++)
{
Console.WriteLine($"Распечатали {i+1} лист А1");
Task.Delay(1000);
}
Console.WriteLine("Аккуратно сворачиваем листы А1");
Task.Delay(3000).Wait();
Console.WriteLine("Распечатка листов формата А1 закончена");
return new ListsA1();
}
private static ListsA3 PrintListA3(int count)
{
Console.WriteLine("Включаем цветной принтер");
Console.WriteLine("Ждем несколько секунд...");
Task.Delay(3000).Wait();
Console.WriteLine("Печатаем формат А3 на цветном принтере");
for (int i = 0; i < count; i++)
{
Console.WriteLine($"Распечатали {i+1} лист А3");
Task.Delay(500);
}
Console.WriteLine("Аккуратно сворачиваем листы А3");
Task.Delay(3000).Wait();
Console.WriteLine("Распечатка листов формата А3 закончена");
return new ListsA3();
}
private static ListsA4 PrintListA4(int count)
{
Console.WriteLine("Включаем черно-белый принтер");
Console.WriteLine("Ждем несколько секунд...");
Task.Delay(3000).Wait();
Console.WriteLine("Печатаем формат А4 на черно-белом принтере");
for (int i = 0; i < count; i++)
{
Console.WriteLine($"Распечатали {i + 1} лист А4");
Task.Delay(100);
}
Console.WriteLine("Аккуратно складываем листы в стопку");
Task.Delay(3000).Wait();
Console.WriteLine("Распечатка листов формата А4 закончена");
return new ListsA4();
}
private static ListsHeap CreateHeap(ListsA4 lists)
{
Console.WriteLine("Берем все листы A4");
Console.WriteLine("Аккуратно складываем вместе...");
Task.Delay(5000).Wait();
Console.WriteLine("Стопка бумаги готова. Можно брошюровать");
return new ListsHeap();
}
private static Booklet CreateBooklet(ListsHeap heap)
{
Console.WriteLine("Берем стопку листов A4");
Console.WriteLine("Брошюруем...");
Task.Delay(5000).Wait();
Console.WriteLine("Брошюра готова");
return new Booklet();
}
static void Main(string[] args)
{
Stopwatch stopwatch= Stopwatch.StartNew();
ListsA1 listsA1 = PrintListA1(2); //печатаем два листа A1
Console.WriteLine("------Задача по распечатке листов А1 выполнена------");
ListsA3 listsA3 = PrintListA3(4); //печатаем четыре листа A3
Console.WriteLine("------Задача по распечатке листов А3 выполнена------");
ListsA4 listsA4 = PrintListA4(10);//печатаем 10 листов A4
Console.WriteLine("------Задача по распечатке листов А4 выполнена------");
ListsHeap heap = CreateHeap(listsA4); //складываем все листы вместе
Console.WriteLine("------Стопка листов А4 готова------");
Booklet booklet = CreateBooklet(heap);//собираем брошюру
Console.WriteLine("------Листы А4 сброшюрованы------");
Console.WriteLine("------Поздравляем с успешно выполненным первым заказом!------");
stopwatch.Stop();
Console.WriteLine($"Заказ выполнен за {stopwatch.Elapsed.TotalSeconds} секунд");
}
}
}
Каждый метод, относящийся к распечатке документа — PrintListA1, PrintListA3, PrintListA4 эмулирует работу с оборудованием — мы включаем принтер/плоттер, ждем пока он проведет самодиагностику, потом печатаем. Каждый распечатанный лист тратит наше драгоценное время по-разному — дольше всех печатается формат А1, быстрее всех — А4. Дальше идут два метода, которые используют результаты предыдущих работ: в метод CreateHeap мы должны передать все распечатанные листы А4, чтобы собрать стопку бумаги, а в метод CreateBooklet — мы передаем аккуратно сложенную стопку бумаги, чтобы собрать из этой стопки буклет. Все эти операции мы выполняем последовательно в методе Main(). Результат работы такой программы будет следующим:
Ждем несколько секунд…
Печатаем формат А1 на плоттере
Распечатали 1 лист А1
Распечатали 2 лист А1
Аккуратно сворачиваем листы А1
Распечатка листов формата А1 закончена
——Задача по распечатке листов А1 выполнена——
Включаем цветной принтер
Ждем несколько секунд…
Печатаем формат А3 на цветном принтере
Распечатали 1 лист А3
……….
Распечатали 4 лист А3
Аккуратно сворачиваем листы А3
Распечатка листов формата А3 закончена
——Задача по распечатке листов А3 выполнена——
Включаем черно-белый принтер
Ждем несколько секунд…
Печатаем формат А4 на черно-белом принтере
Распечатали 1 лист А4
Распечатали 2 лист А4
…….
Распечатали 9 лист А4
Распечатали 10 лист А4
Аккуратно складываем листы в стопку
Распечатка листов формата А4 закончена
——Задача по распечатке листов А4 выполнена——
Берем все листы A4
Аккуратно складываем вместе…
Стопка бумаги готова. Можно брошюровать
——Стопка листов А4 готова——
Берем стопку листов A4
Брошюруем…
Брошюра готова
——Листы А4 сброшюрованы——
——Поздравляем с успешно выполненным первым заказом!——
Заказ выполнен за 28,0748336 секунд
Итого, мы затратили на всё про всё почти 30 секунд виртуального времени. Многовато. Каждая задача выполняется синхронно и пока очередная задача не будет выполнена мы не можем перейти к следующей и, в итоге, время задач у нас суммируется.
Асинхронное выполнение задач
В таком виде, как представлено выше, код блокирует выполняющий (главный) поток, не позволяя выполнять другие действия. Выполнение такого кода не будет прервано до тех пор пока все операции не будут выполнены. Это, согласитесь, не самый оптимальный вариант написания приложений — фактически, наш процессор простаивает впустую, не выполняя никакой работы, пока мы «печатаем». Это все равно, что стоять и смотреть на принтер пока из него лезут распечатанные листы и, при этом, игнорировать всё и всех вокруг. Попробуем изменить наш код так, чтобы он выполнялся асинхронно. Ключевое слово await позволяет обойтись без блокировки главного потока для запуска задачи, а затем продолжить выполнение, когда задача завершается.
Перепишем наш пример. Ниде представлены только измененные участки кода:
private static async Task<ListsA1> PrintListA1Async(int count)
{
Console.WriteLine("Включаем плоттер");
Console.WriteLine("Ждем несколько секунд...");
await Task.Delay(3000);
Console.WriteLine("Печатаем формат А1 на плоттере");
for (int i = 0; i < count; i++)
{
Console.WriteLine($"Распечатали {i+1} лист А1");
await Task.Delay(1000);
}
Console.WriteLine("Аккуратно сворачиваем листы А1");
await Task.Delay(3000);
Console.WriteLine("Распечатка листов формата А1 закончена");
return new ListsA1();
}
private static async Task<ListsA3> PrintListA3Async(int count)
{
Console.WriteLine("Включаем цветной принтер");
Console.WriteLine("Ждем несколько секунд...");
await Task.Delay(3000);
Console.WriteLine("Печатаем формат А3 на цветном принтере");
for (int i = 0; i < count; i++)
{
Console.WriteLine($"Распечатали {i+1} лист А3");
await Task.Delay(500);
}
Console.WriteLine("Аккуратно сворачиваем листы А3");
await Task.Delay(3000);
Console.WriteLine("Распечатка листов формата А3 закончена");
return new ListsA3();
}
private static async Task<ListsA4> PrintListA4Async(int count)
{
Console.WriteLine("Включаем черно-белый принтер");
Console.WriteLine("Ждем несколько секунд...");
await Task.Delay(3000);
Console.WriteLine("Печатаем формат А4 на черно-белом принтере");
for (int i = 0; i < count; i++)
{
Console.WriteLine($"Распечатали {i + 1} лист А4");
await Task.Delay(100);
}
Console.WriteLine("Аккуратно складываем листы в стопку");
await Task.Delay(3000);
Console.WriteLine("Распечатка листов формата А4 закончена");
return new ListsA4();
}
Метод Main
static async Task Main(string[] args)
{
Stopwatch stopwatch= Stopwatch.StartNew();
ListsA1 listsA1 = await PrintListA1Async(2); //печатаем два листа A1
Console.WriteLine("------Задача по распечатке листов А1 выполнена------");
ListsA3 listsA3 = await PrintListA3Async(4); //печатаем четыре листа A3
Console.WriteLine("------Задача по распечатке листов А3 выполнена------");
ListsA4 listsA4 = await PrintListA4Async(10);//печатаем 10 листов A4
Console.WriteLine("------Задача по распечатке листов А4 выполнена------");
ListsHeap heap = CreateHeap(listsA4); //складываем все листы вместе
Console.WriteLine("------Стопка листов А4 готова------");
Booklet booklet = CreateBooklet(heap);//собираем брошюру
Console.WriteLine("------Листы А4 сброшюрованы------");
Console.WriteLine("------Поздравляем с успешно выполненным первым заказом!------");
stopwatch.Stop();
Console.WriteLine($"Заказ выполнен за {stopwatch.Elapsed.TotalSeconds} секунд");
}
Что здесь появилось нового. Во-первых, у методов Print... появилось ключевое слово async. Это ключевое слово означает, что внутри метода могут встречаться асинхронные операции. То есть, наличие async в определении метода совсем не гарантирует, что он автоматически станет выполняться асинхронно, асинхронным метод можно будет назвать только в том случае, если в теле метода встретится хотя бы один раз другое ключевое слово — await. Оператор await в методе говорит о том, что операцию будет выполняться асинхронно и выполнение этой операции не будет блокировать главный поток. В нашем случае, асинхронно будут выполняться задержки времени (Task.Delay()) Во-вторых, в названии методов добавился суффикс Await , например, PrintListA3Async. По большому счёту, это не обязательное требование, но признак хорошего тона. Так мы можем сообщить другому программисту, что перед нами асинхронный метод.
Отдельно стоит отметить, что async/await используются в паре. Причем, если вы используете только async, то компилятор C# вас просто предупредит, что метод все равно будет выполняться синхронно, так как не обнаружено ни одного await. А вот await вы уже отдельно от async использовать не сможете вообще — это приведет к ошибке вида:
Ну и, в-третьих, обратите внимание на метод Main — он теперь тоже стал асинхронным, так как внутри него мы планируем вызывать асинхронные методы. Проверим, как сработает теперь наша типография:
Ждем несколько секунд…
Печатаем формат А1 на плоттере
Распечатали 1 лист А1
Распечатали 2 лист А1
Аккуратно сворачиваем листы А1
Распечатка листов формата А1 закончена
——Задача по распечатке листов А1 выполнена——
Включаем цветной принтер
Ждем несколько секунд…
Печатаем формат А3 на цветном принтере
Распечатали 1 лист А3
……
Распечатали 4 лист А3
Аккуратно сворачиваем листы А3
Распечатка листов формата А3 закончена
——Задача по распечатке листов А3 выполнена——
Включаем черно-белый принтер
Ждем несколько секунд…
Печатаем формат А4 на черно-белом принтере
Распечатали 1 лист А4
Распечатали 2 лист А4
Распечатали 3 лист А4
…….
Распечатали 10 лист А4
Аккуратно складываем листы в стопку
Распечатка листов формата А4 закончена
——Задача по распечатке листов А4 выполнена——
Берем все листы A4
Аккуратно складываем вместе…
Стопка бумаги готова. Можно брошюровать
——Стопка листов А4 готова——
Берем стопку листов A4
Брошюруем…
Брошюра готова
——Листы А4 сброшюрованы——
——Поздравляем с успешно выполненным первым заказом!——
Заказ выполнен за 33,2608234 секунд
Обращаем внимание на результат — время выполнения операции не только не сократилось, но еще и стало больше! Возникает логичный вопрос: зачем тогда мы городили все эти async/await если стало только хуже? На самом деле хуже не стало. Стало лучше и вот по какой причине — при выполнении задач главный поток приложения не блокировался. Задачи выполнялись отдельно и, поэтому, мы могли бы выполнять другие задачи. Конечно, в консольном приложении этот эффект от использования async/await практически не заметен, но попробуйте переписать приложение, используя графический интерфейс и убедитесь, что без асинхронности ваше приложение «зависнет». Ну, а увеличение времени связано всё же с накладными расходами использования Task.
Для некоторых приложений такого подхода бывает достаточно, чтобы «оживить» приложение. Возвращаясь к аналогии с типографией, теперь мы всё ещё вынуждены ждать пока очередной принтер выполнит свою работу, но уже можем не смотреть на него, а общаться с окружающими, успокаивать, что скоро всё будет готово. Но, нам этого мало — нам надо стать самыми быстрыми работниками типографии. Поэтому попробуем ускориться и выполнять часть работ одновременно.
Одновременный запуск асинхронных задач
Опять же, по аналогии с нашим примером: в реальной ситуации мы бы вряд ли ждали пока все принтеры распечатают все листы и только потом бы начали собирать брошюру. Мы бы сделали так: запустили бы сразу все печатные устройства, затем дождались бы пока отработает черно-белый принтер и, не дожидаясь плоттера и цветного принтера начали бы собирать брошюру. Так мы выполнили бы заказ быстрее. Давайте так и сделаем в нашем примере. Перепишем метод Main следующим образом:
static async Task Main(string[] args)
{
Stopwatch stopwatch= Stopwatch.StartNew();
Task<ListsA1> listA1_Task = PrintListA1Async(2); //запускаем принтер в работу (создаем задачу)
Task<ListsA3> listsA3_Task = PrintListA3Async(4);//запускаем принтер в работу (создаем задачу)
Task<ListsA4> listsA4_Task = PrintListA4Async(10);//запускаем принтер в работу (создаем задачу)
//листы А4 нам нужны для стопки и брошюрования
ListsA4 listsA4 = await listsA4_Task; //ждем пока отработает черно-белый принтер
Console.WriteLine("------Задача по распечатке листов А4 выполнена------");
ListsHeap heap = CreateHeap(listsA4); //складываем все листы вместе
Console.WriteLine("------Стопка листов А4 готова------");
Booklet booklet = CreateBooklet(heap);//собираем брошюру
Console.WriteLine("------Листы А4 сброшюрованы------");
//эти задачи пусть выполняются отдельно - они нам не мешают возиться с листами А4
ListsA1 listsA1 = await listA1_Task;
Console.WriteLine("------Задача по распечатке листов А1 выполнена------");
ListsA3 listsA3 = await listsA3_Task;
Console.WriteLine("------Задача по распечатке листов А3 выполнена------");
Console.WriteLine("------Поздравляем с успешно выполненным первым заказом!------");
stopwatch.Stop();
Console.WriteLine($"Заказ выполнен за {stopwatch.Elapsed.TotalSeconds} секунд");
}
}
Собственно, в комментариях к коду все сказано — мы создали несколько задач, затем одну задачу подождали, так как от её результата зависит выполнение двух синхронных операций, а выполнение остальных задачи, так как они оказались достаточно продолжительными, вынесли в конец метода — пусть работают и не мешают. Вот какой результат мы в итоге получим:
Ждем несколько секунд…
Включаем цветной принтер
Ждем несколько секунд…
Включаем черно-белый принтер
Ждем несколько секунд…
Печатаем формат А1 на плоттере
Распечатали 1 лист А1
Печатаем формат А3 на цветном принтере
Распечатали 1 лист А3
Печатаем формат А4 на черно-белом принтере
Распечатали 1 лист А4
Распечатали 2 лист А4
Распечатали 3 лист А4
Распечатали 4 лист А4
Распечатали 5 лист А4
Распечатали 2 лист А3
Распечатали 6 лист А4
Распечатали 7 лист А4
Распечатали 8 лист А4
Распечатали 9 лист А4
Распечатали 10 лист А4
Распечатали 2 лист А1
Распечатали 3 лист А3
Аккуратно складываем листы в стопку
Распечатали 4 лист А3
Аккуратно сворачиваем листы А1
Аккуратно сворачиваем листы А3
Распечатка листов формата А4 закончена
——Задача по распечатке листов А4 выполнена——
Берем все листы A4
Аккуратно складываем вместе…
Распечатка листов формата А1 закончена
Распечатка листов формата А3 закончена
Стопка бумаги готова. Можно брошюровать
——Стопка листов А4 готова——
Берем стопку листов A4
Брошюруем…
Брошюра готова
——Листы А4 сброшюрованы——
——Задача по распечатке листов А1 выполнена——
——Задача по распечатке листов А3 выполнена——
——Поздравляем с успешно выполненным первым заказом!——
Заказ выполнен за 17,1642155 секунд
Обратите внимание на то, что принтеры работали одновременно и не мешали друг другу, а вот собирать брошюру мы начали только после того, как были распечатаны все листы А4. Результат — время выполнения заказа сократилось с 28 секунд в синхронном режиме до 17 секунд в асинхронном. Здесь мы запустили асинхронные задачи, но не стали ожидать их результата, а сразу перешли далее. В итоге, выполнение кода у нас остановилось на строке: ListsA4 listsA4 = await listsA4_Task; //ждем пока отработает черно-белый принтер То есть, задачи listA1_Task, listA3_Task и listA4_Task продолжили выполняться в отдельных потоках, но мы указали оператором await, что необходимо дождаться результата одной из задач. И мы его дождались. Далее выполнились два синхронных метода и пока мы выполняли эти методы у нас отработали задачи listA1_Task и listA3_Task (обратите внимание на лог программы). Когда мы собрали брошюру (то есть отработал синхронный метод CreateBooklet) мы вернулись к принтеру и плоттеру и забрали распечатки — снова выполнили await, но так как на этот момент времени задачи уже выполнили свою работу, то результат был получен практически мгновенно.
Вот таким способом мы добились асинхронности выполнения кода и ускорили его выполнение.
Преимущества и недостатки асинхронного программирования
Какие можно сделать предварительные выводы по поводу асинхронного программирования.
| ЗА асинхронность | ПРОТИВ асинхронности |
| асинхронность позволяет ускорить обработку длительных операций, запустив их выполнение одновременно | использование async/await имеет свои накладные расходы, поэтому использовать асинхронность стоит там, где это действительно необходимо (в операциях ввода-вывода, при работе с большими файлами и т.д.). По незнанию можно легко «затормозить» время выполнения работы программой, хотя интерфейс «виснуть» не будет |
| В отличие от чистой многопоточности асинхронные методы писать, на мой взгляд, по-проще и быстрее | Читать код с async/await сложнее, чем обычный синхронный код |
Итого
Сегодня мы познакомились с ещё одной возможностью C# — асинхронным программированием на основе задач (Task). Естественно, что такую сложную и интересную тему невозможно подробно рассмотреть в рамках одной публикации, однако сегодня мы узнали, что задачи можно выполнять последовательно или одновременно. Для того, чтобы метод был асинхронным он должен: а) содержать ключевое слово async в определении; б) в теле метода должна быть операция с оператором await, который приостанавливает вычисление асинхронного метода до завершения асинхронной операции. Наличие только async не делает метод асинхронным.