Paralelizando procesos en PHP

Publicado: 30/06/2011 en desarrollo, fork, paralelizacion, pcntl, php, Programación, thread
Etiquetas:, , , , , ,

You can read the English version of this post in http://phpsblog.agustinvillalba.com/parallelize-processes-php/

Hoy vamos a explicar cómo poder lanzar múltiples hilos en nuestros scripts PHP, pudiendo así paralelizar aquellos procesos que tengan una gran carga de procesador o que simplemente puedan lanzarse en paralelo dado que no tienen dependencias entre ellos y todos resuelven de forma parcial un problema común.

¿Por qué paralelizar procesos?

Cuando se programa un algoritmo, según el lenguaje en el que lo hagamos, podemos resolver las partes del algoritmo de forma secuencial o paralela. Si tenemos una parte del algoritmo que tiene una gran carga y requiere mucho tiempo para su resolución, podríamos ir resolviendo las demás partes mientras se resuelve la parte más lenta. De forma que el tiempo total de ejecución de nuestro algoritmo pasa de ser la suma de las partes a solamente la parte más lenta. Es cierto que la paralelización realmente se aprovecha cuando disponemos de recursos redundados, por ejemplo procesadores, pero en la gran mayoría de los servidores actuales disponemos de varios núcleos, por lo que tiene bastante utilidad aprender a paralelizar nuestros procesos.

La base teórica de la paralelización nos dice que cuando proceso padre lanza un proceso hijo, se crea en memoria un proceso idéntico al padre, con un identificador de proceso (pid) propio (diferente al del padre) y ejecutándose a partir de la instrucción siguiente a la que creó al propio proceso hijo.

Lanzando multiples hilos en PHP

Antes que nada hemos de tener en cuenta que para poder paralelizar procesos en PHP necesitamos tener instalada la extensión de control de procesos (Process Control Extension [http://www.php.net/manual/en/refs.fileprocess.process.php] en inglés). Dentro de esta extensión, los módulos que nos permitirán paralelizar nuestros procesos son el propio de Control de Procesos (PHP PCNTL [http://www.php.net/manual/en/book.pcntl.php] en inglés) y el de memoria compartida entre procesos (PHP Shared Memory [http://www.php.net/manual/en/book.shmop.php] en inglés). Para poder explicar la paralelización de procesos vamos a poner un ejemplo (bastante simple) en el que un proceso (padre) lanzará 10 hilos en paralelo (hijos) los cuales generarán un número aleatorio, colocarán en una zona de memoria compartida con su padre, éste recogerá cada número generado por sus hijos y devolverá la suma de todos ellos.

Veamos el código:

<?php
function multiple_forks()
{
    $array_pids = array();
    $sumatorio = 0;
    //Almacenamos el process id del proceso padre
    $parent_pid = getmypid();
    for($i=0;$i<10;$i++)
    {
        if(getmypid() == $parent_pid)
        {//Estamos en el proceso padre, asi que lanzamos el proceso hijo y guardamos si pid
            $array_pids[] = pcntl_fork(); //pcntl_fork nos permite lanzar un proceso hijo en paralelo
        }
    }

    //Una vez hemos lanzado los 10 hijos pasamos a generar los números aleatorios (en los hijos)
    //o ir sumandolos si estamos en el padre
    if(getmypid() == $parent_pid)
    {//Estamos en el proceso padre
        while(count($array_pids) > 0)
        {//Mientras queden hijos en ejecución, quedamos a la espera de que terminen
            $pid = pcntl_waitpid(-1,$status);
            //Abrimos la memoria compartida con nuestro hijo $pid
            $shared_id = shmop_open($pid,"a",0,0);
            $share_data = shmop_read($shared_id,0,shmop_size($shared_id));
            $sumatorio += $share_data;
            //Marcamos el bloque para que sea eliminado y lo cerramos
            shmop_delete($shared_id);
            shmop_close($shared_id);
            //Eliminamos el proceso de la cola de hijos en ejecucion
            foreach($array_pids as $key => $hijo)
            {
                if($pid == $hijo) unset($array_pids[$key]);
            }
        }
    }
    else
    {//Estamos en el hijo
        $num = rand(0,100);
        $shared_id = shmop_open(getmypid(),"c",0644,strlen($num));
        if(!$shared_id)
        {//No se pudo crear la memoria compartida
            echo "Error al crear la memoria compartida en el hijo ".getmypid()."\n";
        }
        else
        {
            if(strlen($num) != shmop_write($shared_id,$num,0))
            {
                echo "Error al intentar escribir el numero $num en el hijo ".getmypid()."\n";
            }
            else
            {
                shmop_close($shared_id);
            }
         }
         //Salimos indicando al padre que todo ha ido bien
         exit(0);
    }
    return $sumatorio;
}
?>

Muy probablemente el código se pueda optimizar aun más, pero no es el objetivo de este artículo. Veamos las funciones de control de procesos y memoria compartida que hemos utilizado en el código:

  • pcntl_fork. Nos permite lanzar hijos de un proceso padre, devolviendo el pid del hijo lanzado. (pcntl_fork en PHP)
  • pcntl_waitpid. Nos permite poner al padre en espera de que un hijo suyo termine su ejecución. El parámetro -1 indica que queda a la espera de que algún hijo termine, el primero que lo haga. (pcntl_waitpid en PHP).
  • shmop_open. Nos permite crear o abrir un bloque de memoria. El primer parámetro es a modo de identificador, nada mejor que usar el pid del hijo como identificador, así el padre podrá conocer el identificador con el que se creó dicho bloque de memoria y acceder a los datos compartidos. (shmop_open en PHP)
  • shmop_read. Nos permite leer un bloque de memoria compartida. (shmop_read en PHP)
  • shmop_delete. Marca el bloque de memoria para ser liberado. El bloque será liberado automáticamente por el sistema cuando todos los procesos concurrentes asociados con el bloque se desvinculen del bloque. (shmop_delete en PHP)
  • shmop_close. Cierra un bloque de memoria, indicandole al sistema que el proceso se desvincula del bloque. (shmop_close en PHP)
  • shmop_size. Nos permite conocer el tamaño que ocupa un bloque de memoria compartida. (shmop_size en PHP)
  • shmop_write. Nos permite escribir datos en un bloque de memoria compartida. (shmop_write en PHP)

Con esto tenemos la base necesaria para poder crear procesos paralelos en nuestros controladores y lanzar así tantos hilos como procesadores dispongamos en nuestro servidor.

You can read the English version of this post in http://phpsblog.agustinvillalba.com/parallelize-processes-php/

comentarios
  1. David Alejandro Garcia dice:

    Buena tarde, saludos para todos los miembros de este blog, mi nombre es David, recien estoy aprendiendo a programar con php y estoy haciendo un proyecto que me permite cargar un archivo plano de mas o menos 450MB a un servidor, luego proceso ese archivo, es decir, lo corto en archivos mas pequeños para poder ingresarlos a una DB, hasta ahi no hay ningun problema y la aplicacion de forma local se ejecuta sin ningun problema, el inconveniente esta cuando subo la aplicacion a un hosting de GoDaddy, cuando realizo la subido del archivo se ejecuta sin ningun problema, al momento de fraccionarlo lo hace bien ya que lo he puesto a que fraccione en tramos muy cortos (2000 registros por archivo) y lo hace bien, pero al momento de leer esos archivos e ingresarlos a la DB, se me cae la aplicacion,ya que excede el maximo tiempo de ejecucion, investigando por ahi, vi que hay una forma de lanzar multiples procesos sin deterner la ejecucion de la aplicacion y es con hilos, pero la verdad no entiendo mucho de esa parte, tengo varias preguntas y la primera y principal es: puedo trabajar hilos en php con Windows?, y me podrian orientar en el manejo de hilos con php para lo que necesito?

    De ante mano agradezco enormemente cualquier colaboracion que pueda recibir de los integrantes de este blog. Dios los guarde y los bendiga hoy y siempre.

  2. Rod Rodríguez dice:

    Buenas tarde amigo, gracias por compartir tus tan apreciados conocimientos, yo necesito algo así de parecido, ya que estoy desarrollando una aplicación en PHP de recargas telefónicas y no logro como implementar tu ejemplo ya que a la aplicación se le pueden conectar o hacer recargas al mismo segundo N cantidad de personas (osea no se sabe cuantos) y la conexión es mediante un socket a una operadora y en dicha conexión el id se me repite y me da error ya que el id que le paso mediante el socket debe de ser único y consecutivo. De ante mano muchas gracias por tu ayuda.

    • Si tu problema se trata de que N usuarios intentan acceder al mismo recurso al mismo tiempo, no necesitarías paralelización, sino que tu problema se resuelve con programación concurrente (N clientes compitiendo por un mismo recurso al mismo tiempo). Ese ya es otro tema distinto que requiere un tratamiento totalmente distinto con técnicas diferentes (semáforos, etc). Seguro que en la Red encuentras mucha información al respecto, incluso implementada en PHP.

      Un saludo!

  3. […] tenemos un ejemplo de creación de hilos en PHP, la implementación se antoja un tanto compleja, y además, probablemente no estaríamos […]

  4. jorge dice:

    tengo una pregunta
    puedo acceder a variables globales (y otras) desde los procesos hijos?
    es decir, supongamos que defino una variable u objeto (como el de una determinada coneccion a bases de datos, por ejemplo) puedo acceder de forma directa al objeto desde el proceso hijo?
    espero su respuesta, saludos

  5. Patricio Betancourt dice:

    Muy buena info, se agradece

  6. Agustín, como siempre … un artículo excepcional muy bien explicado. Claro y conciso como nos tienes acostumbrado.

    ¡Enhorabuena y ánimos para seguir compartiendo tu conocimiento!

    Saludos 😉

Deja un comentario