Multiprocesos con fork en PHP (y I)


¿Sabes lo que es el reto de PHP?

He creado el PHP Journey, una plataforma para que recibas de forma gratuita un email cada día con una pequeña perla de sabiduría de PHP y una pregunta que deberás responder.

Cada día pones a prueba tus conocimientos y aprendes algo nuevo.

Apúntate aquí:

https://urlantools.urlanheat.com/newsletter/php-journey/subscribe


Hoy toca hablar de un gran desconocido en el mundillo PHP: el multiprocesamiento. Es posible que ni siquiera hayas oído hablar de que en PHP se puede ejecutar más de un proceso a la vez.

Antes de que sigas: tengo entendido que en sistemas no Unix (como en Windows) no funciona nada de ésto.

¿Qué es un proceso?

Venga, empezamos desde el principio. Un proceso es un programa o script que está ejecutándose. Cada vez que ejecutas un script de PHP se crea un nuevo proceso.

¿En qué consiste esto del multiproceso?

Pues no tiene mayor misterio, consiste en que a partir de un script de PHP que está en marcha podemos crear un proceso hijo igual (con una pequeña diferencia que ya veremos) que se ejecuta en paralelo.

Lo mejor es empezar con un sencillo ejemplo:

<?php

$pid = pcntl_fork();
if ($pid == -1) {
    die('Ha fallado el fork');
}
echo "Y con esto terminamos.\n";

Ya está, esto es todo lo que necesitamos. La magia está en la función pcntl_fork(), que es la responsable de crear un proceso hijo.

Si lanzas este código el resultado será éste:

Y con esto terminamos.
Y con esto terminamos.

¿Cómo es posible? ¡Se ha mostrado el mensaje dos veces y solo hay un echo!

¡Claro! Porque a partir del proceso que estaba en ejecución se ha creado un segundo proceso (el hijo) con la función pcntl_fork() y se han ejecutado los dos en paralelo.

A esto es lo que se llama un fork.

Un dato importante; es posible que en tu sistema no se pueda hacer el fork o puede fallar por alguna razón. Para detectar esa situación hacemos la siguiente comprobación:

if ($pid == -1)

Si falla la creación del nuevo proceso deberemos actuar en consecuencia.

Identificar al padre y al hijo

Es posible que te preguntes ¿y cómo podemos saber si lo que se está ejecutando es el proceso original (padre) o el hijo? Pues para eso usamos la variable $pid que nos ha devuelto la función pcntl_fork():

<?php

$pid = pcntl_fork();
if ($pid == -1) {
    die('Ha fallado el fork');
}

echo "pid tiene el valor $pid.\n";

El resultado de este código es:

pid tiene el valor 1384185.
pid tiene el valor 0.

El proceso hijo es aquel cuyo $pid tiene valor cero. El padre es el que tiene el $pid con un valor mayor que cero.

Seguro que esto que te voy a contar ya lo sabes, pero si te lo digo me sentiré más listo. Todos los procesos se identifican a través de un número (el Process IDentifier o PID).

Cada vez que pones en marcha un proceso recibe un PID diferente. Estos valores que se almacenan en $pid son precisamente eso, los identificadores de los procesos.

Así que podemos usarlos para saber si estamos en el padre o en el hijo:

<?php

$pid = pcntl_fork();
if ($pid == -1) {
    die('Ha fallado el fork');
}

if ($pid) {
    echo "[Padre] Soy el padre.\n";
    echo "[Padre] Mi PID es: " . getmypid() . ".\n";
    echo "[Padre] El PID de mi hijo es: $pid.\n";
}
else {
    echo "[Hijo] Soy el hijo.\n";
    echo "[Hijo] Mi PID es: " . getmypid() . ".\n";
}

La función getmypid() nos dice cuál es el PID del proceso actual.

El resultado será algo así:

[Padre] Soy el padre.
[Padre] Mi PID es: 1742449.
[Padre] El PID de mi hijo es: 1742450.
[Hijo] Soy el hijo.
[Hijo] Mi PID es: 1742450.

Ojo, que aquí no tenemos ninguna garantía de que se muestren primero los mensajes del padre y luego los del hijo. Con un script tan sencillo como éste seguramente se muestre siempre en el orden que lo he puesto yo, pero podría no suceder así. Probemos añadiendo un pequeño sleep():

<?php

$pid = pcntl_fork();
if ($pid == -1) {
    die('Ha fallado el fork');
}

if ($pid) {
    echo "[Padre] Soy el padre.\n";
    sleep(1);
    echo "[Padre] Mi PID es: " . getmypid() . ".\n";
    echo "[Padre] El PID de mi hijo es: $pid.\n";
}
else {
    echo "[Hijo] Soy el hijo.\n";
    echo "[Hijo] Mi PID es: " . getmypid() . ".\n";
}

En este caso tendremos un resultado parecido a éste:

[Padre] Soy el padre.
[Hijo] Soy el hijo.
[Hijo] Mi PID es: 1782256.
[Padre] Mi PID es: 1782255.
[Padre] El PID de mi hijo es: 1782256.

Las variables se copian pero no se comparten

Cuando hacemos un fork el hijo «hereda» todas las variables que tiene el padre con los valores que tienen en ese momento. Podemos verlo con un ejemplo:

<?php

$nombre = 'Dart Vader';

$pid = pcntl_fork();
if ($pid == -1) {
    die('Ha fallado el fork');
}

if ($pid) {
    echo "[Padre] Nombre: $nombre.\n";
}
else {
    echo "[Hijo] Nombre: $nombre.\n";
}

La salida del ejemplo sería:

[Hijo] Nombre: Dart Vader.
[Padre] Nombre: Dart Vader.

Pero ¿qué pasa si el hijo modifica esa variable? Vamos a modificar el ejemplo para que el hijo modifique la variable. Añadimos un sleep() para asegurarnos de que el padre muestra el valor de la variable después de que el hijo la haya modificado y veamos qué pasa:

<?php

$nombre = 'Dart Vader';

$pid = pcntl_fork();
if ($pid == -1) {
    die('Ha fallado el fork');
}

if ($pid) {
    sleep(1);
    echo "[Padre] Nombre: $nombre.\n";
}
else {
    $nombre = 'Luke';
    echo "[Hijo] Nombre: $nombre.\n";
}

El resultado es:

[Hijo] Nombre: Luke.
[Padre] Nombre: Dart Vader.

Es decir, que la variable $nombre del padre no se ve afectada aunque la cambie el hijo. Ya se que soy pesado pero, insisto, el hijo copia las variables del padre en el momento del fork pero son variables diferentes e independientes.

Esperar a que termine el hijo

Algo muy importante que no he comentado aún es que en el proceso padre no debemos olvidar llamar a la función:

pcntl_wait($status);

Esta función hace que el padre espere a su hijo para evitar que éste se quede «zombie». Esto es muy importante y lo comentaré el mayor detalle en otro artículo.

El código completo quedaría así:

<?php

$pid = pcntl_fork();
if ($pid == -1) {
    die('Ha fallado el fork');
}

if ($pid) {
    echo "[Padre] Soy el padre.\n";
    pcntl_wait($status);
}
else {
    echo "[Hijo] Soy el hijo.\n";
}

Quizá te llame la atención que la variable $status no está definida y no se produce ningún error ni aviso. Eso es porque pcntl_wait() recibe esa variable por referencia.

Código común

Puede que te preguntes ¿qué pasa si meto código después del if? Bueno, no pasa nada, se ejecuta como el resto, pero ten cuidado y no te líes. Por ejemplo, no pienses que al salir del if ha terminado el multiprocesamiento o algo similar.

Veamos este código:

$nombre = 'Dart Vader';

$pid = pcntl_fork();
if ($pid == -1) {
die('Ha fallado el fork');
}

if ($pid) {
echo "[Padre] Nombre: $nombre.\n";
}
else {
$nombre = 'Luke';
echo "[Hijo] Nombre: $nombre.\n";
}

echo "Me llamo: $nombre.\n";

Es bastante común pensar que la salida de este código va a ser:

[Padre] Nombre: Dart Vader.
Me llamo: Dart Vader.
[Hijo] Nombre: Luke.

Porque, por alguna razón, tendemos a pensar que al salir del bloque if-else ha terminado el multiprocesamiento. Pero la salida real es:

[Padre] Nombre: Dart Vader.
Me llamo: Dart Vader.
[Hijo] Nombre: Luke.
Me llamo: Luke.

A tener en cuenta si haces un fork

Cuando creas un proceso con pcntl_fork() se hace una copia de todo el proceso con sus variables. Como resultado, si no tenemos cuidado, podemos hacer un uso excesivo de la memoria. Ojo con ésto.

También puedes tener problemas si los hijos y el padre comparten la conexión de la base de datos. En un próximo artículo hablaré del tema.

Y en la próxima entrega…

  • Lanzar varios procesos a la vez sin dejar zombies.
  • Utilidad del multiprocesamiento en casos reales.
  • La conexión de la base de datos.

2 comentarios en «Multiprocesos con fork en PHP (y I)»

Responder a Carlos Cancelar la respuesta