index php page thread
Простой вариант реализации многопоточности на PHP
Многопоточность в PHP отсутствует «из коробки», поэтому вариантов её реализации было придумано великое множество, включая расширения pthreads, AzaThread (CThread), и даже несколько собственных наработок PHP программистов.
Основным минусом для меня стало слишком больше количество «наворотов» у этих решений — не всегда есть необходимость в обмене информации между потоками и родительским процессом или в экономии ресурсов. Всегда должна быть возможность быстро и с минимумом затрат решить задачу.
Заранее хочу оговориться, что в этом посте не открываются великие тайны — он скорее для новичков в языке, и опубликовать его я решил только потому, что в свое время сам столкнулся с проблемой и не найдя готового решения сделал эдакую эмуляцию многопоточности самостоятельно.
Итак, задача состоит в том, что бы обработать большое количество данных, пришедших в наш скрипт. Моей задачей было обработать JSON массив текстовой информации, переварив которую скрипт должен был собрать из неё не менее большой коммит для PostgreSQL.
Первым делом собираем данные в родительском файле:
Размер массива колебался около 400мб (позже сократился до
50мб), и вся информация была текстовой. Не сложно прикинуть скорость, с которой это всё переваривалось, а если учесть, что скрипт выполнялся по cron каждые 15 минут, а вычислительная мощность была такой себе — быстродействие страдало очень сильно.
После получения данных можно прикинуть их объем и при необходимости рассчитать необходимое количество потоков на каждое ядро ЦП, а можно просто решить, что потоков будет 4 и посчитать количество строк для каждого потока:
Стоит сразу оговориться — такой расчет «в лоб» не даст точного результата по количеству элементов для каждого потока. Он нужен скорее для упрощения расчетов.
А теперь самая суть — создаем задачи для каждого потока и запускаем его. Делать мы это будем «в лоб» — создавая задачу для второго файла — thread.php. Он будет выступать в роли «потока», получая диапазон элементов для обработки и запускаясь независимо от основного скрипта:
Функция passthru() используется для запуска консольных команд, но скрипт будет ждать окончания выполнения каждой из них. Для этого мы оборачиваем команду на запуск в набор операторов, которые запустят процесс и тут же вернут ничего, запустив процесс и родительский процесс не приостановится в ожидании выполнения каждого дочернего:
Что конкретно тут происходит, к сожалению, точно сказать не могу — набор параметров мне подсказал мой знакомый линуксоид. Если в комментах сможете расшифровать эту магию — буду признателен и дополню пост.
Вот таким, довольно простым, способом можно реализовать эмуляцию многопоточности в PHP.
Если сократить весь пример до сухого вывода, то думаю он звучал бы так: родительский поток через командную строку запускает дочерние процессы, указывая им какую именно информацию обработать.
Говоря «эмуляцию» я имею в виду, что при таком методе реализации нет возможности для обмена информацией между потоками или между родительским и дочерними потоками. Он подходит в случае, если заранее известно, что такие возможности не нужны.
PHP Profi
Квест → Как хакнуть форму
Многопоточность в PHP с помощью Pthreads Перевод
Похоже, PHP разработчики редко используют параллельность. Говорить о простоте синхронного кода не буду, однопоточное программирование, конечно, проще и понятнее, но иногда небольшое использование параллельности может принести ощутимое повышение производительности.
В этой статье мы взглянем на то, как многопоточность может быть достигнута в PHP с помощью расширения pthreads. Для этого потребуется установленная ZTS (Zend Thread Safety) версия PHP 7.x, вместе с установленным расширением pthreads v3. (На момент написания статьи, в PHP 7.1 пользователям нужно будет установить из ветки master в репозитории pthreads – см. подробнее как установить (en) стороннее расширение.)
Большое спасибо Joe Watkins (создателю расширения pthreads) за вычитку и помощь в улучшении моей статьи!
Когда не стоит использовать pthreads
Прежде чем мы начнём, я хотел бы уточнить, когда вы не должны (да и не можете) использовать расширение pthreads.
В pthreads v2, рекомендация была в том, что pthreads не должна использоваться в веб-серверной среде (т.е. в fcgi процессе). Что касается pthreads v3, эта рекомендация является программным ограничением, так что теперь вы просто не сможете использовать его в среде веб-сервера. Две известные причины:
Вот почему многопоточность не является хорошим решением в такой среде. Если вы рассматриваете многопоточность как решение задач, блокирующих ввод-вывод (например, выполнение http-запросов), то позвольте мне обратить ваше внимание на асинхронное программирование, которое можно реализовать с помощью фреймворков, таких как Amp.
После такого отступления, давайте сразу перейдём к делу!
Обработка разовых задач
Давайте взглянем на иерархию классов, предлагаемую расширением pthreads:
Переиспользование потоков
Вот небольшой пример:
Как хорошая практика, для воркеров и пулов следует всегда подчищать их задачи, как только они завершились, и затем вручную завершать их самих. Потоки, созданные с помощью класса Thread также должны быть присоединены к порождающему потоку.
pthreads и (не)изменяемость
Давайте взглянем на пример, который продемонстрирует новые ограничения неизменяемости:
Давайте снова взглянем на пример, чтобы лучше понимать некоторые вещи:
В качестве демонстрации:
Синхронизация
В последнем разделе этой статьи мы рассмотрим синхронизацию в pthreads. Синхронизация — это метод, позволяющий контролировать доступ к общим ресурсам.
Для примера, давайте реализуем простейший счетчик:
Без использования синхронизации, вывод не детерминирован. Несколько потоков пишут в одну переменную без контролируемого доступа, что означает что обновления будут потеряны.
Синхронизированные блоки кода могут также взаимодействовать друг с другом, используя методы Threaded::wait и Threaded::notify (или Threaded::notifyAll ).
Вот поочерёдный инкремент в двух синхронизированных циклах while:
Заключение
Если вам интересен перевод следующего поста, дайте знать: комментируйте в соц. сетях, плюсуйте и делитесь постом с коллегами и друзьями.
Многопоточные вычисления в PHP: pthreads
Недавно я попробовал pthreads и был приятно удивлен — это расширение, которое добавляет в PHP возможность работать с несколькими самыми настоящими потоками. Никакой эмуляции, никакой магии, никаких фейков — все по-настоящему.
Я рассматриваю такую задачу. Есть пул заданий, которые надо побыстрее выполнить. В PHP есть и другие инструменты для решения этой задачи, тут они не упоминаются, статья именно про pthreads.
Стоит отметить, что автор расширения, Joe Watkins, в своих статьях предупреждает, что многопоточность — это всегда не просто и надо быть к этому готовым.
Кто не испугался, идем далее.
Что такое pthreads
Pthreads — это объектно-ориентированное API, которое дает удобный способ для организации многопоточных вычислений в PHP. API включает в себя все инструменты, необходимые для создания многопоточных приложений. PHP-приложения могут создавать, читать, писать, исполнять и синхронизировать потоки с помощью объектов классов Threads, Workers и Threaded.
Что внутри pthreads
Иерархия основных классов, которые мы только что упомянули, представлена на диаграмме.
Threaded — основа pthreads, дает возможность параллельного запуска кода. Предоставляет методы для синхронизации и другие полезные методы.
Thread. Можно создать поток, отнаследовавшись от Thread и реализовав метод run(). Метод run() начинает выполняться, причем в отдельном потоке, в момент, когда вызывается метод start(). Это можно инициировать только из контекста, который создает поток. Объединить потоки можно тоже только в этом-же контексте.
Worker. Персистентное состояние, которое в большинстве случаев используется разными потоками. Доступно, пока объект находится в области видимости или до принудительного вызова shutdown().
Помимо этих классов есть еще класс Pool. Pool — пул (контейнер) Worker-ов можно использовать для распределения Threaded объектов по Worker-ам. Pool — наиболее простой и эффективный способ организовать несколько потоков.
Не будем сильно грустить над теорией, а сразу попробуем все это на примере.
Пример
Можно решать разные задачи в несколько потоков. Мне было интересно решить одну конкретную и как мне кажется весьма типовую задачу. Напомню ее еще раз. Есть пул заданий, их надо побыстрее выполнить.
Так давайте приступим. Для этого создадим провайдер данных MyDataProvider (Threaded), он будет один и общий для всех потоков.
Для каждого потока у нас будет MyWorker (Worker), где будет храниться ссылка на провайдер.
Сама обработка каждой задачи пула, (пусть это будет некая ресурсоемкая операция), наше узкое горлышко, ради которого мы и затеяли многопоточность, будет в MyWork (Threaded).
Получается довольно элегантно на мой взгляд. Этот пример я выложил на гитхаб.
Вот и все! Ну почти все. На самом деле есть то, что может огорчить пытливого читателя. Все это не работает на стандартном PHP, скомпилированным с опциями по умолчанию. Чтобы насладиться многопоточностью, надо, чтобы в вашем PHP был включен ZTS (Zend Thread Safety).
Настройка PHP
После этого можно ставить расширение pthreads.
Вот теперь все. Ну… почти все. Представьте, что вы написали мультипоточный код, а PHP на машине у коллеги не настроен соответствующим образом? Конфуз, не правда ли? Но выход есть.
pthreads-polyfill
Тут снова спасибо Joe Watkins за пакет pthreads-polyfill. Суть решения такова: в этом пакете содержатся те-же классы, что и в расширении pthreads, они позволяют выполниться вашему коду, даже если не установлено расширение pthreads. Просто код будет выполнен в один поток.
Чтобы это заработало, вы просто подключаете через composer этот пакет и больше ни о чем не думаете. Там происходит проверка, установлено ли расширение. Если расширение установлено, то на этом работа polyfill заканчивается. Иначе подключаются классы-”заглушки”, чтобы код работал хотя бы в 1 поток.
Проверим
Информация о процессоре, на котором запускал тесты
Посмотрим диаграмму загрузки ядер процессора. Тут все соответствует ожиданиям.
А теперь самое главное, ради чего все это. Сравним время выполнения.
$threads | Примечание | Время выполнения, секунд |
---|---|---|
PHP без ZTS | ||
1 | без pthreads, без polyfill | 265.05 |
1 | polyfill | 298.26 |
PHP с ZTS | ||
1 | без pthreads, без polyfill | 37.65 |
1 | 68.58 | |
2 | 26.18 | |
3 | 16.87 | |
4 | 12.96 | |
5 | 12.57 | |
6 | 12.07 | |
7 | 11.78 | |
8 | 11.62 |
Из первых двух строк видно, что при использовании polyfill мы потеряли примерно 13% производительности в этом примере, это относительно линейного кода на совсем простом PHP “без всего”.
Далее, PHP с ZTS. Не обращайте внимание на такую большую разницу во времени выполнения в сравнении с PHP без ZTS (37.65 против 265.05 секунд), я не пытался привести к общему знаменателю настройки PHP. В случае без ZTS у меня включен XDebug например.
Как видно, при использовании 2-х потоков скорость выполнения программы примерно в 1.5 раза выше, чем в случае с линейным кодом. При использовании 4-х потоков — в 3 раза.
Можно обратить внимание, что хоть процессор и 8-ядерный, время выполнения программы почти не менялось, если использовалось более 4 потоков. Похоже, это связано с тем, что физических ядра у моего процессора 4. Для наглядности изобразил табличку в виде диаграммы.
Резюме
В PHP возможна вполне элегантная работа с многопоточностью с использованием расширения pthreads. Это дает ощутимый прирост производительности.
Прогресс выполнения тяжелой задачи в PHP
Случилось мне как-то иметь дело с тяжелым PHP-скриптом. Нужно было каким-то образом в браузере отображать прогресс выполнения задачи в то время, пока в достаточно длительном цикле на стороне PHP проводились расчёты. В таких случаях обычно прибегают к периодичному выводу строки вроде этой:
Этот вариант меня не устраивал по нескольким причинам, к тому же мне в принципе не нравится такой подход.
Итераций у меня было порядка 3000—5000. Я прикинул, что великоват трафик для такой несложной затеи. Кроме того, мне такой вариант казался очень некрасивым с технической точки зрения, а внешний вид страницы и вовсе получался уродлив: футер дойдет еще не скоро — после последнего уведомления о 100% выполнении задачи.
Увернуться от проблемы некрасивой страницы большого труда не составляло, но остальные минусы заставили меня обрадоваться и приступить к поискам более изящного решения.
Решение простое. Основанная страница — это пульт управления. С пульта можно запустить и остановить задачу. Эта страница инициирует XMLHttpRequest — стартует выполнение основной задачи. В процессе выполнения этой задачи (внутри основного цикла) скрипт отправляет клиенту один байт — символ пробела. На пульте в обработчике onreadystatechange мы, получая байт за байтом, сможем делать вывод о прогрессе выполнения задачи.
Схема такая. Скрипт операции:
Однако, итераций всего 50. Об этом мы знаем, потому что сами определили их количество в файле скрипта. А если не знаем или количество может меняться? При readyState == 2 мы можем получить информацию из заголовков. Давайте этим и воспользуемся для определения количества итераций:
А на пульте получим и запомним это значение:
Общая схема должна быть ясна. Поговорим теперь о подводных камнях.
Кроме того, то ли Nginx, то ли FastCGI, то ли сам Chrome считают, что инициировать прием-передачу тела ответа, которое содержит всего-навсего один байт — слишком расточительно. Поэтому нужно предварить всю операцию дополнительными байтами. Нужно договориться, скажем, что первые 20 пробелов вообще ничего не должны означать. На стороне PHP их нужно просто «выплюнуть» в вывод, а в обработчике onreadystatechange их нужно проигнорировать. На мой взгляд — раз уж вся конфигурационная составляющая передается в заголовках — то и это число игнорируемых пробелов тоже лучше передать в заголовке. Назовем это padding-ом.
На стороне клиента это тоже нужно учесть:
Откуда число 20? Если подскажете — буду весьма признателен. Я его установил экспериментальным путем.
С ее помощью можно обойти все уровни буферизации, вывести данные напрямую, после чего все буферы восстанавливаются.
Кстати, а почему именно пробел используется для уведомления о выполненной части задачи? Просто потому что почти любой формат представления данных в вебе такими пробелами не испортишь. Можно применить такой метод передачи уведомления о прогрессе операции, а после всего этого вывести отчет о результатах в JSON.
Если все привести в порядок, немного оптимизировать и дополнить код всеми возможностями, которые могут пригодиться, получится вот что:
Маршрутизация во фреймворках: Управление адресами URL
Каждый разработчик, знакомящийся с каким-либо фреймворком, проходит некий путь знакомства с системой маршрутизации запросов в нём. Данный компонент присутствует во многих системах и служит для использования «красивых» адресов страниц. В этой статье мы познакомимся с работой этой системы.
Генерация адресов ссылок
Кто-то наверняка не желая разобраться в этой теме сразу жёстко прописывает во всех представлениях адреса со вставками:
или, произведя небольшие настройки, делает то же самое, но в другом формате:
Но через некоторое время всё-таки переписывает все ссылки на использование системного метода createUrl :
Попробуем понять эволюцию мыслей разработчика сайта на фреймворке.
Первым делом, прочуствуем сильные стороны автоматизации обработки URL адресов. Давайте разработаем крупную и гибкую систему и автоматизируем в ней абсолютно всё, что попадается под руку.
Управление адресами страниц в приложении
Давным давно, в стране первых веб-программистов PHP файлы произвели революцию в разработке сайтов. Ведь тогда каждый ремесленник мог построить мега-портал с использованием GET параметров, которые он передавал из форм и ссылок на свой сервер.
Он мог динамически выводить страницы:
товары в своём магазине:
редкие записи в своём блоге:
Потом программист задумался о модульности. И подумал, что было бы неплохо взять только файл index.php (который, кстати, вписывать в адресную строку необязательно) и пускать все запросы через него:
и в конфигурации Nginx:
а регулярные выражения переместить в конфигурационные файлы приложения и ту же работу производить вручную в PHP скрипте. Кроме того, удобно разместить правила преобразования адресов в модулях, например правила для блога поместить в соответствующий файл modules/blog/routes.php :
Пишем своё приложение
Попробуем использовать этот подход в небольшом приложении.
Пусть у нас методы-действия для вывода ленты записей блога названы с префиксом action_ и собраны в виде функций в файле-контроллере list.php :
а действия для просмотра (а также, при желании, создания и редактирования) записи – в файле-контроллере post.php :
Аналогично мы можем создать модуль магазина shop с файлами-контроллерами catalog, product, cart, order с соответствующими функциями-действиями внутри.
Добавим настройки к нашему приложению:
И соберём наш парсер адресов в файле index.php :
То есть, если мы теперь зайдём по адресу
Модернизируем наш парсер дальше.
Передача параметров в маршрут
Ссылка кнопки удаления товара из корзины могла бы выглядеть так:
Конечно же, мы могли бы добавить значение номера товара в шаблон адреса:
и указывать номер прямо в пути
но это было бы слишком педантично и малополезно. Так что оставим первый вариант.
Опыт работы с регулярными выражениями может подсказать нам сократить несколько конкретных правил до одного универсального.
Будь у нас такая же возможность, мы могли бы заменить список похожих правил:
всего одной строкой:
Но, к сожалению, функция preg_replace поддерживает обращение к фрагментам только по порядковому номеру, например:
Устраним этот недостаток, а именно воспользуемся функцией str_replace :
Здесь мы операцией
заменяем подстановки на их значения.
Теперь при переходе по адресу
Теперь мы можем совершить чудо. Просто добавим в конец списка тройку правил:
Это будут запасные правила по умолчанию, позволяющие вызывать любое действие любого контроллера. С ними у нас заработает любой модуль, у которого нет своих правил.
Фактически, парсинг адресов на основе регулярных выражений у нас готов.
Достоинства обработки адресов в программном коде
Как мы упоминали, первая причина кроется в облегчении поддержки модульности. Разумнее хранить правила прямо в модуле и подключать к приложению динамически. Иначе бы приходилось каждый раз при подключении нового функционала вручную или в скрипте-установщике дописывать правила в файл ‘.htaccess`. А если используется не Apache, а иной сервер, то ручная правка неизбежна.
Если с этим доводом можно поспорить и нет ничего страшного в правке файлов, то другая причина развевает все сомнения. Эта причина – возможность обратного конструирования адресов на основе маршрутов.
Генерация адресов из маршрутов
Например, если у нас есть правило
то при заходе по адресу
у нас должен произойти разбор адреса и выполниться действие rss контроллера list модуля blog. Одновременно если мы напишем в шаблоне строку
должно произойти обратное преобразование, то есть результирующий HTML-код страницы должен содержать строку
Аналогично вызов функции create_url с дополнительными параметрами
должен найти подходящий шаблон
И в третьем случае
должно найтись совпадение с правилом
и сгенерироваться корректный адрес:
Как выход из данной ситуации можно придумать свой облегчённый язык, реализующий только некоторые необходимые функции, и сделать анализатор конкретно для него.
Первым делом, вместо неудобных конструкций (?P \d+) было бы приятнее использовать облегчённые
Теперь при разборе адреса можно простой заменой строки реализовать преобразование псевдошаблона в настоящее регулярное выражение.
Упрощение анализа шаблонов
Вторая сложность при парсинге регулярных выражений заключается в поддержке необязательных параметров. Например, вместо указания страницы как GET параметр
для поддержки ЧПУ для страниц
Аналогично мы могли бы добавить другие параметры. В парсере такого выражения нам бы пришлось разбираться во множестве скобок.
Это сложный синтаксис, но вовсе не обязательный. Вместо него мы можем использовать несколько простых для анализатора правил:
При наличии параметра page в аргументах функции при вызове create_url сработает первое правило. Иначе случится совпадение со вторым.
Введя свой микроязык можно отойти от настоящего синтаксиса регулярных выражений, упростить его и даже добавить свои идеи. Но это нам пока не нужно.
Теперь рассмотрим такое правило
Здесь в произвольном месте вне именованного параметра id добавлена группа (html|xml|json) для возможности принимать расширение (что может понадобиться для работы сайта по Ajax), но эта группа никак не названа.
При попытке открыть адрес http://site.com/blog/post/52.xml это правило вполне себе сработает, но попытка создать адрес по нему
ни к чему ни приведёт, так как наш анализатор не будет знать, какое расширение нужно указать после точки.
Таким образом, произвольная поддержка регулярных выражений в шаблонах правил может привести к невозможности конструирования адресов из этих шаблонов. Поэтому можно разработать язык шаблонов так, чтобы настоящие регулярные выражения можно было использовать не везде, а только внутри именованных параметров
. Другими словами, обеспечить, чтобы абсолютно все параметры были именованными.
В связи с этим данный шаблон должен выглядеть так:
и вызываться он должен с передачей значений всех имеющихся параметров
По этой же причине мы не можем указать необязательность параметра на нашем псевдоязыке вот так:
так как теперь мы знаем, что скобки не окажут должного эффекта. Система их примет за обычные символы и при генерации просто выведет как есть:
Так что если нужно сделать указание расширения необязательным, то создайте два правила:
При передаче идентификатора и расширения сработает первое, а только для идентификатора – второе.
Можно использовать и запасной работоспособный вариант:
Мы здесь намеренно разрешили использование регулярных выражений только для значений именованных параметров. Это привело к невозможности использовать их для самой строки шаблона, но защитило нас от создания негенерируемых правил и позволило значительно упростило анализ.
Теперь не составит труда выделить из шаблона все упоминаемые в нём параметры одним простым проходом функции preg_match_all или сгенерировать на основе шаблона настоящее регулярное выражение. Генерировать адрес по такому шаблону тоже достаточно легко.
Теперь мы можем продолжить исследование и перейти от процедурного подхода к объектно-ориентированному.
Объектно-ориентированная реализация маршрутизатора
Попробуем сейчас построить каркас, в который мы можем инкапсулировать наш код.
Первым делом, создадим сущность для оформления правила. Дополним для большего удобства наш класс некоторыми свойствами:
Теперь добавим в наше приложение менеджер адресов, который будет хранить список правил и обходить их при поиске нужного:
В самом приложении мы теперь можем использовать наш менеджер:
А в представлениях и контроллерах для построения ссылок использовать метод createUrl :
Обратите внимание, что в классе UrlRule много доступных для настройки опций. Специально для этого мы сделали наш менеджер таким, что он может получать не только пары
но и конфигурируемые записи с любым числом опций:
Свои классы правил маршрутизации
Порой указания простого маршрута не хватает для корректной работы приложения. Это может происходить при конфликтах нескольких похожих адресов.
Пусть, например, у нас статические страницы:
Оно будет срабатывать в последнюю очередь (если предшествующие ему правила не сработают). Но предположим, что у нас есть и стандартный правила, упоминаемые ранее. Если мы поместим наше правило после стандартного:
то при попытке открыть страницу
Если мы поставим правило для статических страниц выше стандартного:
то при заходе по адресу
тоже сработает первое правило, и вместо действия shop/default/index откроется страница shop, которой тоже не существует.
Это неудивительно, так как правила проверяются по списку сверху вниз, и если будет несколько подходящих правил, то выбор остановится на первом из них.
Обычного правила из простой пары шаблон-маршрут недостаточно для работы такого вывода страниц. Во избежание такой путаницы шаблон должен быть «умным», а именно должен проверять наличие страницы и срабатывать только тогда, когда страница существует.
Теперь добавим наше правило с указанием класса:
Вспомним, что наш менеджер производит перебор правил из своего массива rules по очереди:
а наш умный класс сам в методе createUrl генерирует адрес в нужном формате. Также в действии контроллера этот идентификатор благодаря коду
Что мы узнали
в нужном правиле или глобально для всего менеджера (реализовать такую глобальную опцию в классе UrlManager достаточно легко: добавить открытое свойство в класс UrlManager и брать его значение как значение по умолчанию внутри UrlRule), и адреса мгновенно поменяются. Это очень удобно.
Также мы теперь знаем, что в шаблонах правил разные системы маршрутизации могут применять либо «ненастоящий» язык регулярных выражений, то есть свой собственный упрощённый аналог, либо позволяют использовать регулярные выражения только в специфических местах. Поэтому не стоит удивляться, что некоторые символы в правилах разных маршрутизаторов не сработают и будут выведены как простой текст.
В следующей части мы с вами рассмотрим реализацию маршрутизации в Yii.
Не пропускайте новые статьи, бонусы и мастер-классы: