Thread safe php что это
Что такое потокобезопасный или не потокобезопасный в PHP?
Потокобезопасный: он используется для того, чтобы гарантировать, что когда общая структура данных, которой манипулируют разные потоки, не может попасть в состояние гонки. Потоковая безопасность рекомендуется, когда веб-сервер одновременно запускает несколько потоков выполнения для разных запросов. В Thread Safety бинарный может работать в контексте многопоточного веб-сервера. Потокобезопасность работает путем создания копии локального хранилища в каждом потоке, чтобы данные не сталкивались с другим потоком.
Например:
Не поточно-ориентированный: он не проверяет безопасность потоков, что делает его более быстрым, но в то же время он становится более нестабильным и очень часто выходит из строя. Это относится только к одному потоку. В не поточно-ориентированной версии двоичные файлы широко используются при взаимодействии с веб-сервером по протоколу FastCGI, не используя многопоточность.
Например:
Так что это зависит от того, как вы хотите использовать PHP. AFAIR запуск PHP с fastCGI является предпочтительным способом. Если вам неизвестно, какая версия PHP установлена в вашей системе, есть простой способ узнать это.
Проверьте версию установленного PHP Thread Safe или Non Thread Safe:
Откройте phpinfo () и найдите строку Thread thread для многопоточной сборки, которую вы должны найти enable.
Бесплатные уроки программирования на PHP. Интерпретируемый язык программирования PHP. Уроки для начинающих разработчиков сайтов и веб-страниц.
Где скачать и как установить PHP? Инструкция по установке PHP 5.6 на Windows без использования Apache на встроенный в PHP сервер
Привет, посетитель сайта ZametkiNaPolyah.ru! Продолжаем рубрику Веб-программирование и ее раздел PHP. Эта запись является своеобразной вводной инструкцией для новичка по установке PHP 5.6 на Winodws 10. Причем устанавливать PHP на Windows мы будем без использования Apache, вместо него мы будем использовать встроенный в PHP веб-сервер. Сразу оговорюсь, что устанавливать PHP без Apache на Windows даже в ознакомительных целях я бы не рекомендовал, о причинах мы поговорим в конце данной записи, поэтому считайте данную статью ознакомительной.
Где скачать и как установить PHP? Инструкция по установке PHP 5.6 на Windows без использования Apache на встроенный в PHP сервер
Итак, аннотация к нашей инструкции для новичка по установки PHP на Windows без Apache:
Где скачать PHP 5.6?
Итак, давайте разберемся сперва с вопросом: где скачать PHP 5.6. Отметим, что PHP – это бесплатная программная среда, поэтому если вы найдете в сети Интернет ресурс, который предлагает скачать PHP за деньги, знайте, что это мошенники. Скачать PHP, как и многие программные продукты можно с официального сайта. Причем абсолютно бесплатно. Ниже вы можете увидеть скрин страницы, сделанный с официального сайта, с которой можно скачать PHP различных версий.
Страница выбора версии PHP для скачивания и последующей установки
В данном случае нас интересует версия PHP 5.6 и ее реализация для компьютеров с операционной системой Windows на борту, нажав на кнопку «Download Windows», вы перейдете на страницу, в которой сможете выбрать версию PHP, соответствующую версии вашей ОС и ее разрядности, смотрите скрин ниже.
Страница, на которой можно выбрать версию PHP, соответствующую версии ОС и ее разрядности для скачивания и последующей установки
Я буду скачивать PHP VC11 x64 Thread Safe, так как у меня 64 битная Windows 10. Отмечу: если вы счастливый обладатель Windows XP, то, скорее всего, вы не сможете установить PHP версий 5.5 или 5.6. Скачивать нужно zip архив с файлами PHP, ведь мы же не хотим заниматься еще и компиляцией PHP из исходных файлов.
Разница между Thread Safe и Non Thread Safe в PHP
Итак, когда мы скачивали PHP, то видели, что выбор версии PHP, которую необходимо скачать зависит не только от операционной системы и ее разрядности, но и от какого-то непонятного Thread Safe и Non Thread Safe. Давайте разберемся в чем разница между Thread Safe и Non Thread Safe в PHP. Хотя предыдущая фраза звучит не совсем корректно, так как Thread Safe скорее относится к операционной системе, нежели к языку PHP.
На самом деле – это исторический момент для языка PHP. С октября 2000 года, а именно тогда появился PHP 3.0.17, именно эта версия PHP стала работать на машинах под управлением операционных систем семейства Windows. Появилось разделение версий PHP на Thread Safe и Non Thread Safe. Раньше все версии PHP были Thread Safe.
А теперь о том в чем разница между этими версиями сборки PHP. Вся соль в том, что ОС Windows использует мульти-поточную архитектуру работы, а операционные системы семейства UNIX поддерживают мульти-процессовую архитектуру. Если вы выберете версию PHP, скомпилированную, как мульти-процессовое CGI-приложения, то у вас возникнет много проблем в работе ваших PHP приложений в том случае, если вы будете устанавливать PHP как модуль веб-сервера Apache или IIS.
Всё дело в том, что HTTP сервер в Windows, в частности веб-сервер Apache работает в несколько потоков и PHP, установленный как модуль сервера Apache, будет являться одним из его потоков. Следовательно, PHP для правильной работы с Apache, как его модуль, должен быть скомпилирован именно в режиме Thread Safe.
Немного полезной информации о PHP перед его установкой
Итак, мы не только определились с тем какую версию PHP скачать, но и посмотрели на разницу между версиями Thread Safe и Non Thread Safe. Для начала вспомним публикацию о том, что такое PHP, в которой мы говорили о возможностях данного интерпретируемого языка:
Нас интересует больше всего первая функция данного языка программирования. Для этого нам потребуется:
Кстати, вместо установки всех этих компонентов (за исключением браузера) по отдельности, вы можете использовать готовые сборки такие как Денвер или локальный веб-сервер AMPPS. Веб-сервер в данном случае нам нужен для того, чтобы реализовать архитектуру клиент-сервер, так как PHP в данном случае выступает как серверная технология.
Таким образом происходит деление функций на функции клиента и функции сервера. В данном случае происходит взаимодействие по протоколу HTTP, который относится к седьмому уровню семиуровневой модели сетевого взаимодействия OSI. Функции клиента выполняет браузер, который посылает запросы серверу. Сервер понимает, что и в каком виде от него хочет клиент по специальным HTTP заголовкам и методам HTTP протокола. Результаты своей работы сервер отправляет клиенту специальным HTTP сообщением, которое получило название ответ сервера. Ответы сервера снабжены так же заголовками, но еще у них есть особые коды состояния, по которым браузер видит, как его понял сервер и что еще нужно сделать, чтобы достичь желаемого результата.
Отметим, что перед тем, как начать обмениваться сообщениями, браузер и сервер устанавливают HTTP соединение, которое является постоянным. Также у сообщений есть тело или объект HTTP сообщения, который содержит полезную для человека информацию, обычно (но не всегда) в качестве объекта сообщения выступает HTML документ или его фрагмент, со структурой которого мы познакомились ранее.
Заметим, что этот процесс необязательно происходит локально в рамках одной машины, чаще всего такое общение между сервером (серверной технологией) и клиентом (браузером) происходит в сети Интернет, когда вы выбрали и купили хостинг, а также выбрали доменное имя сайта.
Ранее мы разобрались с тем, что PHP может работать как модуль сервера, а может быть собран и установлен как обработчик CGI или FastCGI, во втором случае ваш веб-сервер должен исполнять PHP скрипты как CGI-скрипты. Если вы хотите использовать PHP в командной строке вашей ОС, то вам понадобится PHP CLI. Это не входит в рамки данной публикации, поэтому подробно не будем останавливаться, лишь скажем, что в этом случае нам не потребуется ни браузер, ни веб-сервер.
PHP позволяет создавать приложения с графическим интерфейсом при помощи специального расширения PHP-GTK, тем самым вы сможете управлять окнами на вашем компьютере. Данный подход принципиально отличается от веб-разработки.
Краткая инструкция по установке PHP на Windows 10 без Apache на встроенный сервер
Мы разобрались с версиями PHP их особенностями и поговорили о принципах работы серверных технологий, также мы выбрали версию PHP, которую скачали. Самое время поговорить о том, как установить PHP 5.6 на Windows 10. Заметим, что устанавливать PHP мы будем без использования отдельного веб-сервера, так как начиная с PHP 5.4 в PHP есть свой собственный веб-сервер, который подходит для демонстрационных целей и изучения основ интерпретируемого языка PHP. В отдельной публикации мы поговорим про установку PHP как модуль сервера Apache 2.4. В этой же публикации мы установим PHP без Apache на Windows.
Надеемся, что вы уже скачали нужную вам версию PHP и мы можем начать установку. Установку PHP на Windows мы будем делать по шагам.
Шаг первый. Нам необходимо распаковать скаченный архив в любое удобное место на диске. В нашем случае это будет: c:\PHP\.
Шаг второй. Теперь нам нужно добавить путь к корневому каталогу PHP в переменную PATH. Мы уже подробно рассматривали добавления значения переменной PATH на Windows 7, когда говорили про установку библиотеки SQLite на Windows, а про добавления значения переменной PATH на Windows 10 мы говорили тогда, когда рассматривали установку SASS на Windows. В связи с этим здесь мы этот вопрос рассматривать не будем.
Шаг четвертый. После запуска сервера, встроенного в PHP вы увидите следующее сообщение в командой строке:
What is thread safe or non-thread safe in PHP?
I saw different binaries for PHP, like non-thread or thread safe?
What does this mean?
What is the difference between these packages?
4 Answers 4
Needed background on concurrency approaches:
There are also other completely different concurrency models (using Asynchronous sockets and I/O), as well as ones that mix two or even three models together. For the purpose of answering this question, we are only concerned with the two models above, and taking Apache HTTP server as an example.
Needed background on how PHP «integrates» with web servers:
There are other methods for chaining PHP with Apache and other web servers, but mod_php is the most popular one and will also serve for answering your question.
You may not have needed to understand these details before, because hosting companies and GNU/Linux distros come with everything prepared for us.
Now, onto your question!
At this point, you should be thinking «OK, so if I’m using a multi-threaded web server and I’m going to embed PHP right into it, then I must use the thread-safe version of PHP». And this would be correct thinking. However, as it happens, PHP’s thread-safety is highly disputed. It’s a use-if-you-really-really-know-what-you-are-doing ground.
Final notes
In case you are wondering, my personal advice would be to not use PHP in a multi-threaded environment if you have the choice!
Потоки выполнения и 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). Мы называем это «параллелизм».
Раньше A и Б были процессами: полностью независимыми обработчиками. Но потоки выполнения — это не процессы. Потоки — это единицы, живущие внутри процессов. То есть процесс может распределить работу по нескольким более мелким задачам, выполняемым одновременно. К примеру, процессы А и Б могут породить потоки А1, А2, Б1 и Б2. Если компьютер оснащён несколькими процессорами, например восемью, то все четыре потока могут выполняться в одном временном интервале (timeframe).
Потоки выполнения — это способ разделения работы процесса на несколько мелких подзадач, решаемых параллельно (в одном временном интервале). Причём потоки выполняются примерно так же, как и процессы: диспетчер программного потока ядра (Kernel thread scheduler) управляет потоками с помощью состояний.
Потоки выполнения легче процессов, им для работы нужен лишь стек и несколько регистров. А процессам требуется много всего: новый кадр виртуальной машины (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 будет обрабатывать 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).
Также можно воспользоваться phpinfo() :
Можете в своём коде считать константу 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() :
Как мы уже видели, каждое расширение является ресурсом и предоставляет какое-то пространство для своих глобалов. Для возвращения хранилища конкретному расширению используется ID. TSRM создаст это хранилище для текущего потока выполнения, когда появится новый запрос/поток.
Заключение
Программирование потоков выполнения — задача непростая. Здесь я лишь показал вам, как PHP работает с управлением глобалами: он изолирует каждое глобальное хранилище с помощью TLS, создаваемого для каждого нового потока выполнения при запуске запроса, движком или выделенным уровнем — TSRM. Он блокирует мьютекс, создаёт хранилище для глобалов текущего потока, а затем разблокирует мьютекс. Таким образом, каждое расширение и каждая часть PHP может обращаться к своему собственном хранилищу без необходимости блокировать мьютекс при каждом доступе.
Всё становится абстрактным за пределами уровня TSRM: уровня С-кода, облегчающего управление глобалами, особенно для создателей расширений. Для обращения к своему глобальному пространству вы используете макрос, и если вы работаете в ZTS, то этот макрос будет преобразован в специфический код для обращения только к вашему маленькому хранилищу посреди каждого расширения. Благодаря кешу TSRM при каждом обращении к глобалу вам не нужен поиск: вам предоставили указатель на ваше конкретное хранилище, кешируйте его и берите снова, когда нужно обратиться к глобалу.
Очевидно, что всё это справедливо для глобалов, относящихся к запросу (request-bound). Вы по-прежнему можете использовать настоящие С-глобалы, но не пытайтесь писать в них, применяя servinf к запросу: вы столкнётесь со странным поведением, плохо поддающимся отладке, или даже порушите весь веб-сервер.