php многопоточный или однопоточный

Многопоточные вычисления в PHP: pthreads

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

php многопоточный или однопоточный. Смотреть фото php многопоточный или однопоточный. Смотреть картинку php многопоточный или однопоточный. Картинка про php многопоточный или однопоточный. Фото php многопоточный или однопоточный

Я рассматриваю такую задачу. Есть пул заданий, которые надо побыстрее выполнить. В PHP есть и другие инструменты для решения этой задачи, тут они не упоминаются, статья именно про pthreads.

Стоит отметить, что автор расширения, Joe Watkins, в своих статьях предупреждает, что многопоточность — это всегда не просто и надо быть к этому готовым.

Кто не испугался, идем далее.

Что такое pthreads

Pthreads — это объектно-ориентированное API, которое дает удобный способ для организации многопоточных вычислений в PHP. API включает в себя все инструменты, необходимые для создания многопоточных приложений. PHP-приложения могут создавать, читать, писать, исполнять и синхронизировать потоки с помощью объектов классов Threads, Workers и Threaded.

Что внутри pthreads

Иерархия основных классов, которые мы только что упомянули, представлена на диаграмме.
php многопоточный или однопоточный. Смотреть фото php многопоточный или однопоточный. Смотреть картинку php многопоточный или однопоточный. Картинка про php многопоточный или однопоточный. Фото php многопоточный или однопоточный

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 поток.

Проверим

Информация о процессоре, на котором запускал тесты

Посмотрим диаграмму загрузки ядер процессора. Тут все соответствует ожиданиям.

php многопоточный или однопоточный. Смотреть фото php многопоточный или однопоточный. Смотреть картинку php многопоточный или однопоточный. Картинка про php многопоточный или однопоточный. Фото php многопоточный или однопоточный

php многопоточный или однопоточный. Смотреть фото php многопоточный или однопоточный. Смотреть картинку php многопоточный или однопоточный. Картинка про php многопоточный или однопоточный. Фото php многопоточный или однопоточный

php многопоточный или однопоточный. Смотреть фото php многопоточный или однопоточный. Смотреть картинку php многопоточный или однопоточный. Картинка про php многопоточный или однопоточный. Фото php многопоточный или однопоточный

php многопоточный или однопоточный. Смотреть фото php многопоточный или однопоточный. Смотреть картинку php многопоточный или однопоточный. Картинка про php многопоточный или однопоточный. Фото php многопоточный или однопоточный

А теперь самое главное, ради чего все это. Сравним время выполнения.

$threadsПримечаниеВремя выполнения, секунд
PHP без ZTS
1без pthreads, без polyfill265.05
1polyfill298.26
PHP с ZTS
1без pthreads, без polyfill37.65
168.58
226.18
316.87
412.96
512.57
612.07
711.78
811.62

Из первых двух строк видно, что при использовании polyfill мы потеряли примерно 13% производительности в этом примере, это относительно линейного кода на совсем простом PHP “без всего”.

Далее, PHP с ZTS. Не обращайте внимание на такую большую разницу во времени выполнения в сравнении с PHP без ZTS (37.65 против 265.05 секунд), я не пытался привести к общему знаменателю настройки PHP. В случае без ZTS у меня включен XDebug например.

Как видно, при использовании 2-х потоков скорость выполнения программы примерно в 1.5 раза выше, чем в случае с линейным кодом. При использовании 4-х потоков — в 3 раза.

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

php многопоточный или однопоточный. Смотреть фото php многопоточный или однопоточный. Смотреть картинку php многопоточный или однопоточный. Картинка про php многопоточный или однопоточный. Фото php многопоточный или однопоточный

Резюме

В PHP возможна вполне элегантная работа с многопоточностью с использованием расширения pthreads. Это дает ощутимый прирост производительности.

Источник

Простой вариант реализации многопоточности на PHP

Многопоточность в PHP отсутствует «из коробки», поэтому вариантов её реализации было придумано великое множество, включая расширения pthreads, AzaThread (CThread), и даже несколько собственных наработок PHP программистов.

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

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

Итак, задача состоит в том, что бы обработать большое количество данных, пришедших в наш скрипт. Моей задачей было обработать JSON массив текстовой информации, переварив которую скрипт должен был собрать из неё не менее большой коммит для PostgreSQL.

Первым делом собираем данные в родительском файле:

Размер массива колебался около 400мб (позже сократился до

50мб), и вся информация была текстовой. Не сложно прикинуть скорость, с которой это всё переваривалось, а если учесть, что скрипт выполнялся по cron каждые 15 минут, а вычислительная мощность была такой себе — быстродействие страдало очень сильно.

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

Стоит сразу оговориться — такой расчет «в лоб» не даст точного результата по количеству элементов для каждого потока. Он нужен скорее для упрощения расчетов.

А теперь самая суть — создаем задачи для каждого потока и запускаем его. Делать мы это будем «в лоб» — создавая задачу для второго файла — thread.php. Он будет выступать в роли «потока», получая диапазон элементов для обработки и запускаясь независимо от основного скрипта:

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

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

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

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

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

Источник

Потоки выполнения и PHP

php многопоточный или однопоточный. Смотреть фото php многопоточный или однопоточный. Смотреть картинку php многопоточный или однопоточный. Картинка про php многопоточный или однопоточный. Фото php многопоточный или однопоточный

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

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

PHP очень далёк от других технологий. Например, в Java очень активно используются потоки выполнения, ещё они могут встречаться в пользовательских программах. В PHP такого нет. И тому есть причины.

Движок PHP обходится без потоков выполнения в основном ради упрощения структуры. Прочитав следующий раздел, вы узнаете, что потоки выполнения — не «магическая технология, позволяющая ускорить работу любой программы». Похоже на речь продавца, правда? Но мы не торговцы — мы технари, и мы знаем, о чём говорим. В настоящий момент в движке PHP нет потоков выполнения. Возможно, в будущем они появятся. Но это повлечёт за собой столько сложностей, что результат может оказаться далёк от ожидаемого. Главная трудность — кроссплатформенное многопоточное программирование (thread programming). Вторая трудность — общие ресурсы (shared resources) и управление блокировками. Третья — потоки выполнения подходят не для каждой программы. Архитектура PHP зародилась в районе 2000 года, а в то время потоковое программирование было малораспространённым и незрелым. Поэтому авторы PHP (преимущественно движка Zend) решили сделать цельный движок без потоков. Да и не было у них нужных ресурсов для создания стабильного кроссплатформенного многопоточного движка.

Кроме того, потоки выполнения невозможно применять в пользовательском пространстве PHP. Этот язык не так выполняет код. Концепция PHP — «выстрелил и забыл». Запрос должен обрабатываться как можно быстрее, чтобы освободить PHP для следующего запроса. PHP создан как связующий язык: вы не обрабатываете сложные задачи, требующие потоков. Вместо этого обращаетесь к fast-and-ready ресурсам, связываете всё воедино и отправляете обратно пользователю. PHP — язык действия, а если что-то требует на обработку «больше времени, чем обычно», то это нужно делать не в PHP. Поэтому для асинхронной обработки каких-то тяжёлых задач используется система на базе очередей (Gearman, AMQP, ActiveMQ и т. д.). В Unix принято делать так: «Разрабатывай маленькие, самодостаточные инструменты и связывай их друг с другом». PHP не рассчитан на активное распараллеливание, это удел других технологий. Каждой проблеме — правильный инструмент.

Несколько слов о потоках выполнения

Освежим в памяти, что такое потоки выполнения. Не будем углубляться в подробности, их вы найдёте в интернете и книгах.

Поток выполнения — «малая» единица обработки (light unit of work treatment), находящаяся внутри процесса. Процесс может создавать многочисленные потоки выполнения. Поток должен быть частью только одного процесса. Процесс — «большая» единица обработки в рамках операционной системы. На многоядерных (многопроцессорных) компьютерах несколько ядер (процессоров) работают параллельно и обрабатывают часть нагрузки исполняемых задач. Если процессы А и Б готовы к постановке в очередь и два ядра (процессора) готовы к работе, то А и Б должны быть одновременно отправлены в обработку. Тогда компьютер эффективно обрабатывает несколько задач в единицу времени (временной интервал, timeframe). Мы называем это «параллелизм».

php многопоточный или однопоточный. Смотреть фото php многопоточный или однопоточный. Смотреть картинку php многопоточный или однопоточный. Картинка про php многопоточный или однопоточный. Фото php многопоточный или однопоточный

php многопоточный или однопоточный. Смотреть фото php многопоточный или однопоточный. Смотреть картинку php многопоточный или однопоточный. Картинка про php многопоточный или однопоточный. Фото php многопоточный или однопоточный

php многопоточный или однопоточный. Смотреть фото php многопоточный или однопоточный. Смотреть картинку php многопоточный или однопоточный. Картинка про php многопоточный или однопоточный. Фото php многопоточный или однопоточный

Раньше A и Б были процессами: полностью независимыми обработчиками. Но потоки выполнения — это не процессы. Потоки — это единицы, живущие внутри процессов. То есть процесс может распределить работу по нескольким более мелким задачам, выполняемым одновременно. К примеру, процессы А и Б могут породить потоки А1, А2, Б1 и Б2. Если компьютер оснащён несколькими процессорами, например восемью, то все четыре потока могут выполняться в одном временном интервале (timeframe).

Потоки выполнения — это способ разделения работы процесса на несколько мелких подзадач, решаемых параллельно (в одном временном интервале). Причём потоки выполняются примерно так же, как и процессы: диспетчер программного потока ядра (Kernel thread scheduler) управляет потоками с помощью состояний.

php многопоточный или однопоточный. Смотреть фото php многопоточный или однопоточный. Смотреть картинку php многопоточный или однопоточный. Картинка про php многопоточный или однопоточный. Фото php многопоточный или однопоточный

Потоки выполнения легче процессов, им для работы нужен лишь стек и несколько регистров. А процессам требуется много всего: новый кадр виртуальной машины (VM frame) от ядра, куча, разная сигнальная информация, информация о файловых дескрипторах, блокировках и т. д.

Память процесса управляется на аппаратном уровне ядром и MMU, а память потока выполнения — на программном уровне программистом и потоковыми библиотеками (threading library).

Так что запомните: потоки выполнения легче процессов и проще управляются. При грамотном использовании они ещё и работают быстрее процессов, поскольку ядро ОС почти не вмешивается в управление потоками и их диспетчеризацию.

Схема памяти потоков выполнения

У потоков выполнения есть свой стек. Поэтому при обращении к переменным, объявленным в функциях, они получают собственную копию этих данных.

Куча процесса совместно используется потоками выполнения, как и глобальные переменные, и файловые дескрипторы. Это и преимущество, и недостаток. Если мы только считываем из глобальной памяти, то нужно это делать вовремя. Например, после потока Х и до потока Y. Если мы пишем в глобальную память, то стоит удостовериться, что туда же и в то же время не попытаются записать несколько потоков. Иначе эта область памяти окажется в непредсказуемом состоянии — так называемом состоянии гонки (race condition). Это главная проблема в потоковом программировании.

На случай одновременного доступа вам нужно внедрить в код некоторые механизмы вроде повторного входа (reentrancy) или подпрограмм синхронизации (synchronization routine). Повторный вход нарушает согласованность (concurrency). А синхронизация позволяет управлять согласованностью предсказуемым образом.

Процессы не используют память совместно, они идеально изолированы на уровне ОС. А потоки выполнения внутри одного процесса совместно используют большой объём памяти.

Поэтому им нужны инструменты синхронизации доступа к общей памяти, например семафоры и мьютексы (mutexes). Работа этих инструментов основана на принципе «блокировки»: если ресурс заблокирован, а поток пытается получить к нему доступ, то по умолчанию поток будет ожидать разблокировки ресурса. Поэтому потоки выполнения сами по себе не сделают вашу программу быстрее. Без эффективного распределения задач по потокам и управления блокировками общей памяти ваша программа станет работать ещё медленнее, чем при использовании одного процесса без потоков выполнения. Просто потоки будут постоянно ожидать друг друга (и я ещё не говорю о взаимоблокировках, голодании и т. д.).

Если у вас нет опыта в потоковом программировании, то оно окажется для вас непростым занятием. Чтобы наработать опыт работы с потоками выполнения, понадобится много часов практики и решения WTF-моментов. Стоит забыть о какой-то мелочи — и вся программа пойдёт в разнос. Труднее отлаживать программу с потоками, чем без них, если мы говорим о реальных проектах с сотнями или тысячами потоков в одном процессе. Вы сойдёте с ума и просто утонете во всём этом.

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

Такая схема совместного использования памяти потоками не всегда удобна. Поэтому появилось локальное хранилище потока (Thread Local Storage, TLS). TLS можно описать как «глобалы, принадлежащие какому-то одному потоку и не используемые другими». Это области памяти, отражающие глобальное состояние, приватные для конкретного потока выполнения (как в случае использования одних лишь процессов). При создании потока выделяется часть кучи процесса — хранилище. У потоковой библиотеки запрашивается ключ, который ассоциируется с этим хранилищем. Он должен использоваться потоком выполнения при каждом обращении к своему хранилищу. Для уничтожения выделенных ресурсов в конце жизни потока требуется деструктор.

Приложение «потокобезопасно» (thread safe), если каждое обращение к глобальным ресурсам находится под полным контролем и полностью предсказуемо. В противном случае вам перейдёт дорогу диспетчер (scheduler): будут неожиданно выполняться какие-то задачи, и производительность упадёт.

Потоковые библиотеки

Потокам выполнения нужна помощь ядра ОС. В операционных системах потоки выполнения появились в середине 1990-х, так что методики работы с ними отшлифованы.

Но существуют проблемы кроссплатформенности. Особенно много различий между Windows- и Unix-системами. В этих экосистемах приняты разные модели потокового выполнения и используются разные потоковые библиотеки.

В Linux для создания потока или процесса ядро осуществляет системный вызов clone(). Но он невероятно сложен, поэтому для облегчения повседневного потокового программирования системные вызовы используют код на Си. Libc до сих пор не управляет потоковыми операциями (подобную инициативу демонстрирует стандартная библиотека из С11), этим занимаются внешние библиотеки. Сегодня в Unix-системах обычно применяется pthread (есть и другие библиотеки). Pthread — сокращение от Posix threads. Эта POSIX-нормализация использования потоков и их поведения берёт своё начало в 1995-м. Если вам нужны потоки выполнения, подключите библиотеку libpthread: передайте в GCC -lpthread. Она написана на С, её код открыт, есть собственный механизм контроля и управления версиями.

Итак, в Unix-системах чаще всего используется библиотека pthread. Она обеспечивает согласованность (concurrency), а параллелизм зависит от конкретной ОС и компьютера.

Согласованность — это когда несколько потоков беспорядочно выполняются на одном процессоре. Параллелизм — это когда несколько потоков одновременно выполняются на разных процессорах.

php многопоточный или однопоточный. Смотреть фото php многопоточный или однопоточный. Смотреть картинку php многопоточный или однопоточный. Картинка про php многопоточный или однопоточный. Фото php многопоточный или однопоточный

php многопоточный или однопоточный. Смотреть фото php многопоточный или однопоточный. Смотреть картинку php многопоточный или однопоточный. Картинка про php многопоточный или однопоточный. Фото php многопоточный или однопоточный

PHP и потоки выполнения

Для начала вспомним:

Так что насчёт потоков выполнения в PHP?

Как PHP обрабатывает запросы

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

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

В такой ситуации PHP ничем не может помочь: процессы полностью изолированы. Процесс А, обрабатывающий запрос А о данных клиента А, не сможет взаимодействовать (читать или писать) с процессом Б, обрабатывающим запрос Б клиента Б. Нам это и нужно.

В 98 % случаев используются две архитектуры: php-fpm и Apache с mpm_prefork.

Под Windows всё сложнее, как и в Unix-серверах с потоками выполнения.

Windows — действительно замечательная ОС. У неё лишь один недостаток — закрытый исходный код. Но в сети или в книгах можно найти информацию о внутреннем устройстве многих технических ресурсов. Инженеры Microsoft много рассказывают о том, как работает Windows.

У Windows другой подход к согласованности и параллелизму. Эта ОС очень активно использует потоки выполнения. По сути, создание процесса в Windows — такая тяжёлая задача, что обычно её избегают. Вместо этого всегда и везде применяют потоки выполнения. Потоки в Windows на порядок мощнее, чем в Linux. Да, именно так.

Когда PHP работает под Windows, веб-сервер (любой) будет обрабатывать клиентские запросы в потоках, а не процессах. То есть в таком окружении PHP выполняется в потоке. И поэтому ему стоит особенно тщательно подходить к спецификациям потоков: он должен быть потокобезопасным (thread safe).

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

За эту защиту отвечает уровень Zend Thread Safety (ZTS, потокобезопасность Zend).

Обратите внимание, что всё то же самое верно и под Unix, если вы решите использовать потоки выполнения для распараллеливания обработки клиентских запросов. Но для Unix-систем это очень необычная ситуация, поскольку для таких задач здесь традиционно используются классические процессы. Хотя никто не мешает выбрать потоки, это способно повысить производительность. Потоки легче процессов, так что система может выполнять гораздо больше потоков. Кроме того, если вашему PHP-расширению нужна потокобезопасность (вроде ext/pthread), то вам потребуется и потокобезопасный PHP.

Подробности реализации ZTS

ZTS активируется с помощью —enable-maintainer-zts. Обычно вам не нужен этот переключатель, если вы не запускаете PHP под Windows или не запускаете PHP с расширением, для работы которого необходима потокобезопасность движка.

Есть ряд способов проверки текущего режима работы. CLI и php –v скажут вам, что сейчас активирован NTS (Not Thread Safe) или ZTS (Zend Thread Safe).

php многопоточный или однопоточный. Смотреть фото php многопоточный или однопоточный. Смотреть картинку php многопоточный или однопоточный. Картинка про php многопоточный или однопоточный. Фото php многопоточный или однопоточный

Также можно воспользоваться phpinfo() :

php многопоточный или однопоточный. Смотреть фото php многопоточный или однопоточный. Смотреть картинку php многопоточный или однопоточный. Картинка про php многопоточный или однопоточный. Фото php многопоточный или однопоточный

Можете в своём коде считать константу PHP_ZTS из PHP.

При компилировании с ZTS вся основа PHP становится потокобезопасной. Но активированные расширения при этом могут не быть потокобезопасными. Все официальные расширения (распространяемые с PHP) безопасны, но за сторонние поручиться нельзя. Ниже вы увидите, что освоение потокобезопасности расширений PHP требует особого использования API. И, как это постоянно происходит с потоками: одно упущение — и весь сервер может посыпаться.

При использовании потоков выполнения, если вы не вызываете реентерабельные функции (обычно из libc) или вслепую обращаетесь к истинной глобальной переменной (true global variable), это приведёт к странному поведению во всех одноуровневых потоках (sibling threads). Например, накосячите с потоками в одном расширении — и это повлияет на каждого клиента, обслуживаемого во всех потоках выполнения на сервере! Кошмарная ситуация: один клиент может испортить все остальные клиентские данные.

При проектировании расширений PHP:

Реентерабельные функции

При проектировании расширения PHP используйте реентерабельные функции: функции, работа которых не зависит от глобального состояния. Хотя это слишком упрощённо. Если подробнее, то реентерабельные функции могут вызываться, пока не завершился их предыдущий вызов. Они способны работать параллельно в двух и более потоках выполнения. Если бы они использовали глобальное состояние, то не были бы реентерабельными. Однако они могут блокировать собственное глобальное состояние, а значит, быть потокобезопасными 😉 Многие традиционные функции из libc нереентерабельны, потому что создавались, когда ещё не придумали потоки выполнения.

strtok() => strtok_r(); strerror(), strerror_r(); readdir() => readdir_r() — и т. д.

Сам PHP предоставляет некоторые функции в основном для кроссплатформенного использования. Взгляните на main/reentrancy.c.

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

Не привязывайтесь к потоконебезопасным библиотекам

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

Если ваше расширение привязано к библиотеке, которая точно потоконебезопасна, то придётся разработать собственные способы обеспечения потокобезопасности, чтобы защитить в библиотеке доступ к глобальному состоянию. В потоковом программировании и С такое бывает часто, но легко упускается из виду.

Использование ZTS

ZTS — это уровень кода, контролирующий доступ к глобальным потоковым переменным с помощью TLS (Thread Local Storage) в PHP 7.

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

Есть истинные глобалы (true globals), представляющие собой просто традиционные глобальные переменные С. У них всё в порядке с архитектурой, но поскольку мы не защитили их от согласованности в потоках, то можем только считывать их, когда PHP обрабатывает запросы. Истинные глобалы создаются и записываются до того, как будет создан хотя бы один поток выполнения. По внутренней терминологии PHP этот шаг называется модульной инициализацией (module init). Это хорошо видно на примере расширений:

Этот псевдокод показывает, как может выглядеть любое PHP-расширение. Расширения имеют несколько перехватчиков (hooks), инициализируемых в течение жизненного цикла PHP. Перехватчик MINIT() относится к инициализации PHP. При этой процедуре запускается PHP и можно безопасно читать глобальную переменную или писать в неё, как в приведённом примере.

Второй важный перехватчик — RINIT(), инициализация запроса. Эта процедура вызывается для каждого расширения, при обработке каждого нового запроса. То есть RINIT() может вызываться расширением тысячи раз. На этом этапе PHP уже уходит в поток. Веб-сервер разобьёт изначальный процесс на потоки, поэтому в RINIT() необходима потокобезопасность. Это совершенно логично в ситуации, когда создаются потоки для одновременной обработки нескольких запросов. Не забывайте — вы не создаёте потоки. Вместо PHP потоки создаёт веб-сервер.

Также мы используем потоковые глобалы (thread globals). Это глобальные переменные, чья потокобезопасность обеспечивается уровнем ZTS:

Необходимость макросов

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

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

Для этого используются макросы.

Макрос WOW_G() будет обрабатываться разными способами, в соответствии с работой многозадачного движка PHP (процессы или потоки). Вы можете на это влиять, перекомпилируя своё расширение. Поэтому расширения PHP несовместимы при переключении между режимами ZTS и неZTS. Несовместимы на уровне двоичного кода (binary incompatible)!

ZTS несовместим на уровне двоичного кода с неZTS. При переключении с одного режима на другой нужно перекомпилировать исключения.

При работе в процессе макрос WOW_G() обычно обрабатывается так:

При работе в потоке:

В ZTS-режиме сложнее.

Тогда макрос обрабатывается так:

И всё. При работе в процессе — ничего сложного.

Но при работе в потоке — с использованием ZTS — у нас больше нет истинных глобалов С. Но объявления глобалов выглядят так же:

В ZTS и неZTS глобалы объявляются одинаково.

Уровень TSRM

ZTS использует так называемый уровень TSRM — Thread Safe Resource Manager. Это просто кусок кода на С, ничего более!

Именно благодаря уровню TSRM возможна работа ZTS. По большей части он находится в папке /TSRM исходного кода PHP.

При анализе TSRM мы будем обсуждать только реализацию на основе pthreads.

Загрузка TSRM

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

Ресурсы TSRM

Когда уровень TSRM загружен, нужно добавить в него новые ресурсы. Под ресурсом подразумевается область памяти, содержащая набор глобальных переменных, обычно относящихся к расширению PHP. Ресурс должен принадлежать текущему потоку выполнения или быть защищённым для доступа.

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

Уровень TSRM передаёт ресурсу уникальный ID. Затем вызывающая функция (caller) должна сохранить этот ID, поскольку он потом понадобится для возвращения защищённой области памяти из TSRM.

TSRM-функция, создающая новый ресурс:

Как видите, этой функции нужна взаимоисключающая блокировка (mutex lock). Если она вызывается в дочернем потоке выполнения (а она будет вызвана в каждом из них), то заблокирует другие потоки, пока не закончит манипулировать глобальным состоянием хранилища потока (global thread storage state).

Запуск запроса

Кеш локального хранилища расширений

Каждое расширение в PHP 7 может объявить свой кеш в локальном хранилище. Это означает, что при запуске каждого нового потока выполнения каждое расширение должно считывать область локального хранилища своего собственного потока выполнения, а не итерировать по списку хранилищ при каждом обращении к глобалу (global access). Тут нет никакой магии, для этого нужно выполнить несколько вещей.

Для начала вы должны компилировать PHP с поддержкой кеша: введите в командной строке компиляции -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1. В любом случае это должно делаться по умолчанию. Далее, при объявлении глобалов вашего расширения используйте макрос ZEND_TSRMLS_CACHE_DEFINE() :

#define ZEND_TSRMLS_CACHE_DEFINE(); __thread void *_tsrm_ls_cache = ((void *)0);

Затем нужно заполнить это хранилище void* данными из хранилища, зарезервированного для ваших глобалов уровнем TSRM. Для этого в конструкторе глобалов можете использовать ZEND_TSRMLS_CACHE_UPDATE() :

php многопоточный или однопоточный. Смотреть фото php многопоточный или однопоточный. Смотреть картинку php многопоточный или однопоточный. Картинка про php многопоточный или однопоточный. Фото php многопоточный или однопоточный

Как мы уже видели, каждое расширение является ресурсом и предоставляет какое-то пространство для своих глобалов. Для возвращения хранилища конкретному расширению используется ID. TSRM создаст это хранилище для текущего потока выполнения, когда появится новый запрос/поток.

Заключение

Программирование потоков выполнения — задача непростая. Здесь я лишь показал вам, как PHP работает с управлением глобалами: он изолирует каждое глобальное хранилище с помощью TLS, создаваемого для каждого нового потока выполнения при запуске запроса, движком или выделенным уровнем — TSRM. Он блокирует мьютекс, создаёт хранилище для глобалов текущего потока, а затем разблокирует мьютекс. Таким образом, каждое расширение и каждая часть PHP может обращаться к своему собственном хранилищу без необходимости блокировать мьютекс при каждом доступе.

Всё становится абстрактным за пределами уровня TSRM: уровня С-кода, облегчающего управление глобалами, особенно для создателей расширений. Для обращения к своему глобальному пространству вы используете макрос, и если вы работаете в ZTS, то этот макрос будет преобразован в специфический код для обращения только к вашему маленькому хранилищу посреди каждого расширения. Благодаря кешу TSRM при каждом обращении к глобалу вам не нужен поиск: вам предоставили указатель на ваше конкретное хранилище, кешируйте его и берите снова, когда нужно обратиться к глобалу.

Очевидно, что всё это справедливо для глобалов, относящихся к запросу (request-bound). Вы по-прежнему можете использовать настоящие С-глобалы, но не пытайтесь писать в них, применяя servinf к запросу: вы столкнётесь со странным поведением, плохо поддающимся отладке, или даже порушите весь веб-сервер.

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *