[БЕЗ_ЗВУКА] В этой серии видео уроков мы рассмотрим асинхронное программирование и тот подход, в котором оно реализовано в Go. Для начала определение. Что такое вообще асинхронное программирование? Асинхронное программирование — это когда операции вашей программы выполняются не строго последовательно, а могут быть прерваны какими-то другими операциями вашей же программы. Самым известным примером асинхронного программирования является технология Ajax — асинхронный JavaScript и XML, когда во время запроса на сервер ваша страница не замораживается, а продолжает работать. Давайте теперь начнем переходить к тому, почему вообще асинхронное программирование появилось и почему оно эффективно. Прежде чем мы начнем рассматривать то, каким образом реализована обработка запроса в современном веб-сервере, давайте просмотрим одно понятие в процессоре, которое важно в этом случае. Итак, для начала память. Скорость современного процессора гораздо больше, чем скорость оперативной памяти. Для того чтобы как-то компенсировать это, был введен кэш процессора, который располагается вместе с ним на одном кристалле. Это очень быстрая память, но она очень маленькая. Сначала был введен кэш первого уровня, потом второго, потом третьего, когда-нибудь, скорее всего, введут и четвертого уровня. После кэша идет уже оперативная память. То есть вот процессор, и в нем есть кэш, и он быстрый. Есть плашки памяти, которые являются оперативной памятью. Там память медленнее. Например, для того чтобы получить доступ из ядра процессора в основную оперативную память, может затребоваться до 100 наносекунд. С учетом количества операций, которые выполняет современный процессор, это достаточно много. Почему память важна? Память важна при рассмотрении такого понятия, как переключение контекста. Переключение контекста — что это такое? Процессор выполняет просто последовательно какие-то операции, он ничего не знает про какие-то другие программы. Переключением из одной программы в другую программу занимается планировщик задач. Что он делает? Он берёт один процесс, один тред, сохраняет его состояние куда-то. Потом берет состояние другого треда, загружает его в процессор, и начинается выполняться этот процесс. Причем здесь память? Дело в том, что при переключении задач, переключении процессов или системных тредов может возникнуть потребность в обращении к основной оперативной памяти, потому что в кэше данных для этого процесса нет. И нужно будет их подгрузить, то есть инвалидировать вообще весь процессорный кэш. Поэтому переключение контекста — операция достаточно дорогая. Мы можем затратить на нее до одной микросекунды в современных процессорах, что довольно много. Именно из-за переключения контекста, когда вы увеличиваете количество программ, которые выполняются на вашем компьютере, всё начинает работать медленнее. Этот подход к вытеснению одной программы другой, это называется вытесняющей многозадачностью. Теперь, помня и зная про переключение контекста, можно начать рассматривать уже методы обработки запросов в веб-сервере. Итак, начнем мы с технологии cgi-bin. Что это такое? cgi-bin — это когда на каждый запрос поднимается новая программа, создается новый процесс, это тяжелая операция, это нужно подключить довольно много памяти. Там выполняется какой-то запрос, и после этого программа убивается. Это может быть очень не эффективно, потому что при увеличении количества запросов мы начнем тратить много времени на создание и завершение процессов и можем банально упереться в количество оперативной памяти. Эволюцией этого подхода является worker pool, когда у нас есть некое количество процессов, которые не убиваются после завершения работы, а остаются в ожидании следующего запроса. Также есть подход, который называется мультитрединг. Это значит, что мы уже создаем не целый процесс на один запрос или одно соединение, а всего лишь тред. Тред — это более легкая сущность, чем процесс. Тред имеет доступ к памяти своего процесса, То есть вы можете переиспользовать какие-то соединения, например, к базе данных. Тред занимает меньше памяти, но для процессора это тоже системный тред, он тоже выполняется процессором, его тоже нужно переключать context switch. Таким образом, за счет хотя бы экономии памяти мы можем обработать большее количество запросов. Также эволюцией тут является то, что мы можем создать worker pool и обрабатывать запросы, не плодя бесконечно новые треды, а распределять запросы по фиксированному количеству. Может быть, это как-то можно ускорить? Для того чтобы понять это, давайте посмотрим внутрь запроса, что происходит внутри запроса сейчас. На самом деле оновное время на современном web api уходит на ожидание запроса от какой-то удаленной базы данных, от какого-то веб-сервиса. И в случае с подходом с процессами на запрос либо тредом на запрос получается так, что тред блокируется на ожидание этого ответа и не выполняет никакой другой полезной работы. Из понимания этого родился событийный подход к обработке запросов, который реализован, например, в JS, в JavaScript. Что это значит? Это значит, что у нас неблокирующий ввод-вывод. Когда мы отправили запрос в базу данных, мы на этом не блокируемся, мы продолжаем выполнять какие-то другие запросы, потому что процессор у нас бездействует. Таким образом, мы можем получить очень хорошую производительность. Мы можем обрабатывать много запросов внутри одного треда. Но тут есть нюансы. Какие нюансы? Дело в том, что, поскольку у нас тред один, то мы никак не можем выполнять параллельно запросы. В случае с вытесняющей многозадачностью, когда тред блокируется, какой-то другой тред работает. В случае с кооперативной многозадачностью в этом случае, то мы должны дождаться окончания работы запроса № 1 для того, чтобы выполнять запрос № 2, и это может быть плохо, если у нас много операций на ЦПУ. Например, мы считаем какие-то хеши, занимаемся шифрованием либо архивированием, потому что это тяжелая процессорная операция, и нам будет не хватать того времени, которое мы проводим в ожидании ответов от базы данных. Нам будет не хватать для обработки всех операций, всех запросов. Соответственно, хочется как-то разнести это на несколько ядер, для того чтобы, пока один тред занят, мы могли выполнять что-то в другом треде. То есть размасштабироваться. И именно такой подход реализован в Go. Основан он на модели, которая называется communicating sequential processes от Тони Хоара, и оперируем мы в этом подходе такой сущностью, как горутина. Горутина — это аналог сопрограммы, когда в одном системном треде может выполняться несколько горутин, несколько сопрограмм. При этом особенностью является то, что наша горутина может начать выполняться на одном треде, потом уйти в ожидание данных из базы и продолжить выполняться в другом системном треде, потому что первый системный тред занят уже какой-то другой горутиной. Этот подход позволяет получить очень хорошую производительность, очень хорошую пропускную способность. Это является одним из ключевых особенностей языка Go и одной из самых сильных его сторон. А теперь давайте рассмотрим, как это выглядит в коде.