Это статья про программирование, в которой с примерами посмотрим в чём отличие между асинхронностью, многопоточностью и параллельностью. Примеры написаны на языке C# , а сама статья будет полезна начинающим программистам и студентам. В статье также рассматриваются примеры использования делегатов и лямбд.
Если Вы хотите научиться создавать программы для Windows, но совсем не знаете с чего начать, то с этой статьи.
Асинхронность
Для того чтобы разобраться с параллельной и асинхронной работой напишем программу, которая проверяет доступность сетевых адресов в интернете (пингер). Для этого сделаем список адресов: Яндекс, Гугл, Мэйл.ру, Госуслуги, Сбербанк и ещё из десятка адресов в интернете. Затем в цикле проверим их доступность с помощью метода Send класса Ping.
Ping pingSender = new Ping();
foreach (string address in richTextBox1.Lines){
PingReply reply = pingSender.Send(address);
richTextBox2.AppendText(reply.RoundtripTime+"\n");}
До тех пор пока всё не проверится программа не отвечает, а окно программы "зависает", а если ресурс недоступен, то зависает весьма надолго. Форма обновляется только после окончания одного запроса - перед вызовом следующего. В итоге мы получаем ответы в том порядке в котором отправили запросы.
Если использовать метод SendAsync, то всё работает как надо: все пули вылетают одновременно, а ответы приходят в порядке увеличения времени отклика.
Ping pingSender = new Ping();
foreach (string address in richTextBox1.Lines){
pingSender.PingCompleted += new PingCompletedEventHandler(PingCompletedCallback);
pingSender.SendAsync(address);}
...
private void PingCompletedCallback(object sender, PingCompletedEventArgs e) {
PingReply reply = e.Reply;
richTextBox2.AppendText(reply.RoundtripTime+"\n");}
Если все адреса представлены доменными именами, либо все адреса представлены IP-адресами, то в можно запрашивать не в цикле, а передавать в метод SendAsync сразу массив адресов.
Обратите внимание, что метод SendAsync не возвращает объект типа PingReply, в котором мы смотрели результат нашего запроса методом Send. Вместо этого метод SendAsync ждёт удачного или не удачного результата и вызывает событие PingCompleted. Чтобы посмотреть его результат, надо воспользоваться делегатом PingCompletedEventHandler, в который PingCompleted передаёт параметры события, в том числе и PingReply. Если вы ещё не взяли на вооружение делегаты, то настоятельно рекомендую ознакомиться с этой страницей учебника.
Когда основная работа выполняется на стороннем компьютере, такое называют асинхронностью. В этом примере основное время работы тратилось на путешествие запросов по проводам, а за ресурсы текущего компьютера конкурировали только процессы, которые ждали ответа.
При работе с классами пространства имён System.Net можно держать в уме, что ко многим методам можно добавить "Async", чтобы получить асинхронную версию. Например WebRequest.GetResponseAsync возвращает объект типа Task<WebResponse>.
То есть можно пока ждём ответа сообщить пользователю:
resultLabel.Text = "ожидайте окончания запроса";
response = await request.GetResponseAsync();
А затем в этот же resultLabel добавить то что нужно и з потока response. Пользователю будет намного приятнее смотреть на такое, чем на зависшее окно программы. А чтобы было ещё приятнее не забывайте брать код с await в блок try, а затем в конце блока finaly не забывайте response.Close(); чтобы очистить память в случае, если ответ окажется большим.
Многопоточность, параллельность и класс Task
Когда разные операции выполняются на одном ядре процессора их выделяют в потоки. У потоков есть приоритет выполнения, а распределением ресурсов управляет операционная система. Скорость выполнения одновременно двух задач в разных потоках при 100% загрузке этого ядра будет даже немного больше, чем последовательного при тех же 100% загрузки. Идеальный случай когда операции запускаются на разных ядрах процессора и между ними нет конкуренции за ресурсы называется параллельностью. Примерами по-настоящему параллельной программы являются гипервизоры.
Использование метода SendAsync сэкономило время на получение ответа и позволило независимо начать обрабатывать результат запроса. Но если сама обработка будет занимать значительное время, тогда надо будет перейти к параллельности и многопоточности.
Самый удобный для программиста способ реализации параллельности (и многопоточности, когда не хватает свободных ядер процессора) на С# является использование классов Task и Task<TResult> пространства имён System.Threading.Tasks.
У класса Ping существует метод SendPingAsync, возвращающий ответ типа Task<PingReply>, отправляющий запросы параллельно (в разных потоках) и асинхронно.
Если просто написать PingReply reply = pingSender.SendPingAsync(address).Result; , то отличия для пользователя в работе от Send не будет. Из-за того что программа последовательно ждёт каждого ответа, она всё равно будет "зависать" до получения результата.
Для этого надо полученные ответы обрабатывать также либо асинхронно (Task'и выполняются в разных потоках, поэтому и обработка будет многопоточной асинхронной), либо многопоточно используя класс Thread пространства имён System.Threading. Рассмотрим оба варианта. Из примера станет понятно, почему рекомендация Microsoft когда всё равно что использовать - использовать Task'и.
Вариант1: обработка результатов в асинхронном методе.
Чтобы обработать результаты запросов многопоточно надо вызвать pingSender.SendPingAsync(address) не дожидаясь получения Result. А в объявление метода, который обрабатывает запросы, добавить модификатор async: private async void button1_Click(object sender, EventArgs e) . Асинхронный метод (с модификатором async) должен иметь в себе что-то, чего он ждёт. Например подождём пока не придёт последний ответ.
var Tasks = new List<Task<PingReply>>();
Ping pingSender = new Ping();
foreach (string address in richTextBox1.Lines)
Tasks.Add(pingSender.SendPingAsync(address));
...
await Task.WhenAll(Tasks);
foreach (Task<PingReply> t in Tasks)
richTextBox2.AppendText(t.Result.RoundtripTime+"\n");
Пока не придёт последний ответ ни один результат не будет выведен, но программа не "зависает", а значит пользователю можно показать ProgressBar или просто позволить ему делать в этой же программе что-то ещё.
Так как в подобных программах возможны различные баги связанные с работой с памятью и ошибки в ответах на запросы. Поэтому я рекомендую открывать блок try и сразу за ним вместо Ping pingSender = new Ping(); пользоваться using(Ping pingSender = new Ping()) .
Вариант 2: обработка результатов а отдельных потоках.
Идея в том, чтобы в цикле создавать поток, который будет запускать метод, в котором будет и отправка и обработка запроса. Это даже звучит громоздко, поэтому можно воспользоваться лямбдо-выражением (их также называют просто лямбдой или стрелочной функцией). Если вы ещё не взяли их на вооружение - обязательно ознакомитесь с этой страницей учебника.
foreach (string address in richTextBox1.Lines){
Ping pingSender = new Ping();
var th = new Thread(() => { MessageBox.Show(pingSender.SendPingAsync(address).Result.RoundtripTime.ToString()); });
th.Start();}
Использование разных потоков накладывает кучу ограничений и вызывает ещё больше багов, связанных с работой с памятью.
Во-первых нельзя использовать конструкцию using так как в ней проверяется существует ли этот объект и если ещё нет, то создаёт его. Но он может существовать в другом потоке, что вызовет ошибку. Закрывая блок using вызывался метод Dispose() , но в варианте с новыми потоками вся надежда на сборщик мусора. На всякий случай можно вызвать его вручную если запросы делаются какими-то большими группами, но это всё равно не рекомендуется Microsoft).
Во-вторых надо передавать данные между потоками (форма создаётся в другом потоке). Поэтому вместо вывода в richTextBox2 я использовал в примере MessageBox. Можно использовать модификатор static для вызывающего метода если вывод идёт не на элемент Windows.Forms.Control, иначе придётся воспользоваться делегатом Action.
Action action = () => richTextBox2.AppendText (pingSender.SendPingAsync(address).Result.RoundtripTime + "\n");
richTextBox2.Invoke(action);
Но тогда снова выполнение станет последовательным и снова придётся прибегнуть к способу с формированием списка ответов, а они типа Task<>, так что использование Thread в конкретно этой задаче совсем бессмысленно, но приём можно запомнить.
Использование Thread считается устаревшим (legacy), но к нему же придётся прибегнуть, если хочется, чтобы программа запускалась на Windows XP.
Вариант 3: параллельные циклы
В пространстве имён System.Threading.Tasks есть класс Parallel, в котором есть всего три метода: Invoke(Action[]) и циклы For и ForEach. Но они могут применяться в широком спектре задач.
Parallel.ForEach(richTextBox1.Lines, (string address) => {
using (Ping pingSender = new Ping())
MessageBox.Show(pingSender.SendPingAsync(address).Result.RoundtripTime + "\n"); });
В этот раз если использовать для вывода элемент Windows.Forms.Control приём с делегатом Action сработает как надо.
Параллельные задачи в Unity
Task'и являются рекомендуемым для многопоточности инструментом, но у них есть ощутимый минус: каждый запущенный Task занимает в памяти некоторый объём. С современными объёмами памяти в компьютерах и телефонах кажется, что это не является проблемой. Но представим себе игру, в которой симулируется город, где каждый житель каждый кадр спрашивает доделалось ли его дело, и каждая машина спрашивает приехала ли она.
В Unity (а большинство игр, написанных на C# , сделаны в Unity) для решения этой задачи используются корутины (coroutine), перекочевавшие в этот фреймворк из языка Java (но это не точно). Они представляют собой итераторы, которые "ставятся на паузу" до выполнения определённых условий, которые запускаются всегда в новом потоке. Например, машина просто меняет свою координату пока не приедет. Кстати, координату менять в Unity считается хорошим тоном тоже корутиной, а не циклом.
___________
В следующей статье из этого цикла рассмотрим библиотеки классов (DLL) и немного поговорим о комментариях к коду и решениях
#c# #.net #программирование #параллельность #Unity #многопоточность #асинхронность