Статьи

Обновление с Pthreads v2 до v3: на что обратить внимание

Изрядное количество изменилось для расширения pthreads с выпуском pthreads v3. Эта статья призвана охватить необходимую информацию для тех, кто хочет обновить свои приложения с pthreads v2 до v3.

Если вы не знакомы с pthreads, проверьте мое введение в pthreads !

Большое спасибо Джо Уоткинсу за корректуру и помощь в улучшении моей статьи!

Абстрактное изображение параллельных дорожек с наложенными номерами 2 и 3, указывающими на изменение версии

Общие изменения

В pthreads v3 было внесено несколько общих изменений. Первое и, пожалуй, самое заметное, это то, что pthreads нельзя использовать ни в какой среде, кроме интерфейса командной строки. Он никогда не предназначался для использования в среде веб-сервера (т. Е. В процессе FCGI) из-за проблем безопасности и масштабирования, поэтому теперь совет от pthreads v2 был реализован.

Также произошли некоторые изменения в рабочих. Ранее необходимо было отслеживать рабочие объекты, переданные рабочим, иначе, если они были уничтожены до того, как были выполнены рабочим потоком, возникла ошибка сегментации. Это было хорошо известным поведением, которое было кратко продемонстрировано в многопоточности в PHP со списком pthreads со следующим фрагментом:

class W extends Worker { public function run(){} } class S extends Stackable { public function run(){} } /* 1 */ $w = new W(); /* 2 */ $j = array( new S(), new S(), new S() ); /* 3 */ foreach ($j as $job) $w->stack($job); /* 4 */ $j = array(); $w->start(); $w->shutdown(); 

Это больше не проблема, потому что сами рабочие теперь отслеживают сложенные рабочие объекты.

Кроме того, в pthreads v3 произошли некоторые изменения в значении модификаторов методов. В pthreads v2 модификаторы методов имели особое значение в контексте потоковых объектов. В частности, защищенные методы имели неявный синхронизированный доступ (позволяющий безопасно выполнять их в нескольких контекстах), а частные методы могли выполняться только в контексте, к которому они привязаны. Эти отличающиеся семантики были теперь удалены из-за проблем надежности.

Например, возьмем следующий фрагмент:

 class ExampleThread extends Thread { public $value = 0; public function run() { $this->exclusive(); } protected function exclusive() { for ($i = 0; $i < 10000; ++$i) { ++$this->value; } } } class Test extends ExampleThread { public function callExclusive() { $this->exclusive(); } }; $thread = new Test(); $thread->start(); $thread->callExclusive(); $thread->join(); var_dump($thread->value); 

В pthreads v2 вызов ExampleThread::exclusive из основного и нового контекста потока был безопасным. Выходное значение в конце скрипта всегда будет int(20000) . Но в pthreads v3 это значение может быть любым от 1 до 20000 из-за условий состязания между двумя несинхронизированными циклами for .

Чтобы добиться точно такого же поведения в pthreads v3, мы должны явно синхронизировать доступ, используя встроенный метод Threaded::synchronized . Это нужно применять только к телу метода ExampleThread::exclusive :

 protected function exclusive() { $this->synchronized(function () { for ($i = 0; $i < 10000; ++$i) { ++$this->value; } }); } 

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

Удаленные Классы

Классы Mutex и Cond были удалены. Это связано с тем, что их функциональность не нужна из-за функций синхронизации, уже предоставленных классом Threaded . Использование взаимных исключающих блокировок и условий в PHP-коде также никогда не было особенно безопасным, поскольку взаимные блокировки могли легко возникнуть из ошибочного кода.

Класс Collectable который расширил Threaded , также был удален. Теперь у нас есть интерфейс Collectable который реализован с помощью Threaded . Интерфейс обеспечивает isGarbage метод isGarbage . Метод setGarbage больше не нужен, потому что pthreads автоматически обрабатывает, когда задача должна считаться мусором (когда задача завершена). Класс Threaded реализует метод Threaded::isGarbage по умолчанию, который следует использовать в подавляющем большинстве случаев. Реализация по умолчанию всегда возвращает true , так как любая задача в очереди задач является мусором (задача не может быть собрана перед выполнением). Только в редких случаях может потребоваться пользовательская реализация, поэтому переопределение метода Threaded::isGarbage должно быть редкостью.

Ниже приведен краткий пример использования встроенного сборщика мусора в pthreads:

 $worker = new Worker(); for ($i = 0; $i < 10; ++$i) { $worker->stack(new class extends Threaded {}); } $worker->start(); while ($worker->collect()); // blocks until all tasks have finished executing and have been collected $worker->shutdown(); 

Наконец, класс Stackable , ранее связанный с классом Threaded , был удален. Любые классы, которые расширили Stackable теперь должны быть изменены для расширения Threaded .

Удаленные Методы

Следующие методы были удалены:

  • Threaded::getTerminatedInfo — из-за того, что сериализировать исключения небезопасно Встроенных альтернатив не существует, но поскольку PHP 7 преобразовал подавляющее большинство фатальных ошибок в исключения, вместо них можно использовать обработчики универсальных исключений:

     $task = new class extends Thread { public function run() { try { $this->task(); } catch (Throwable $ex) { // handle error here var_dump($ex->getMessage()); } } private function task() { $this->data = new Threaded(); $this->data = new StdClass(); // RuntimeException thrown var_dump(1); // never reached } }; $task->start() && $task->join(); 

    (См. Ниже о новом добавлении класса Volatile и впоследствии, почему приведенный выше код ошибочен.)

  • Threaded::from — поскольку в PHP 7 есть анонимные классы, которые гораздо предпочтительнее использовать.

  • Threaded::isWaiting — потому что он просто не нужен при синхронизации. Поток не должен задаваться вопросом, ожидает ли он чего-то, и, как таковой, нет альтернатив этому методу.

  • Threaded::lock и его аналог Threaded::unlock — по тем же причинам классы Mutex и Cond были удалены. Учитывая, что синхронизация теперь синхронизирует таблицу свойств Threaded объектов, ее следует использовать вместо этого.

  • Thread::kill — из-за того, что это не безопасно для выполнения. Альтернатив нет — код не должен просто убивать поток в такой высокоуровневой среде.

  • Thread::detach — из-за того, что это не безопасно. Альтернатив нет — любой код, основанный на этом, необходимо будет переписать.

  • Worker::isWorking — потому что в этом нет необходимости. Чтобы увидеть, остались ли у работника какие-либо задачи, следует использовать метод Worker::getStacked , который будет возвращать размер оставшегося стека.

Измененные Методы

Следующие методы были изменены:

  • Worker::unstack — он больше не принимает параметр (который ранее удалял переданную задачу из стека). Это означает, что по умолчанию теперь просто удаляется только первая задача (самая старая) из стека, а не удаляются все задачи из стека.

  • Pool::collect — теперь возвращает количество задач, которые нужно собрать, и колбэк-коллектор теперь необязательный. Если обратный вызов коллектора не используется, используется метод Worker::collector по умолчанию.

Новые классы

Класс Volatile был добавлен благодаря новой семантике неизменяемости классов Threaded , где, если у них есть свойства, являющиеся объектами Threaded , они являются неизменяемыми. Класс Volatile позволяет еще раз изменить код, который ранее зависел от изменчивости таких членов.

Например, следующий фрагмент кода работал бы на pthreads v2:

 class Task extends Threaded { public function __construct() { $this->data = new Threaded(); $this->data = new StdClass(); // previously ok, but not in pthreads v3 } } new Task(); 

Но теперь в pthreads v3 переназначение $this->data вызовет RuntimeException так как это свойство Threaded из класса Threaded . Чтобы правильно переназначить свойство, класс Task должен расширять Volatile :

 class Task extends Volatile { public function __construct() { $this->data = new Threaded(); $this->data = new StdClass(); } } new Task(); 

Массивы, назначаемые свойствам Threaded объектов, теперь автоматически приводятся к Volatile объектам, а не к Threaded объектам, поэтому их поведение остается в основном неизменным.

Хотя это новое ограничение неизменности немного увеличивает сложность, оно было введено для значительного увеличения производительности, которое оно дает при доступе к Threaded свойствам Threaded объектов.

Новые Методы

Были добавлены следующие методы:

  • Worker::collect — это было введено для освобождения задач, завершившихся в стеке работника. Необязательная функция сборщика может быть передана, однако сборщик по умолчанию (из Worker::collector ) должен быть достаточным в подавляющем большинстве случаев.

    Например, следующее:

     $worker = new Worker(); var_dump(memory_get_usage()); // original memory usage for ($i = 0; $i < 500; ++$i) { $worker->stack(new class extends Threaded {}); } var_dump(memory_get_usage()); // memory usage after stacking 500 tasks $worker->start(); while ($worker->collect()); $worker->shutdown(); var_dump(memory_get_usage()); // memory usage after worker shutdown 

    Выходы:

     int(372912) int(486304) int(374528) 

    Со строкой, которая вызывает Worker::collect , использование памяти почти возвращается к норме. Без этого использование памяти не изменилось бы между 500 сложенными задачами и выключением рабочего. Хотя память в конечном итоге освободилась бы после уничтожения объекта, лучше явно освободить эту память (особенно для долго выполняющихся процессов, которым может потребоваться выполнить много задач). Поэтому всегда собирайте мусор, оставленный рабочими (а также бассейнами).

  • Worker::collector — это было введено как реализация по умолчанию, используемая методом Worker::collect . Мы можем переопределить этот метод для случаев, когда мы хотим отложить сбор потраченных предметов. Как упомянуто выше, коллектор по умолчанию будет достаточным в подавляющем большинстве случаев, поэтому переопределите этот метод, только если вы знаете, что делаете!

  • Threaded::notifyOne — это дополняет Threaded::notifyOne , позволяя отправлять сигнал только в один из ожидающих синхронизированных контекстов.

Вывод

В pthreads v3 был внесен ряд изменений, сделавших расширение более производительным и надежным. Некоторые вещи стали проще, особенно в отношении совместно используемых ресурсов, которые могут обрабатываться только через механизмы синхронизации ( Threaded::wait и Threaded::notify ). Другие вещи немного усложнились, особенно в отношении новых ограничений неизменности (в обмен на гораздо лучшую производительность). Но в целом, pthreads v3 получил хорошую очистку и выглядит все лучше.

Вы используете это? Пришлось обновляться с v2 до v3? Расскажите нам об этом — мы хотели бы написать о практическом примере обновления.