Я уверен, что то, что я пытаюсь сделать, очень просто, но я никогда раньше не работал с многопоточностью, поэтому не уверен, с чего начать.
Я использую PCNTL для создания многопоточного приложения PHP. Я хочу, чтобы одновременно работали 3 функции, и я хочу, чтобы их возвращаемые значения были объединены в один массив. Поэтому, по логике вещей, мне нужна либо некоторая переменная, совместно используемая всеми дочерними элементами, к которой они добавляют свои результаты, либо три переменные, совместно используемые только одним дочерним элементом и родителем, - тогда родитель может объединить результаты позже.
Проблема в том, что я понятия не имею, как это сделать. Первое, что приходит на ум, это использование разделяемой памяти , но я чувствую, что должен быть более простой метод.
Кроме того, если он имеет какой-либо эффект, функция, которая разветвляет процесс, является методом открытого класса. Мой код выглядит примерно так:
<?php
class multithreaded_search {
/* ... */
/* Constructors and such */
/* ... */
public function search( $string = '' ) {
$search_types = array( 'tag', 'substring', 'levenshtein' );
$pids = array();
foreach( $search_types as $type ) {
$pid = pcntl_fork();
$pids[$pid] = $type;
if( $pid == 0 ) { // child process
/* confusion */
$results = call_user_func( 'multithreaded_search::'.$type.'_search', $string );
/* What do we do with $results ? */
}
}
for( $i = 0; $i < count( $pids ); $i++ ) {
$pid = pcntl_wait();
/* $pids[$pid] tells me the type of search that just finished */
/* If we need to merge results in the parent, we can do it here */
}
/* Now all children have exited, so the search is complete */
return $results;
}
private function tag_search( $string ) {
/* perform one type of search */
return $results;
}
private function substring_search( $string ) {
/* perform one type of search */
return $results;
}
private function levenshtein_search( $string ) {
/* perform one type of search */
return $results;
}
}
?>
Так что мне нужно будет использовать shmop_open
, прежде чем я вызову pcntl_fork
, чтобы создать общую память и сохранить там результаты, или у детей общие переменные класса? Или они имеют только глобальные переменные? Я уверен, что ответ прост ... Я просто не знаю.
Ответы (для тех, кто находит это)
У меня есть еще несколько лет опыта, поэтому я постараюсь поделиться некоторыми знаниями.
Во-первых, есть два важных различия, которые нужно понимать, когда речь идет о реализации многопроцессорной обработки в ваших приложениях:
- Резьбы против процессов против разветвленных процессов
- Общая память против передача сообщений
Потоки, процессы, разветвленные процессы
- Потоки : У потоков очень низкие накладные расходы, поскольку они выполняются в том же пространстве процессов, что и родительский, и совместно используют адрес памяти родителя. Это означает меньшее количество вызовов ОС для создания или уничтожения потока. Потоки являются «дешевой» альтернативой, если вы планируете их часто создавать и уничтожать. PHP не имеет встроенной поддержки потоков. Однако, начиная с PHP 7.2, существуют расширения PHP (написанные на C), которые обеспечивают многопоточную функциональность. Например: pthreads
- Процессы : Процессы имеют гораздо большие издержки, потому что операционная система должна выделять для них память, а в случае интерпретируемых языков, таких как PHP, часто существует целая среда выполнения, которая должна быть загруженным и обработанным до выполнения вашего собственного кода. PHP имеет встроенную поддержку процессов порождения через exec (синхронно) или proc_open (асинхронно)
- Разветвленные процессы : раздвоенный процесс разделяет разницу между этими двумя подходами. Отдельный процесс выполняется в области памяти текущих процессов. Существует также встроенная поддержка для этого через pctnl
Выбор правильного инструмента для работы часто сводится к задаче: «Как часто вы будете раскручивать дополнительные потоки / процессы»? Если это не так часто (возможно, вы запускаете пакетное задание каждый час, и задание может быть распараллелено), тогда процессы могут быть более простым решением. Если каждый запрос, поступающий на ваш сервер, требует какой-либо формы параллельных вычислений и вы получаете 100 запросов в секунду, то потоки, скорее всего, помогут вам.
Общая память, передача сообщений
- Общая память : это когда нескольким потокам или процессам разрешено выполнять запись в один и тот же раздел ОЗУ. Преимущество состоит в том, что он очень быстрый и простой в понимании - он похож на общую доску в офисе. Любой может читать или писать на него. Однако он имеет несколько недостатков, когда речь идет об управлении параллелизмом. Представьте, что если два процесса пишут в одно и то же место в памяти в одно и то же время, тогда третий процесс пытается прочитать результат. Какой результат это увидит? PHP имеет встроенную поддержку разделяемой памяти через shmop , но для ее правильного использования требуются блокировки, семафоры, мониторы или другие сложные процессы системного проектирования
- Передача сообщений : Это «горячая новая вещь» ™, которая существует с 70-х годов. Идея состоит в том, что вместо записи в общую память вы пишете в свое собственное пространство памяти, а затем говорите другим потокам / процессам: «Привет, у меня есть для вас сообщение». Язык программирования Go имеет известный девиз, связанный с этим: «Не общайтесь, разделяя память, делитесь памятью, общаясь». Существует множество способов передачи сообщений, в том числе: запись в файл, запись в сокет, запись в стандартный вывод, запись в общую память и т. Д.
Базовое решение для сокетов
Сначала я попытаюсь воссоздать свое решение с 2012 года. @MarcB указал мне на сокеты UNIX . На этой странице явно упоминается fsockopen , который открывает сокет как указатель файла. Он также включает в раздел «См. Также» ссылку на socket_connect , которая дает вам немного более низкий уровень контроля над сокетами.
В то время я, вероятно, проводил много времени, исследуя эти socket_*
функции, пока не получил что-то работающее. Теперь я сделал быстрый поиск в Google для socket_create_pair
и нашел эту полезную ссылку, чтобы вы начали
Я переписал приведенный выше код, записав результаты в сокеты UNIX и считав результаты в родительский поток:
<?php
/*
* I retained the same public API as my original StackOverflow question,
* but instead of performing actual searches I simply return static data
*/
class multithreaded_search {
private $a, $b, $c;
public function __construct($a, $b, $c) {
$this->a = $a;
$this->b = $b;
$this->c = $c;
}
public function search( $string = '' ) {
$search_types = array( 'tag', 'substring', 'levenshtein' );
$pids = array();
$threads = array();
$sockets = array();
foreach( $search_types as $type ) {
/* Create a socket to write to later */
$sockets[$type] = array();
socket_create_pair(AF_UNIX, SOCK_STREAM, 0, $sockets[$type]);
$pid = pcntl_fork();
$pids[] = $pid;
$threads[$pid] = $type;
if( $pid == 0 ) { // child process
/* no more confusion */
$results = call_user_func( 'multithreaded_search::'.$type.'_search', $string );
/* What do we do with $results ? Write them to a socket! */
$data = serialize($results);
socket_write($sockets[$type][0], str_pad($data, 1024), 1024);
socket_close($sockets[$type][0]);
exit();
}
}
$results = [];
for( $i = 0; $i < count( $pids ); $i++ ) {
$pid = $pids[$i];
$type = $threads[$pid];
pcntl_waitpid($pid, $status);
/* $threads[$pid] tells me the type of search that just finished */
/* If we need to merge results in the parent, we can do it here */
$one_result = unserialize(trim(socket_read($sockets[$type][1], 1024)));
$results[] = $one_result;
socket_close($sockets[$type][1]);
}
/* Now all children have exited, so the search is complete */
return $results;
}
private function tag_search() {
return $this->a;
}
private function substring_search() {
return $this->b;
}
private function levenshtein_search() {
return $this->c;
}
}
$instance = new multithreaded_search(3, 5, 7);
var_dump($instance->search());
Примечания
В этом решении используются разветвленные процессы и передача сообщений через локальный (в памяти) сокет. В зависимости от вашего варианта использования и настройки, это может быть не лучшим решением. Например:
- Если вы хотите разделить обработку между несколькими отдельными серверами и передать результаты обратно на центральный сервер, то
create_socket_pair
не будет работать. В этом случае вам нужно будет создать сокет, связать сокет с адресом и портом, затем вызвать socket_listen
, чтобы дождаться результатов от дочерних серверов. Кроме того, pcntl_fork
не будет работать в многосерверной среде, поскольку пространство процесса не может быть разделено между различными машинами
- Если вы пишете приложение для командной строки и предпочитаете использовать потоки, то вы можете использовать pthreads или стороннюю библиотеку, которая абстрагирует pthreads
- Если вам не нравится копаться в сорняках и просто хотите простую многопроцессорность, не беспокоясь о деталях реализации, загляните в библиотеку, подобную Amp / Parallel