PHP7: paralelizando procesos, aprovechando el procesador al 100%

php7-logoIgual que en otros lenguajes de programación, aquí en PHP también tenemos disponibles funciones del estándar POSIX para gestionar procesos, obtener información de ellos, hacer colas FIFO, enviar señales, hacer procesos hijos, esperar a que terminen unos para continuar otros, matarlos.. etcétera. Todo esto te dará lo que necesitas para pasar de una programación lineal, paso a paso, a una programación concurrente, paralelizando los procesos.

Esto se usa desde línea de comandos. No recomiendan en absoluto que se active y se use esto sobre servidor web, ya que los servidores web tienen sus propias estrategias de paralelización. Pero si como yo, has estado trabajando desde línea de comandos con este tema. Y has llegado a tareas que tardan mucho tiempo y se lanzan desde línea de comandos. Con esto podrás lanzar en paralelo todas las tareas que necesites, así aprovecharás todos los núcleos del procesador, y terminará antes el proceso completo.

Necesitarás tener activado el módulo PCNTL en PHP. Para saber si ya lo tienes activado ejecuta php -i, si no edita el php.ini para activarlo.. En PHP7 ya me venía activado. Con esto te evitas también la engorrosa instalación de Pthreads recompilando PHP con ZTS, además de la instalación posterior de Pthreads. Simplemente usando el mencionado PCNTL..

Porqué paralelizar procesos

Si tenemos un procesador con varios núcleos, cada script que lancemos se ejecutará desde un sólo núcleo. Ahora bien, si tenemos una tarea que ejecuta muchas subtareas una detrás de otra. Hasta que llega al final. Si podemos dividir esta tarea en subtareas independientes entre sí, o podemos hacerlo de manera que pudiésemos ejecutar estas subtareas a la vez y en paralelo, entonces podremos reducir el tiempo total.

Es decir, si podemos dividir la tarea en subtareas y que se ejecute cada una en un núcleo del procesador, el total de tiempo se reducirá aprovechando el 100% del procesador. Es lo mismo que ya hablé hace tiempo en: multiprocesamiento en Java: ¡dale caña a tu procesador!. Ya me estoy repitiendo mucho..

Vamos con un ejemplo que se verá muy claro

Una tarea que se compone de 4 subtareas. Ejecutadas una detrás de otra:

PID 16987: executing process1..
PID 16987: executing process2..
PID 16987: executing process3..
PID 16987: executing process4..
PID 16987: exiting..
Time elapsed: 6.1045458316803 seconds.

Cada subtarea es lo que he nombrado como process1, process2, process3 y process4. A continuación estas mismas subtareas paralelizadas, el proceso inicial se subdivide haciendo 4 procesos hijos. Cada uno ejecuta una de las subtareas mientras que el proceso padre se espera a que todas terminen para medir el tiempo:

PID 16920: executing process..
PID 16922: executing process..
PID 16919: my children are: 
Array
(
 [0] => 16920
 [1] => 16921
 [2] => 16922
 [3] => 16923
)
I want childs to terminate their work, so I'll stay waiting..
PID 16921: executing process..
PID 16923: executing process..
PID 16920 finished with status 0
PID 16921 finished with status 0
PID 16922 finished with status 0
PID 16923 finished with status 0
PID 16919: exiting..
Time elapsed: 2.8952360153198 seconds.

Paralelizando las 4 subtareas tenemos que tarda todo menos de la mitad de tiempo.

Porqué

Aquí sin hacer paralelización de procesos:

PHP tasks without forking

A continuación haciendo paralelización de procesos:

PHP tasks with forking

Si nos fijamos a la derecha de esta imagen, el segundo script paralelizando las tareas en varios procesos, se ejecutan en los 4 cuatro núcleos del procesador. Esto explica que el segundo script haya tardado menos de la mitad. Hemos sacrificado algo de tiempo haciendo los procesos hijos, pero hemos ganado tiempo total porque hemos aprovechado el procesador al 100%. Si tuviésemos un procesador de 8 núcleos, pues más de lo mismo, lo interesante sería paralelizar a 8 procesos.

El código

El primer ejemplo, sin paralelizar procesos:

<?php

$timeStart = microtime(true);

echo 'PID '.getmypid().': executing process1..'.PHP_EOL;
heavyProcess1();
echo 'PID '.getmypid().': executing process2..'.PHP_EOL;
heavyProcess2();
echo 'PID '.getmypid().': executing process3..'.PHP_EOL;
heavyProcess3();
echo 'PID '.getmypid().': executing process4..'.PHP_EOL;
heavyProcess4();
echo 'PID '.getmypid().': exiting..'.PHP_EOL;

echo 'Time elapsed: '.(microtime(true) - $timeStart).' seconds.'.PHP_EOL;
exit(0);

function heavyProcess1()
{
    for ($i = 0; $i < 10000; ++$i) {
        for ($j = 0; $j < 10000; ++$j) {
            $a = $i * $j;
        }
    }
}
function heavyProcess2()
{
    heavyProcess1();
}
function heavyProcess3()
{
    heavyProcess1();
}
function heavyProcess4()
{
    heavyProcess1();
}

El segundo ejemplo, haciendo procesos hijos, paralelizando:

<?php

$timeStart = microtime(true);

$myPid = getmypid(); // guardo PID inicial para saber quién es el padre

for ($i = 0; $i < 4; ++$i) { 
    if (getmypid() == $myPid) { // si es el padre hacemos fork creando hijos
        $pid = $pidsArray[$i] = pcntl_fork(); // guardo PIDs para luego
        if ($pid == 0) { // si es un hijo se pone a trabajar en la tarea pesada que le corresponda
            echo 'PID '.getmypid().': executing process..'.PHP_EOL;
            switch ($i) {
                case 0: // subprocess 1
                    heavyProcess1();
                    break;
                case 1: // subprocess 2
                    heavyProcess2();
                    break;
                case 2: // subprocess 3
                    heavyProcess3();
                    break;
                case 3: // subprocess 4
                    heavyProcess4();
                    break;
            }
        }
    }
}

if (getmypid() == $myPid) { // sólo si el PID del proceso es el inicial, es decir, si es el padre..
    echo 'PID '.getmypid().': my children are: '.PHP_EOL;
    print_r($pidsArray);
    echo 'I want childs to terminate their work, so I\'ll stay waiting..'.PHP_EOL;
    for ($i = 0; $i < 4; ++$i) {
        pcntl_waitpid($pidsArray[$i], $returnedStatus); // queda esperando a que termine cada proceso hijo..
        echo 'PID '.$pidsArray[$i].' finished with status '.$returnedStatus.PHP_EOL;
    }
    echo 'PID '.getmypid().': exiting..'.PHP_EOL;
    echo 'Time elapsed: '.(microtime(true) - $timeStart).' seconds.'.PHP_EOL;
}
exit(0);

function heavyProcess1()
{
    for ($i = 0; $i < 10000; ++$i) {
        for ($j = 0; $j < 10000; ++$j) {
            $a = $i * $j;
        }
    }
}
function heavyProcess2()
{
    heavyProcess1();
}
function heavyProcess3()
{
    heavyProcess1();
}
function heavyProcess4()
{
    heavyProcess1();
}

Terminando

Espero que el código hable por sí mismo. El kit de la cuestión está en la función pcntl_fork(). Esta función duplica el proceso actual, devolviendo 0 al proceso hijo, y el PID del hijo al proceso padre. Continuando la ejecución..

La función fork lo que hace es dividir el proceso como si de una célula se tratase. Crea dos procesos idénticos con cada fork que hace. Así, repitiendo 4 veces el fork, hacemos 4 hijos. Hacemos los hijos sólo desde el proceso padre inicial. Además de que, sólo desde los procesos hijos, lanzamos las tareas pesadas. Esto puede ser un poco galimatías, pero dejo marcadas las 3 líneas de código en negrita que controlan esto. Lo mejor es copiarlo y pegarlo para que lo adaptes a lo que necesites. Seguro que si no me he explicado bien, jugueteando un poco con el código que en seguida lo ves claro 😉

Para más información sobre estas funciones, y muchas otras, me remito a la documentación oficial:

http://php.net/manual/es/ref.pcntl.php

http://php.net/manual/es/ref.posix.php

Compartir..

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *