Эта статья представляет собой краткий (шутка!) конспект одноименного (почти) вебинара, недавно проведенного автором.

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

Ну и немного раскрыть глаза на то, что, оказывается в PHP есть и асинхронность, и многопоточность, и в общем-то не нужно ждать мифической версии PHP 10, чтобы начать их использовать уже прямо сейчас!

Что такое "асинхронность"?

Если кратко, то асинхронное выполнение кода - это возможность некий блок кода (иначе говоря, "задачу") выполнить не в заранее заданном порядке, а в порядке, который зависит от.

От чего? От внешних условий: от наступления определенного события или, к примеру, наступления момента времени.

Самый простой пример, который можно привести, это, конечно, знаменитая функция setTimeout из JS (немного иронично, что в статье про PHP первый пример будет на языке JavaScript - но что уж поделать...):

setTimeout(function () { alert('Я выполнюсь через 5 секунд'); }, 5000);
alert('А я выполнюсь сразу');

Пример, несмотря на очевидную простоту, полностью объясняет идею асинхронного исполнения кода:

  • У нас есть задача - это функция, являющаяся первым аргументом setTimeout();

  • Мы определили условие, по которому эта задача будет отложенно выполнена - это наступление события "прошло 5 секунд";

  • Далее код выполняется синхронно, ровно в том порядке, в котором он и написан, пока не наступит ожидаемое событие;

  • Наступление события активирует отложенную задачу - она выполняется.

Возможно ли такое в PHP с использованием стандартного синтаксиса языка и стандартной библиотеки?

Нет.

Event loop - цикл событий

Всё дело в том, что PHP изначально не реализует так называемый "цикл обработки событий", или "Event Loop". Не реализует не потому, что PHP - плохой язык, а JS - хороший, тут вообще не применимы моральные оценки - а потому что PHP зачастую живет в другой парадигме.

Как работает PHP, если опустить нюансы? Очень просто, "запрос" - "веб-сервер" - "процесс PHP" - "веб-сервер" - "ответ". И даже если опустить дурацкую поговорку про то, что "PHP рожден, чтобы умирать", всё равно понятно, что в режиме совместной работы с веб-сервером программа на PHP заинтересована в том, чтобы заканчивать свою работу как можно быстрее. Какие уж тут циклы событий, какая асинхронность - чем быстрее процесс отработает, тем быстрее клиент получит ответ!

Но всё меняется, когда мы переходим от модели "веб-сервер + PHP" к написанию долгоиграющих консольных или, чем не шутит черт, GUI (а такие примеры уже есть) приложений на PHP. В этих случаях необходимость отложенного выполнения кода становится очевидной, ведь даже простейшая задача вроде "Если клиент нажал А, то..." становится асинхронной!

Что делать?

Придется написать Event Loop самим! Начнем.

Предусмотрим перечисление с условными кодами событий:

enum Event
{
    case I;
    case O;
    case U;
    case A;
}

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

Как задел на будущее - укажем флаг bool $once- как указание на то, должна ли задача выполняться однократно, или многократно.

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

class Tasks
{
    private array $tasks = [];

    public function addTask(Event $event, callable $task, bool $once=false): self
    {
        $this->tasks[$event->name][] = ['task' => $task, 'once' => $once];
        return $this;
    }

    public function getTasksByEvent(Event $event): array
    {
        $tasks = $this->tasks[$event->name] ?? [];
        $ret = [];
        foreach ($tasks as $i => $task) {
            if ($task['once']) {
                unset($this->tasks[$event->name][$i]);
            }
            $ret[] = $task['task'];
        }
        return $ret;
    }
}

Теперь сделаем класс, который будет заниматься генерацией событий. В качестве события будем рассматривать нажатие пользователем на определенную клавишу на клавиатуре. Если интересующая нас клавиша нажата - вернем код события, если нет - вернем null.

class KeyboardEventsEmitter
{

    public function __construct()
    {
        readline_callback_handler_install('Нажмите клавишу "i", "o", "u" или "a": ', function(){});
    }

    public function emit(): ?Event
    {
        static $fh = STDIN;
        $key = stream_get_contents($fh, 1);
        return match ($key) {
            'i' => Event::I,
            'o' => Event::O,
            'u' => Event::U,
            'a' => Event::A,
            default => null,
        };
    }
}

Небольшой "хак" с функцией readline_callback_handler_install() требуется, чтобы заставить стандартный поток ввода отдавать нам каждый символ после нажатия соответствующей клавиши, не дожидаясь, когда пользователь нажмет "Enter".

Ну и, наконец, добавим задачи и организуем сам Event Loop - то есть бесконечный цикл получения и обработки событий:

$tasks = new Tasks();
$tasks->addTask(Event::I, function () { echo 'Ma ya hi' . PHP_EOL;});
$tasks->addTask(Event::O, function () { echo 'Ma ya ho' . PHP_EOL;});
$tasks->addTask(Event::U, function () { echo 'Ma ya hu' . PHP_EOL;});
$tasks->addTask(Event::A, function () { echo 'Ma ya ha-ha' . PHP_EOL;}, true);

$events = new KeyboardEventsEmitter();

while (true) {
    $event = $events->emit();
    if (null === $event) {
        continue;
    }
    foreach ($tasks->getTasksByEvent($event) as $task) {
        $task();
    }
}

Удовольствие запустить этот код и понаблюдать, что будет в ответ на нажатие соответствующих клавиш (внимание - регистр нижний, алфавит - латинский!) оставляю читателю :)

Event-driven в Symfony - это асинхронность?

Коротко: нет.

Если рассмотреть встроенный, скажем в Symfony или Laravel или (это классический пример!) punBB механизм "событий", "уведомлений" и их "обработчиков" - может сложиться ложное впечатление, что всё это - асинхронное выполнение кода.

На самом деле я глубоко убежден, что event-driven программирование на PHP в парадигме конечного процесса "запрос-работа-ответ" - это средство прежде всего запутать программиста, создав у него иллюзию, что он овладел волшебной асинхронностью. При том, что на самом деле он овладел искусством создания запутанной лапши вместо кода.

Поэтому - нет. Event-driven это не про асинхронность, это архитектурный паттерн построения синхронных программ, со сложным и заранее непрогнозируемым потоком исполнения. Никакого цикла генерации и обработки событий этот паттерн не добавляет.

Разумеется, всё сказанное выше не имеет отношения к распределенной асинхронности, о которой речь пойдет дальше.

Распределенная асинхронность

Впрочем, всё меняется, если вы раскладываете свой код на >=2 независимых сервиса и соединяете их некой "шиной" или "очередью" событий.

В качестве такой "шины событий" может выступать, к примеру, RabbitMQ или, скажем, встроенный в Redis механизм PUB/SUB.

В таком случае мы действительно получаем настоящую асинхронность (ключевые для понимания моменты выделены):

  • HTTP-сервис принимает запрос;

  • Поняв, что бизнес-логика требует отложенного действия (например: отправки письма пользователю) HTTP-сервис создает в очереди событие, а сам продолжает выполняться дальше;

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

Это - асинхронное выполнение кода. Пусть и ценой увеличения количества сервисов в приложении.

Посмотрите, к примеру, как такой подход реализован в том же Laravel, где он называется "Queued Events".

Два слова про React PHP

Говоря про Event Loop, невозможно не упомянуть о React PHP - пожалуй первой PHP-библиотеке, где этот паттерн был полноценно реализован.

Пример из официальной документации весьма красноречиво описывает возможности React PHP:

use React\EventLoop\Loop;

$timer = Loop::addPeriodicTimer(0.1, function () {
    echo 'Tick' . PHP_EOL;
});

Loop::addTimer(1.0, function () use ($timer) {
    Loop::cancelTimer($timer);
    echo 'Done' . PHP_EOL;
});

Чем не аналог setTimeout() ?

Разумеется, одним только Even Loop не исчерпываются возможности React PHP. Он предоставляет много интересного: это и неблокирующие стримы ввода-вывода, и своя реализация промисов, и компоненты для работы с сетевыми соединениями.

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

Кооперативная многозадачность на примере генераторов и корутин

Хорошо, предположим, что мы с вами в совершенстве освоили технику создания Event Loop и научились выполнять задачи отложенно. Но как быть, если задачи достаточно объемные? К примеру, задачей может быть чтение большого файла с данными, обработка этих данных и запись в базу. Поможет ли асинхронное исполнение оптимизировать производительность? Нет.

Нам нужно найти какой-то способ разбивать крупные задачи на кванты и выполнять их "дискретно", чередуя выполнение квантов задач. К примеру - прочитали одну строку из файла (квант задачи №1), преобразовали эти данные в нужный вид (квант задачи №2), записали в базу (квант задачи №3), снова вернулись к чтению очередной строки из файла (следующий квант задачи №2).

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

Для такого "квантования" в PHP существует ряд языковых средств. Первое из них - генераторы и корутины.

Попробуем решить задачу построчного чтения файла на генераторах:

$task1 = function () {
    $fh = fopen(__DIR__ . '/test.txt', 'r');
    while (!feof($fh)) {
        yield trim(fgets($fh));
    }
};

Как это работает?

Генератор в PHP, если сильно упрощать, это - функция, которая:

  1. Как бы не совсем функция, хотя синтаксически очень на нее похожа.

  2. Вызов генератора, как функции, вернет нам объект класса Generator, реализующий интерфейс Iterator, с которым мы уже будем работать.

  3. Генератор умеет не просто возвращать одно значение (оператор return), но вместо этого генерировать последовательность: оператор yield выдает очередное значение последовательности.

  4. Сохраняет своё состояние, когда ее работа прервана оператором yield.

  5. Умеет продолжать работу с сохраненного состояния, когда работа генератора возобновлена вызовом метода Iterator::next()

Первая задача представляет из себя генератор, который будет построчно читать некий файл и генерировать последовательность прочитанных строк.

Использовать генератор можно с помощью цикла foreach (совместный цикл) или явно вызывая его методы:

// так:
foreach ($task1() as $str) {
    echo $str . PHP_EOL;
}

// или так:
$gen1 = $task1();
while (true) {
    if (!$gen1->valid()) {
        break;
    }
    $str = $gen1->current();
    echo $str . PHP_EOL;
    $gen1->next();
}

Однако, на этом возможности генераторов не исчерпываются. Мы можем не только получать от генератора очередные значения генерируемой им последовательности, но и передавать в генератор значения на каждом шаге! Для этого используется то же ключевое слово yield, но уже как выражение.

Давайте попробуем перевести на язык генераторов вторую задачу: "Принять строку, преобразовать ее к верхнему регистру, выдать, как значение последовательности, ждать следующую строку":

$task2 = function () {
    while (true) {
        $value = yield; // Приняли очередное значение извне
        yield mb_strtoupper($value); // Использовали его для генерации
    } // И так повторяем бесконечно
};

Такой генератор, который умеет принимать извне значения, называется "корутиной" или, по-русски, "сопрограммой".

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

$gen1 = $task1();
$gen2 = $task2();

while (true) {
    if (!$gen1->valid()) {
        break;
    }
    $str = $gen1->current();
    echo 'Прочитано: ' . $str . PHP_EOL;
    $str = $gen2->send($str);
    echo 'Обработано: ' . $str . PHP_EOL;

    $gen1->next();
    $gen2->next();
}

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

Мы реализовали с вами кооперативную многозадачность (конкурентность) - псевдопараллельное выполнение задач, основанное на том, что задача может выполнить квант работы, прервать сама себя, сохранив своё состояние и передать управление другой задаче.

Разумеется, никакой настоящей параллельности здесь нет. Скажем, если первой задаче для кванта работы требуется одна секунда, второй задаче для своего кванта - тоже секунда, а всего таких квантов 100, в целом программа будет выполняться минимум 200 секунд. Мы выигрываем лишь в ресурсах (в памяти) и в возможности прервать работу, оставив ее сделанной частично. Но принципиально мы по-прежнему находимся в рамках 2*100=200.

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

Файберы, как еще один маленький шаг вперед

Если генераторы и сопрограммы были в PHP почти всегда (добавлены в версию 5.4) то "файберы" ("волокна" в переводе) - это новинка недавняя, появившаяся в версии 8.1

Файберы - это способ останавливать любые функции, а не только генераторы в любом месте (в том числе во вложенных вызовах) и возобновлять их.

Перепишем предыдущий пример с использованием файберов:

$task1 = new Fiber(function () {
    Fiber::suspend();
    $fh = fopen(__DIR__ . '/test.txt', 'r');
    while (!feof($fh)) {
        Fiber::suspend(trim(fgets($fh)));
    }
});

$task2 = new Fiber(function () {
    $value = Fiber::suspend();
    while (true) {
        $value = Fiber::suspend(mb_strtoupper($value));
    }
});

$task1->start();
$task2->start();

while (true) {
    // Получаем от первого файбера очередную строку из файла
    $str = $task1->resume();

    // Если его работа закончена - закончен и наш "бесконечный" цикл
    if ($task1->isTerminated()) {
        break;
    }

    echo 'Прочитано: ' . $str . PHP_EOL;

    // Передаем прочитанную строку второй задаче, получаем от нее результат ее работы
    $str = $task2->resume($str);
    echo 'Обработано: ' . $str . PHP_EOL;
}

Самое сложное для понимания место в этом коде - строка №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.

Пишем задачи:

$tasks = [

    1 => function () {
        foreach (range(1, 25, +1) as $value) {
            sleep(1);
            echo $value . PHP_EOL;
        }
    },

    2 => function () {
        foreach (range(25, 1, -1) as $value) {
            sleep(1);
            echo $value . PHP_EOL;
        }
    },

];

Не забываем добавить к своей установке PHP расширение pcntl и пишем код, управляющий задачами:

foreach ($tasks as $task) {
    $pid = pcntl_fork();
    if (0 == $pid) {
        $task();
    }
}

pcntl_wait($status);

Что тут происходит?

Всё достаточно просто:

  1. Для каждой задачи мы с помощью функции pcntl_fork() запускаем отдельный процесс, дочерний по отношению к текущему.

  2. Функция pcntl_fork() вернет нам PID запущенного дочернего процесса, если мы находимся в родительском и 0, если мы в дочернем.

  3. Пользуемся этой возможностью, чтобы выполнить задачу, если мы находимся в дочернем процессе.

  4. С помощью функции pcntl_wait() заставляем основной процесс остановиться и дождаться окончания всех дочерних.

Запустите этот код и убедитесь, что он отрабатывает за 25 секунд. Наши задачи действительно выполняются параллельно! Это огромный плюс.

Какие минусы? Их достаточно много...

  • Создание процесса - не самая дешёвая операция, даже если мы это делаем с помощью fork();

  • Переключение контекста между процессами тоже стоит процессорного времени. Если на 4-ядерном сервере вы запустите 4 процесса или, скажем, 40 - в целом будет нормально. А вот если вы наплодите 4000 процессов - процессор большую часть времени будет переключаться между ними, а не делать полезную работу.

  • Дочерний процесс не унаследует дескрипторы - все открытые ранее файлы и сетевые соединения придется переоткрывать;

  • И, самое главное, процессам трудно общаться между друг другом. Да, есть сигналы, но это сложно назвать полноценным общением. С помощью сигналов мы не передадим значение из одной задачи в другую... Значит придется придумывать какую-то общую шину данных между процессами, например брать одно из распространенных key-value хранилищ.

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

Многопоточность

В современных операционных системах есть еще одно средство параллельного исполнения - это "потоки" ("threads"). Поддерживаются потоки и в PHP, при условии их поддержки на уровне ОС.

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

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

Когда-то давно для управления потоками в PHP требовалось собрать его инстанс с флагом ZTS (Zend Thread Safe), специальным расширением pthreads и работать с потоками на достаточно низком уровне вызовов операционной системы.

К счастью сейчас существует новое расширение для работы с потоками - Parallel. Оно устанавливается гораздо проще (но по-прежнему требует библиотеку pthreads в ОС) и предоставляет очень удобный интерфейс для запуска задач в отдельных потоках и для общения между задачами.

Давайте перепишем предыдущий пример с использованием Parallel. Определение задач у нас останется прежним, изменится лишь блок их параллельного запуска:

$futures = [];
foreach ($tasks as $num => $task) {
    $runtime = new parallel\Runtime();
    $futures[$num] = $runtime->run($task);
}

Весь секрет работы с потоками заключен в объекте класса parallel\Runtime С помощью метода run() этого объекта мы запустим задачу на параллельное исполнение в отдельном потоке. Метод run() вернет нам так называемый "фьючерс" - специальный объект класса parallel\Future, с помощью которого мы сможем узнать статус выполняющейся задачи и получить ее результат, когда она закончит выполняться.

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

Запустите код и убедитесь, что 25+25 = 25. Мы сумели в одном процессе выполнить 2 задачи действительно параллельно.

Кроме того использование потоков решает проблему блокирующих операций. Достаточно запустить задачу, требующую выполнения блокирующего кода в отдельном потоке и, затем, занимаясь другими задачами, опрашивать поток - закончился ли он? а при окончании получить результат его работы.

Написание примера, сочетающего event loop, блокирующую задачу и вынос задач в отдельные потоки оставлю в качестве упражнения читателям, у них для этого теперь есть все необходимые инструменты.

Swoole и его go-рутины

Конечно же, говоря о многопоточности, нельзя не упомянуть Swoole - модный сейчас асинхронный фреймворк для PHP.

Про него уже был ряд статей на хабре, дублировать их - неблагодарное занятие. Поэтому просто приведу пример из официальной документации, который, как я надеюсь, заинтересует вас этим фреймворком:

Co\run(function()
{
    go(function()
    {
        Co::sleep(1);
        echo "Done 1\n";
    });

    go(function()
    {
        Co::sleep(1);
        echo "Done 2\n";
    });
});

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

Разумеется, Swoole не несет в себе какой-то особой магии помимо того, что мы уже изучили. В его основе лежат всё те же корутины, файберы, если они доступны, и запуск кода в параллельных процессах.

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

Заключение

Верно ли, что PHP - не "асинхронный" язык? Конечно верно. В текущей реализации PHP нет встроенного цикла обработки событий или выполнения блокирующих операций в отдельном потоке, как в JS. Тут не о чем спорить.

Но верно ли, что в PHP в данный момент есть всё необходимое для того, чтобы писать асинхронный неблокирующий параллельный код? Да, тоже верно. Есть достаточное количество инструментов, библиотек и фреймворков, позволяющих вам это делать прямо сейчас, не дожидаясь появления в самом языке волшебных ключевых слов async/await.

Кстати, а так ли они нужны в PHP? Вопрос открыт...

Ссылки на материалы для чтения

Комментарии (14)


  1. LaserPro
    25.05.2022 23:37
    +1


    Спасибо за статью, интересно. Но всё же, как надоел это пример про генераторы и корутины с чтением файла по строкам!
    "Конечно, нужно отметить, что мы получаем возможность работы с
    потенциально бесконечными задачами, что без генераторов невозможно."


    Но почему-то всегда приводится этот пример, который элементарно переписывается без всяких генераторов. При этом, также останется чтение по строкам, без считывания всего файла в память. И в обычной работе, в коде вроде порой хочется ввернуть генератор. Даже напишешь, а потом понимаешь что можно проще и понятнее без него.

    Может есть какой-то пример, где действительно без них никак, и есть настоящая польза?


    1. AlexLeonov Автор
      25.05.2022 23:39
      -2

      Может есть какой-то пример, где действительно без них никак, и есть настоящая польза?

      Конечно есть. Представьте себе файл со списком недействительных паспортов: https://проверки.гувм.мвд.рф/upload/expired-passports/list_of_expired_passports.csv.bz2

      Всего-то 500+ мегабайт CSV в архиве. Сделайте его обработку без генераторов и со считыванием всего файла в память.


      1. LaserPro
        25.05.2022 23:53
        +4

        но зачем загружать файл в память целиком? Если вы его можете обрабатывать построчно с генератором, читайте так же построчно без генератора. обработали строку, выдали результат - взяли следующую.

        И если вам для работы с файлом, по условию задачи, необходимо считать его весь в память (ну мало ли, что там за формат внутри) , то генератор вам уже не поможет, все равно придется считать.


        1. AlexLeonov Автор
          25.05.2022 23:59

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

          Я же вам рассказываю - что такое рейсфедер и как его применять, безотносительно ложки.

          Причем тут ваш пример?


          1. LaserPro
            26.05.2022 00:41
            +7

            Причем тут ваш пример?

            Это не мой пример, это ваш (и не только) пример. Он хорошо показывает "как работают генераторы" и плохо показывает "как получить пользу от применения генераторов".
            Я всего лишь спросил, надеясь на ваш богатый опыт, нет ли другого примера, где использование генератора приводит к реальной экономии ресурсов, которой не достичь без оного. Ну нет, так нет.

            Попробую ответить самому себе, какое у меня к данному моменту сложилось впечатление (но может, у кого-то еще найдется пример):
            - Чтобы реализовать аналогичный функционал без генератора, вам придется или использовать свой объект с состоянием, или вынести весь код в тело цикла, где и будет состояние выполнения тех или иных операций. В первом случае все довольно просто, но дополнительная писанина и размножение сущностей. Во втором случае - может получиться сложночитаемая каша. Таким образом главная выгода от генераторов - сокращение и упрощение кода, но вовсе не экономия ресурсов. И выгода тем больше, чем больше этаких "задач с состоянием" мы пытаемся выполнить за такт цикла, и чем больше кода в каждой "задаче".


    1. aktuba
      27.05.2022 12:17

      Может есть какой-то пример, где действительно без них никак, и есть настоящая польза?

      С одной стороны - нет таких примеров, т.к. генератор = цикл, а значит тело генератора всегда можно развернуть в обычный цикл.

      С другой стороны - нельзя передать цикл из одного обработчика в другой, а генератор можно. Да, конечно можно обойтись if/switch и получить обычный цикл, но с генераторами получится проще код.

      На примере покер-калькулятора: представьте, что у вас есть метод, считающий комбинацию из набора 5 карт, т.е. на вход 5 карт, на выходе - какая это комбинация.

      И есть 2 типа игры: холдем и омаха. Чуток информации для тех, кто не в курсе правил: в холдеме на руках 2 карты + 5 на столе, берем из этих 7и 5 с максимальной комбинацией. В омахе 4 на руках (про другие типы тоже знаю, но сейчас не об этом) + 5 на столе, всего 9, но с ручных учитываются только любые 2.

      Если наш калькулятор работает с обоими типами игр - придется в цикле делать цикл для генерации всех вариантов с рук + то же самое, что в холдеме. Код сильно усложняется для чтения и поддержки.

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


  1. Andrey7070
    26.05.2022 09:31
    -4

    Отличная статья! PHP - сила!


  1. SerafimArts
    26.05.2022 12:36
    +1

    Возможно ли такое в PHP с использованием стандартного синтаксиса языка и стандартной библиотеки?

    Нет.

    Ну как сказать, вот набросал на коленке по-быстрому: https://github.com/SerafimArts/simple-interval-example


    P.S. А, дочитал до момента где описывается React c почти аналогичной реализацией. Зря получается пример набрасывал: "Коммент пиши — статью не читай", ага.


    Тогда я не понял почему в этом тезисе "нет" написано? Что стандартными средствами языка подобное невозможно.


    1. AlexLeonov Автор
      26.05.2022 13:01
      +1

      Кирилл, я имел в виду, что в стандартной библиотеке нет готового "setInterval" или иных инструментов, чтобы отложить задачу.

      А за пример спасибо, интересно.


      1. SerafimArts
        26.05.2022 13:10
        +8

        Кирилл, я имел в виду, что в стандартной библиотеке нет готового "setInterval" или иных инструментов, чтобы отложить задачу.

        Давай я чуток подушню тогда)))


        Чтобы именно "отложить" задачу — могу назвать как минимум 3 сходу:


        Через GC (выполнится во время подчистки мусора)
        $deferred = new class {
            public function __destruct() {
                echo 'Deferred';
            }
        };
        
        echo 'Some ';
        
        // Some Deferred

        Внедрение в 'тики' VM
        <?php
        declare(ticks=1);
        
        register_tick_function(function () {
            echo 'ТАЙМЕР';
        });
        
        echo 'делаем';
        echo 'какую-то';
        echo 'фигню';
        
        // ТАЙМЕРделаемТАЙМЕРкакую-тоТАЙМЕРфигнюТАЙМЕР

        Через хендлер завершения процесса
        <?php
        
        register_shutdown_function(function () {
            echo 'Я всё';
        });
        
        echo 'Привет';
        echo 'Мир';
        // ПриветМирЯ всё


        1. AlexLeonov Автор
          26.05.2022 13:14
          +2

          Ты не душнишь, ты экзотику накидываешь, за что тебе большое спасибо! ))


        1. rjhdby
          26.05.2022 13:52

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

          Hidden text
          <?php
          declare(ticks=1);
          
          $ticks = 0;
          $break = null;
          
          register_tick_function(
              function () {
                  global $ticks, $break;
                  $ticks++;
                  if ($break != null && $ticks > $break) {
                      die("DIE MOTHERFUCKER, DIE!!!");
                  }
              }
          );
          
          while (true) {
              sleep(1);
              echo "I'm alive for tick $ticks" . PHP_EOL;
          
              if (rand(0, 100) > 50) {
                  echo "Death looking for you" . PHP_EOL;
                  $break = $ticks + 2;
              }
          }


          1. AlexLeonov Автор
            26.05.2022 17:36

            Вместо глобальных переменных можно использовать статические методы какого-либо класса, как, кстати, делает Fiber.


  1. Gemorroj
    27.05.2022 12:08

    https://www.php.net/manual/ru/intro.pcntl.php только для cgi/cli. что сильно ограничивает применение. https://stackoverflow.com/questions/35026153/call-to-undefined-function-pcntl-fork-php-fpm-nginx#answer-35029409 комментарий от Joe Watkins.

    parallel погиб - комментарий от тогоже Joe Watkins https://github.com/krakjoe/parallel/issues/201#issuecomment-892442141

    по факту нужно смотреть в сторону swoole/amphp