Обновлено: Я пометил этот C в надежде получить больше ответов. Меня интересует больше теория, чем реализация на конкретном языке. Поэтому, если вы программист на C, рассматривайте следующий PHP как псевдокод и не стесняйтесь отвечать, написав на C.
Я пытаюсь ускорить сценарий PHP CLI, заставляя его выполнять свои задачи параллельно, а не последовательно. Задачи полностью независимы друг от друга, поэтому не имеет значения, в каком порядке они начинаются / заканчиваются.
Вот исходный сценарий (обратите внимание, что все эти примеры для ясности сокращены):
<?php
$items = range(0, 100);
function do_stuff_with($item) { echo "$item\n"; }
foreach ($items as $item) {
do_stuff_with($item);
}
Мне удалось заставить его работать на $items
параллельно с pcntl_fork()
, как показано ниже:
<?php
ini_set('max_execution_time', 0);
ini_set('max_input_time', 0);
set_time_limit(0);
$items = range(0, 100);
function do_stuff_with($item) { echo "$item\n"; }
$pids = array();
foreach ($items as $item) {
$pid = pcntl_fork();
if ($pid == -1) {
die("couldn't fork()");
} elseif ($pid > 0) {
// parent
$pids[] = $pid;
} else {
// child
do_stuff_with($item);
exit(0);
}
}
foreach ($pids as $pid) {
pcntl_waitpid($pid, $status);
}
Теперь я хочу расширить это так, чтобы одновременно было, скажем, максимум 10 детей. Как лучше всего с этим справиться? Я пробовал несколько вещей, но без особого успеха.
Ха. Что ж, я рад, что моя проблема в том, что она есть, а не в том, что вы думали :-)
мужчина 2 setrlimit
В любом случае это будет для каждого пользователя, который май будет тем, что вы хотите.
Как насчет того, чтобы я хотел сделать это на уровне приложения?
Это системный вызов, который вы можете вызвать из своего приложения. Он очень часто используется для ограничения ядра, дампов ядра, файловых дескрипторов и т. д. Вы также можете использовать его для ограничения загрузки ЦП (я так часто делаю) и количества процессов.
Лучшее, что я могу придумать, - это добавить все задачи в очередь, запустить максимальное количество потоков, которое вы хотите, а затем заставить каждый поток запрашивать задачу из очереди, выполнять задачу и запрашивать следующую. Не забывайте, что потоки завершаются, когда больше нет задач.
Разветвление - дорогостоящая операция. Судя по всему, вам действительно нужно multiзаправка, а не multiобработка. Разница в том, что потоки имеют гораздо меньший вес, чем процессы, поскольку потоки совместно используют виртуальное адресное пространство, но процессы имеют отдельные виртуальные адресные пространства.
Я не разработчик PHP, но быстрый поиск в Google показывает, что PHP изначально не поддерживает многопоточность, но для этой работы есть библиотеки.
В любом случае, как только вы выясните, как создавать потоки, вы должны выяснить, сколько потоков нужно создавать. Для этого вам нужно знать, в чем узкое место вашего приложения. Узким местом является процессор, память или ввод-вывод? Вы указали в своих комментариях, что вы привязаны к сети, а сеть - это тип ввода-вывода.
Если вы были привязаны к ЦП, вы получите столько же параллелизма, сколько у вас ядер ЦП; больше потоков, и вы просто тратите время на переключение контекста. Предполагая, что вы можете выяснить, сколько всего потоков нужно создать, вам следует разделить свою работу на это количество единиц и заставить каждый поток обрабатывать одну единицу независимо.
Если бы у вас была ограниченная память, многопоточность не помогла бы.
Поскольку вы привязаны к вводу-выводу, выяснить, сколько потоков нужно создать, немного сложнее. Если для обработки всех рабочих элементов требуется примерно одинаковое время с очень низкой дисперсией, вы можете оценить, сколько потоков нужно запустить, измерив, сколько времени занимает один рабочий элемент. Однако, поскольку сетевые пакеты, как правило, имеют сильно различающиеся задержки, это маловероятно.
Один из вариантов - использовать пулы потоков - вы создаете целую группу потоков, а затем для каждого обрабатываемого элемента вы видите, есть ли в пуле свободный поток. Если есть, значит, этот поток выполняет работу, и вы переходите к следующему элементу. В противном случае вы ждете, пока поток станет доступным. Выбор размера пула потоков важен - слишком большой, и вы тратите время на ненужные переключения контекста. Слишком мало, и вы слишком часто ждете потоков.
Еще один вариант - отказаться от многопоточности / многопроцессорности и вместо этого просто выполнять асинхронный ввод-вывод. Поскольку вы упомянули, что работаете над одноядерным процессором, это, вероятно, будет самым быстрым вариантом. Вы можете использовать такие функции, как socket_select()
, чтобы проверить, есть ли у сокета данные. Если это так, вы можете прочитать данные, в противном случае вы перейдете на другой сокет. Это требует гораздо большего учета, но вы избегаете ожидания поступления данных в один сокет, когда данные доступны в другом сокете.
Если вы хотите отказаться от потоков и асинхронного ввода-вывода и придерживаться многопроцессорной обработки, это все равно может иметь смысл, если обработка отдельных элементов обходится достаточно дорого. Затем вы можете сделать такое разделение работы:
$my_process_index = 0;
$pids = array();
// Fork off $max_procs processes
for($i = 0; $i < $max_procs - 1; $i++)
{
$pid = pcntl_fork();
if ($pid == -1)
{
die("couldn't fork()");
}
elseif ($pid > 0)
{
// parent
$my_process_index++;
$pids[] = $pid
}
else
{
// child
break;
}
}
// $my_process_index is now an integer in the range [0, $max_procs), unique among all the processes
// Each process will now process 1/$max_procs of the items
for($i = $my_process_index; $i < length($items); $i += $max_procs)
{
do_stuff_with($items[$i]);
}
if ($my_process_index != 0)
{
exit(0);
}
Спасибо за ответ, попробую. Я читал кое-где, что в Unix fork () на самом деле не так уж и дорого, и как в любом случае реализована многопоточность? Это устаревшая / неверная информация?
«Вы получите столько же параллелизма, сколько у вас ядер ЦП; больше потоков - и вы просто тратите время на переключение контекста». Что ж, в моем случае у меня только 1 ядро, но я могу значительно ускорить процесс, выполняя сразу 20 задач. Задачи зависят от (медленных) сетевых операций.
Нет, многопоточность реализована не так. Трудно объяснить с помощью 300 символов - прочтите о том, как реализована fork (), о виртуальной памяти, виртуальных адресных пространствах, копировании при записи и многих других связанных темах.
fork () не дорого, exec () стоит. Ядро не копирует сегмент кода - его разделяют и родительский, и дочерний. Сегмент данных будет дублироваться только тогда, когда родитель или потомок что-то изменят в сегменте данных (копирование при записи). NB! Я предполагаю, что мы говорим здесь о Unix.
Системного вызова для получения списка дочерних идентификаторов PID нет, но ps
может сделать это за вас.
Переключатель --ppid
перечислит всех дочерних процессов для вашего процесса, поэтому вам просто нужно подсчитать количество строк, выводимых ps
.
В качестве альтернативы вы можете поддерживать свой собственный счетчик, который вы будете увеличивать для fork()
и уменьшать для сигнала SIGCHLD
, предполагая, что ppid
остается неизменным для обработки fork'ed.
Я решил пойти с SIGCHLD и wait (), чтобы отслеживать завершенные дочерние элементы. Спасибо за ответ.
@skix - Вы не рассказываете, как это выглядит?
Ха! Мне нравится название - мне пришлось прийти и прочитать вопрос только потому, что название напомнило бездушных диктаторов, мстящих политическим злодеям, пытая своих детей вилками ...