Код
#статьи

Асинхронное программирование. Часть 1: как работает процессор

Процессоры могут выполнять программы асинхронно. Объясняем, как это происходит, зачем нужно и что значит для программирования.

vlada_maestro / shutterstock

Асинхронность, многопоточность и параллелизм — очень важные аспекты программирования, без которых сложно представить современные компьютеры.

Язык программирования C# предоставляет разработчикам множество инструментов для создания приложений, которые могут работать с большим количеством потоков, выполняя их параллельно и асинхронно. Это одна из причин популярности этого языка, а также главная причина, почему именно он выбран для этой серии статей.

Сразу разобраться в этих концепциях сложно, поэтому мы начнём с небольшого ликбеза о том, как компьютерные процессоры выполняют операции.

ВНИМАНИЕ!

Информация в следующих разделах сильно упрощена.

Как работает процессор

За единицу времени (она называется тик) процессор может выполнить только одну задачу — например, прочитать значение ячейки памяти. Поэтому на то, чтобы выполнить следующие операции, потребуется 4 тика:

  1. Прочитать данные из двух ячеек.
  2. Сложить их.
  3. Записать результат в другую ячейку.

Это касается только атомарных операций, то есть самых маленьких и неделимых. Более сложные задачи могут состоять из нескольких атомарных. Например, чтобы провести умножение числа A на число B, нужно будет прибавить к числу A само себя B — 1 раз.

5 * 5 = 5 + 5 + 5 + 5 + 5

Ещё больше тиков потребуется на деление, а если операцию нужно провести над числами с плавающей запятой, то даже представить страшно, сколько их нужно.

Прямо сейчас у вас могут быть открыты десяток вкладок в браузере, плеер, мессенджер, редактор кода и ещё много всего. Поэтому кажется странным, что ничего из этого на самом деле не работает одновременно.

Несмотря на то что процессор выполняет только одну задачу за один раз, инженеры и программисты нашли способ распределять время его работы так, чтобы он тратил немного времени на одну задачу, потом переключался на другую и так далее.

В этом им помогло то, что за одну секунду процессор может выполнять огромное количество операций, поэтому человек не замечает, что они все выполняются по очереди.

Количество тиков измеряется в герцах (Гц) — это единица измерения частоты протекания периодических процессов. Например, если мимо вашего дома раз в секунду проезжает гоночный болид, то его частота будет равна 1 Гц. Если болид проезжает два раза в секунду, то его частота — 2 Гц; если он проезжает трижды, то давно пора подумать о переезде.

Процессор так быстро выполняет процессы, что его частота измеряется в гигагерцах.

1 ГГц = 1 000 000 000 Гц

Частота современных процессоров обычно равна 2-3 ГГц.

Как процессор выполняет программы

Каждая программа состоит из множества процессов: нужно 500 раз провести сложение, записать данные в 2000 ячеек, перемножить всё это, а потом, наконец, поделить.

Если выполнять всё это линейно, то программы станут очень неудобными. Например, когда вы будете нажимать на кнопку скачивания в браузере, ваш компьютер будет блокироваться до тех пор, пока скачивание не завершится. Хорошо, если вы хотя бы будете видеть, сколько процентов загрузилось.

Тогда код программы будет выглядеть примерно так:

Выполнять цикл, пока есть пакеты для скачивания:
	Загрузить пакет во временное хранилище;
	Перенести его из временного хранилища в ячейку по адресу X;
	Посчитать, какой процент пакетов загружен;
	Обновить полосу загрузки;
Конец цикла.

Пока выполняется этот цикл, вы не сможете сделать ничего, что не прописано внутри него. И, как видите, здесь нет строчек вроде проигрывать музыку или листать ленту. Более того, даже строчек для отслеживания движений мышью нет.

Поэтому, чтобы компьютерами было удобно пользоваться, мы делим все выполняемые программы на потоки. Допустим, у нас запущено 10 программ, а процессор работает на скорости 100 тиков в секунду.

Тогда каждый поток получит по 10 тиков. То есть процессор будет в течение 10 тиков выполнять инструкции от одного потока, а потом переходить к инструкциям другого — и так по кругу. Также у каждого потока есть приоритет: более важные программы будут получать больше тиков.

В вашей операционной системе каждая запущенная программа выполняется в отдельном потоке. Это происходит автоматически, потому что так спроектированы ОС. Но когда вы пишете программу, вы также можете создавать новые потоки: это позволит вам делать приложения удобнее для пользователей.

Когда нужна асинхронность

Чаще всего асинхронность нужна в программах с графическим интерфейсом. В них основная логика и работа с изображением помещены в разные потоки. Поэтому, когда логика занята, вы всё ещё можете пользоваться приложением.

Если же всё это выполняется в одном потоке, то приложение будет подвисать, когда выполняется сложная инструкция. В ОС Windows часто можно заметить, что, когда приложение что-то делает, а вы кликаете на него, то в заголовке окна можно увидеть словосочетание «Не отвечает».

Это не всегда означает, что приложение зависло. Возможно, оно просто занято решением какой-то сложной задачи, которая выполняется в том же потоке.

Пример асинхронного приложения

Чтобы лучше закрепить тему, давайте напишем приложение с использованием асинхронности. Оно будет разделено на два потока (Thread): первый будет обновлять полосу загрузки, а второй — выполнять саму загрузку.

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Text;

namespace Async
{
    class Program
    {

   	 static int full = 100;
   	 static int completed = 0;
   	 static int state = 0;

   	 static char[] cursors = new char[] { '-', '/', '|', '\\' };

   	 static void Main(string[] args)
   	 {
   		 LoadAsync(); //Старт загрузки
   		 UpdateLoading(); //Старт обновления полосы загрузки

   		 Console.ReadKey();

   		 Console.WriteLine();
   	 }

   	 static void UpdateLoading() //Этот метод каждые 100 миллисекунд будет стирать старую полосу загрузки, а потом выводить новую
   	 {

   		 while(completed <= full)
   		 {
   			 Console.Clear();

   			 state++;

   			 if(state == 4)
   			 {
   				 state = 0;
   			 }

   			 string loadingBar = GetLoadingString();
   			 Console.WriteLine(loadingBar + " " + cursors[state]);

   			 Thread.Sleep(100);


   		 }
   	 }

   	 static string GetLoadingString() //Метод, который создаёт текстовую полосу загрузки
   	 {

   		 StringBuilder loadingBar = new StringBuilder("[");

   		 for(int i = 0; i <= full; i++)
   		 {
   			 if(i < completed)
   			 {
   				 loadingBar.Append("#");
   			 }
   			 else
   			 {
   				 loadingBar.Append(".");
   			 }
   		 }

   		 loadingBar.Append($"] {completed} %");

   		 return loadingBar.ToString();
   	 }

	 static async void LoadAsync() //Асинхронный метод, который создаёт поток для выполнения метода Load() 
   	 {
   		 await Task.Run(()=>Load()); //Метод ожидает выполнения метода Load()
   	 }

   	 static void Load() //Метод загрузки, который каждые 500 миллисекунд прибавляет к значению completed единицу
   	 {
   		 for(int i = 0; i <= full; i++)
   		 {
   			 completed++; 
   			 Thread.Sleep(500);
   		 }
   	 }
    }
}

В следующих статьях вы узнаете, как всё устроено, а пока посмотрим, как это работает:

Здесь можно заметить, что курсор обновляется чаще (каждые 100 мс), чем проценты (каждые 500 мс), как и было записано в коде программы.

Заключение

Асинхронность, многопоточность и параллелизм — очень мощные и полезные инструменты, которые каждый день делают нашу жизнь лучше. Однако они хранят в себе массу опасностей, которые могут стать причиной катастрофы (буквально), — этому будет посвящена отдельная статья серии.

Учитывая сложность этих инструментов, понадобится не одна статья, чтобы их объяснить. Я постараюсь не только показать, как их использовать, но и помочь на более глубоком уровне понять принципы их работы.

Изучайте IT на практике — бесплатно

Курсы за 2990 0 р.

Я не знаю, с чего начать
Научитесь: Профессия Python-разработчик Узнать больше
Понравилась статья?
Да

Пользуясь нашим сайтом, вы соглашаетесь с тем, что мы используем cookies 🍪

Ссылка скопирована