Эта статья представляет собой краткий (шутка!) конспект одноименного (почти) вебинара, недавно проведенного автором. Цель и вебинара и статьи - дать начинающим представление о тех понятиях, которые вынесены в заголовок, чтобы помочь избежать повсеместной путаницы, сопровождающей эти темы. Ну и немного раскрыть глаза на то, что, оказывается в PHP есть и асинхронность, и многопоточность, и в общем-то не нужно ждать мифической версии PHP 10, чтобы начать их использовать уже прямо сейчас! Если кратко, то асинхронное выполнение кода - это возможность некий блок кода (иначе говоря, "задачу") выполнить не в заранее заданном порядке, а в порядке, который зависит от.  От чего? От внешних условий: от наступления определенного события или, к примеру, наступления момента времени. Самый простой пример, который можно привести, это, конечно, знаменитая функция setTimeout из JS (немного иронично, что в статье про PHP первый пример будет на языке JavaScript - но что уж поделать...): Пример, несмотря на очевидную простоту, полностью объясняет идею асинхронного исполнения кода: У нас есть задача - это функция, являющаяся первым аргументом setTimeout(); Мы определили условие, по которому эта задача будет отложенно выполнена - это наступление события "прошло 5 секунд"; Далее код выполняется синхронно, ровно в том порядке, в котором он и написан, пока не наступит ожидаемое событие; Наступление события активирует отложенную задачу - она выполняется. Возможно ли такое в PHP с использованием стандартного синтаксиса языка и стандартной библиотеки?  Нет. Всё дело в том, что PHP изначально не реализует так называемый "цикл обработки событий", или "Event Loop". Не реализует не потому, что PHP - плохой язык, а JS - хороший, тут вообще не применимы моральные оценки - а потому что PHP зачастую живет в другой парадигме. Как работает PHP, если опустить нюансы? Очень просто, "запрос" - "веб-сервер" - "процесс PHP" - "веб-сервер" - "ответ". И даже если опустить дурацкую поговорку про то, что "PHP рожден, чтобы умирать", всё равно понятно, что в режиме совместной работы с веб-сервером программа на PHP заинтересована в том, чтобы заканчивать свою работу как можно быстрее. Какие уж тут циклы событий, какая асинхронность - чем быстрее процесс отработает, тем быстрее клиент получит ответ! Но всё меняется, когда мы переходим от модели "веб-сервер + PHP" к написанию долгоиграющих консольных или, чем не шутит черт, GUI (а такие примеры уже есть) приложений на PHP. В этих случаях необходимость отложенного выполнения кода становится очевидной, ведь даже простейшая задача вроде "Если клиент нажал А, то..." становится асинхронной! Что делать? Придется написать Event Loop самим! Начнем. Предусмотрим перечисление с условными кодами событий: Добавим класс - репозиторий задач. Предусмотрим добавление задачи с указанием кода события, на который должна реагировать задача и получение всех задач по коду события. Как задел на будущее - укажем флаг bool $once- как указание на то, должна ли задача выполняться однократно, или многократно. Ну и при получении списка задач по событию, если задача была запланирована, как однократная - удалим ее из списка. Теперь сделаем класс, который будет заниматься генерацией событий. В качестве события будем рассматривать нажатие пользователем на определенную клавишу на клавиатуре. Если интересующая нас клавиша нажата - вернем код события, если нет - вернем null. Небольшой "хак" с функцией readline_callback_handler_install() требуется, чтобы заставить стандартный поток ввода отдавать нам каждый символ после нажатия соответствующей клавиши, не дожидаясь, когда пользователь нажмет "Enter". Ну и, наконец, добавим задачи и организуем сам Event Loop - то есть бесконечный цикл получения и обработки событий: Удовольствие запустить этот код и понаблюдать, что будет в ответ на нажатие соответствующих клавиш (внимание - регистр нижний, алфавит - латинский!) оставляю читателю :) Коротко: нет. Если рассмотреть встроенный, скажем в Symfony или Laravel или (это классический пример!) punBB механизм "событий", "уведомлений" и их "обработчиков" - может сложиться ложное впечатление, что всё это - асинхронное выполнение кода. На самом деле я глубоко убежден, что event-driven программирование на PHP в парадигме конечного процесса "запрос-работа-ответ" - это средство прежде всего запутать программиста, создав у него иллюзию, что он овладел волшебной асинхронностью. При том, что на самом деле он овладел искусством создания запутанной лапши вместо кода. Поэтому - нет. Event-driven это не про асинхронность, это архитектурный паттерн построения синхронных программ, со сложным и заранее непрогнозируемым потоком исполнения. Никакого цикла генерации и обработки событий этот паттерн не добавляет. Разумеется, всё сказанное выше не имеет отношения к распределенной асинхронности, о которой речь пойдет дальше. Впрочем, всё меняется, если вы раскладываете свой код на >=2 независимых сервиса и соединяете их некой "шиной" или "очередью" событий.  В качестве такой "шины событий" может выступать, к примеру, RabbitMQ или, скажем, встроенный в Redis механизм PUB/SUB. В таком случае мы действительно получаем настоящую асинхронность (ключевые для понимания моменты выделены): HTTP-сервис принимает запрос; Поняв, что бизнес-логика требует отложенного действия (например: отправки письма пользователю) HTTP-сервис создает в очереди событие, а сам продолжает выполняться дальше; Вспомогательный сервис, работая в cli и выполняя бесконечный цикл, получает из очереди уведомление о событии и выполняет связанную с ним задачу, а затем продолжает ждать следующее событие. Это - асинхронное выполнение кода. Пусть и ценой увеличения количества сервисов в приложении. Посмотрите, к примеру, как такой подход реализован в том же Laravel, где он называется "Queued Events". Говоря про Event Loop, невозможно не упомянуть о React PHP - пожалуй первой PHP-библиотеке, где этот паттерн был полноценно реализован. Пример из официальной документации весьма красноречиво описывает возможности React PHP: Чем не аналог setTimeout() ? Разумеется, одним только Even Loop не исчерпываются возможности React PHP. Он предоставляет много интересного: это и неблокирующие стримы ввода-вывода, и своя реализация промисов, и компоненты для работы с сетевыми соединениями.  Эти возможности уже не раз освещались в разных статьях, поэтому сейчас не будем на них останавливаться. Хорошо, предположим, что мы с вами в совершенстве освоили технику создания Event Loop и научились выполнять задачи отложенно. Но как быть, если задачи достаточно объемные? К примеру, задачей может быть чтение большого файла с данными, обработка этих данных и запись в базу. Поможет ли асинхронное исполнение оптимизировать производительность? Нет. Нам нужно найти какой-то способ разбивать крупные задачи на кванты и выполнять их "дискретно", чередуя выполнение квантов задач. К примеру - прочитали одну строку из файла (квант задачи №1), преобразовали эти данные в нужный вид (квант задачи №2), записали в базу (квант задачи №3), снова вернулись к чтению очередной строки из файла (следующий квант задачи №2). Если мы решим задачу квантования задач - мы получим в итоге так называемую "кооперативную многозадачность" или, иными словами, конкурентность. Кванты будут выполняться последовательно, конкурируя за процессорное время, но при этом возникнет иллюзия параллельного выполнения задач и неиллюзорная экономия ресурсов - ведь хранить в памяти и обрабатывать одну строчку из файла гораздо выгоднее, нежели прочесть весь файл целиком, а затем заняться им. Для такого "квантования" в PHP существует ряд языковых средств. Первое из них - генераторы и корутины. Попробуем решить задачу построчного чтения файла на генераторах: Как это работает? Генератор в PHP, если сильно упрощать, это - функция, которая: Как бы не совсем функция, хотя синтаксически очень на нее похожа. Вызов генератора, как функции, вернет нам объект класса Generator, реализующий интерфейс Iterator, с которым мы уже будем работать. Генератор умеет не просто возвращать одно значение (оператор return), но вместо этого генерировать последовательность: оператор yield выдает очередное значение последовательности. Сохраняет своё состояние, когда ее работа прервана оператором yield. Умеет продолжать работу с сохраненного состояния, когда работа генератора возобновлена вызовом метода Iterator::next() Первая задача представляет из себя генератор, который будет построчно читать некий файл и генерировать последовательность прочитанных строк. Использовать генератор можно с помощью цикла foreach (совместный цикл) или явно вызывая его методы: Однако, на этом возможности генераторов не исчерпываются. Мы можем не только получать от генератора очередные значения генерируемой им последовательности, но и передавать в генератор значения на каждом шаге! Для этого используется то же ключевое слово yield, но уже как выражение. Давайте попробуем перевести на язык генераторов вторую задачу: "Принять строку, преобразовать ее к верхнему регистру, выдать, как значение последовательности, ждать следующую строку": Такой генератор, который умеет принимать извне значения, называется "корутиной" или, по-русски, "сопрограммой". Полностью код, который будет использовать обе наши задачи, может теперь выглядеть так: Попробуйте записать какой-нибудь текст в тестовый файл и запустить этот код. Вы увидите, как первая задача-генератор читает очередную строку из файла, значение передается второй задаче-сопрограмме, и так строчка за строчкой, пока не закончится исходный файл. Мы реализовали с вами кооперативную многозадачность (конкурентность) - псевдопараллельное выполнение задач, основанное на том, что задача может выполнить квант работы, прервать сама себя, сохранив своё состояние и передать управление другой задаче. Разумеется, никакой настоящей параллельности здесь нет. Скажем, если первой задаче для кванта работы требуется одна секунда, второй задаче для своего кванта - тоже секунда, а всего таких квантов 100, в целом программа будет выполняться минимум 200 секунд. Мы выигрываем лишь в ресурсах (в памяти) и в возможности прервать работу, оставив ее сделанной частично. Но принципиально мы по-прежнему находимся в рамках 2*100=200. Конечно, нужно отметить, что мы получаем возможность работы с потенциально бесконечными задачами, что без генераторов невозможно. Если генераторы и сопрограммы были в PHP почти всегда (добавлены в версию 5.4) то "файберы" ("волокна" в переводе) - это новинка недавняя, появившаяся в версии 8.1 Файберы - это способ останавливать любые функции, а не только генераторы в любом месте (в том числе во вложенных вызовах) и возобновлять их. Перепишем предыдущий пример с использованием файберов: Самое сложное для понимания место в этом коде - строка №12. В ней происходит та самая магия приостановки задачи. Многоликий метод Fiber::suspend делает три дела сразу - и возвращает из задачи выходное значение ( mb_strtoupper($value) ), и приостанавливает выполнение задачи-файбера до следующего вызова метода resume() извне задачи, и возвращает принятое извне входное значение для следующего кванта работы файбера. Обратите внимание, что первой строкой в каждой задаче я пишу Fiber::suspend(); Я делаю это намеренно, чтобы задачи встали на паузу сразу же после вызова метода $task->start() Принесли ли файберы что-то новое по сравнению с генераторами и корутинами? Да, разумеется. Появилась возможность оборачивать в файбер любую функцию, приостанавливать ее на любом уровне вложенности с сохранением стека вызовов и контекста. Добавился удобный объектно-ориентированный интерфейс для работы с задачами. Является ли это новое чем-то принципиальным и революционным? Нет. Файберы, как и генераторы, реализуют конкурентность, лишь, возможно, делая её чуть более удобной.  Равенство 2*100 = 200 по-прежнему остается актуальным, мяч у нас по-прежнему один и задачи лишь перекидывают его друг другу. Более того, нас всё еще держит проблема блокирующего кода - если какая-то из задач решит оставить мяч у себя и не перекидывать своей соседке, мы ничего не сможем с этим сделать... Итак, у нас есть задачи. Есть кванты их работы, определяемые генераторами или файберами. И мяч, который задачи перекидывают друг другу, освобождая и передавая. Что это за мяч? Это поток исполнения. Как бы мы с вами ни старались усовершенствовать Event Loop и Concurrency - мяч всё равно один. И тот игрок (задача), который зачем-то решит задержать мяч (поток) у себя, остановит (заблокирует) всю командную игру - остальные задачи будут вынуждены его ждать. Что же может заблокировать поток исполнения кода?  Очень много что. В первую очередь - это операции ввода-вывода. Чтение из файла? Запись в файл? Получение данных от базы? Да, разумеется. Всё это - блокирующие операции, так называемый "блокирующий I/O", то есть "ввод-вывод". Мы не можем с вами остановиться посередине функции fgets() или метода PDO::query(). Если их выполнение началось - нужно ждать окончания, сколько бы это ни заняло времени. А мяч, точнее поток исполнения? Стоит. Ждет. Потому что эти функции синхронные и блокирующие. Блокирующий I/O - это фундаментальная проблема. Она не зависит от операционной системы (ввод-вывод везде блокирующий), от языка программирования (он тут вообще ничего не решает) или от фреймворка.  Проблема блокирующего I/O усугубляется еще и тем, что, зачастую, даже системные библиотеки написаны в синхронном стиле. И изменить это невозможно лишь силами PHP-сообщества. К примеру, в mysqlnd (драйвер для работы с MySQL) в принципе заложена возможность асинхронных запросов и, при желании, ее даже можно использовать в PHP, а вот аналогичных клиентских библиотек для некоторых других баз данных просто нет в природе. Какой же выход? Как нам получить реальную пользу от асинхронного выполнения кода? Выход только один - асинхронные задачи нужно запускать параллельно основному потоку исполнения. Один способ мы уже знаем - это распределенная асинхронность и использование очереди событий. Есть ли другие способы? Да. Есть. Оказывается, можно закинуть на площадку несколько мячей. Для того, чтобы нам увидеть параллельное исполнение задач, давайте их должным образом подготовим. Пусть у нас первая задача считает числа от 1 до 25 с паузой в 1 секунду между ними, а вторая - точно также считает числа от 25 до 1, в обратном порядке. Таким образом каждая задача выполняется 25 секунд, при последовательном или конкурентном исполнении обе выполнятся за 50 секунд, а при реально параллельном - за те же 25. Пишем задачи: Не забываем добавить к своей установке PHP расширение pcntl и пишем код, управляющий задачами: Что тут происходит? Всё достаточно просто: Для каждой задачи мы с помощью функции pcntl_fork() запускаем отдельный процесс, дочерний по отношению к текущему. Функция pcntl_fork() вернет нам PID запущенного дочернего процесса, если мы находимся в родительском и 0, если мы в дочернем. Пользуемся этой возможностью, чтобы выполнить задачу, если мы находимся в дочернем процессе. С помощью функции pcntl_wait() заставляем основной процесс остановиться и дождаться окончания всех дочерних. Запустите этот код и убедитесь, что он отрабатывает за 25 секунд. Наши задачи действительно выполняются параллельно! Это огромный плюс. Какие минусы? Их достаточно много... Создание процесса - не самая дешёвая операция, даже если мы это делаем с помощью fork(); Переключение контекста между процессами тоже стоит процессорного времени. Если на 4-ядерном сервере вы запустите 4 процесса или, скажем, 40 - в целом будет нормально. А вот если вы наплодите 4000 процессов - процессор большую часть времени будет переключаться между ними, а не делать полезную работу. Дочерний процесс не унаследует дескрипторы - все открытые ранее файлы и сетевые соединения придется переоткрывать; И, самое главное, процессам трудно общаться между друг другом. Да, есть сигналы, но это сложно назвать полноценным общением. С помощью сигналов мы не передадим значение из одной задачи в другую... Значит придется придумывать какую-то общую шину данных между процессами, например брать одно из распространенных key-value хранилищ. Однако тот факт, что подобная многопроцессность представляет из себя полноценную многозадачность и реально параллельное выполнение кода, причем без каких-то особых сложностей в коде - конечно, перевешивает минусы. В современных операционных системах есть еще одно средство параллельного исполнения - это "потоки" ("threads"). Поддерживаются потоки и в PHP, при условии их поддержки на уровне ОС. Потоки работаю параллельно внутри одного процесса. В каждом процессе всегда есть как минимум один поток и есть возможность запустить другие. Потоки совместно используют код и контекст - например каждый поток в случае PHP будет иметь доступ ровно к тем же классам, функциям и глобальным переменным. Процесс можно сравнить с процессом приготовления блюда, а потоки - с несколькими поварами, которые работают над одним блюдом по одному рецепту параллельно, распределив между собой задачи. Когда-то давно для управления потоками в PHP требовалось собрать его инстанс с флагом ZTS (Zend Thread Safe), специальным расширением pthreads и работать с потоками на достаточно низком уровне вызовов операционной системы.  К счастью сейчас существует новое расширение для работы с потоками - Parallel. Оно устанавливается гораздо проще (но по-прежнему требует библиотеку pthreads в ОС) и предоставляет очень удобный интерфейс для запуска задач в отдельных потоках и для общения между задачами. Давайте перепишем предыдущий пример с использованием Parallel. Определение задач у нас останется прежним, изменится лишь блок их параллельного запуска: Весь секрет работы с потоками заключен в объекте класса parallel\Runtime С помощью метода run() этого объекта мы запустим задачу на параллельное исполнение в отдельном потоке. Метод run() вернет нам так называемый "фьючерс" - специальный объект класса parallel\Future, с помощью которого мы сможем узнать статус выполняющейся задачи и получить ее результат, когда она закончит выполняться. Parallel устроен по умолчанию так, что наш процесс будет продолжаться до тех пор, пока не закончатся все порожденные в нем потоки, поэтому явного вызова wait() или аналогичной функции не требуется. Запустите код и убедитесь, что 25+25 = 25. Мы сумели в одном процессе выполнить 2 задачи действительно параллельно. Кроме того использование потоков решает проблему блокирующих операций. Достаточно запустить задачу, требующую выполнения блокирующего кода в отдельном потоке и, затем, занимаясь другими задачами, опрашивать поток - закончился ли он? а при окончании получить результат его работы. Написание примера, сочетающего event loop, блокирующую задачу и вынос задач в отдельные потоки оставлю в качестве упражнения читателям, у них для этого теперь есть все необходимые инструменты. Конечно же, говоря о многопоточности, нельзя не упомянуть Swoole - модный сейчас асинхронный фреймворк для PHP. Про него уже был ряд статей на хабре, дублировать их - неблагодарное занятие. Поэтому просто приведу пример из официальной документации, который, как я надеюсь, заинтересует вас этим фреймворком: В данном примере создается один контекст выполнения (пул задач, другими словами) и в нем запускаются две параллельные задачи.  Разумеется, Swoole не несет в себе какой-то особой магии помимо того, что мы уже изучили. В его основе лежат всё те же корутины, файберы, если они доступны, и запуск кода в параллельных процессах. Однако этот фреймворк привлекает качеством кода, документации, богатством возможностей и, безусловно, заслуживает изучения, если вы интересуетесь асинхронным и параллельным PHP. Верно ли, что PHP - не "асинхронный" язык? Конечно верно. В текущей реализации PHP нет встроенного цикла обработки событий или выполнения блокирующих операций в отдельном потоке, как в JS. Тут не о чем спорить. Но верно ли, что в PHP в данный момент есть всё необходимое для того, чтобы писать асинхронный неблокирующий параллельный код? Да, тоже верно. Есть достаточное количество инструментов, библиотек и фреймворков, позволяющих вам это делать прямо сейчас, не дожидаясь появления в самом языке волшебных ключевых слов async/await. Кстати, а так ли они нужны в PHP? Вопрос открыт... https://developer.mozilla.org/ru/docs/Web/JavaScript/EventLoop  https://symfony.com/doc/current/event_dispatcher.html https://laravel.com/docs/9.x/events https://laravel.com/docs/9.x/events#queued-event-listeners https://www.rabbitmq.com/tutorials/tutorial-one-php.html https://redis.io/docs/manual/pubsub/ https://reactphp.org/event-loop/ https://www.php.net/manual/ru/language.generators.overview.php https://ru.wikipedia.org/wiki/Многозадачность#Совместная_или_кооперативная_многозадачность https://ru.wikipedia.org/wiki/Windows_3.x https://www.php.net/manual/ru/language.fibers.php https://www.php.net/manual/ru/mysqli.reap-async-query.php https://www.php.net/manual/ru/book.pcntl.php https://ru.wikipedia.org/wiki/Поток_выполнения https://www.php.net/manual/ru/book.pthreads.php https://www.php.net/manual/ru/book.parallel.php https://www.php.net/manual/ru/book.swoole.php https://openswoole.com/ https://pr-of-it.ru/webinars/110.html  Программист. CTO. Архитектор. Преподаватель.