Programación multiproceso en PHP utilizando Pthreads Translation. Computación multiproceso en PHP: pthreads Path of the Jedi - usando la extensión PCNTL

Recientemente probé pthreads y me sorprendió gratamente: es una extensión que agrega la capacidad de trabajar con múltiples subprocesos reales en PHP. Sin emulación, sin magia, sin falsificaciones: todo es real.



Estoy considerando tal tarea. Hay un conjunto de tareas que deben completarse rápidamente. PHP tiene otras herramientas para resolver este problema, no se mencionan aquí, el artículo trata sobre pthreads.



¿Qué son los subprocesos?

¡Eso es todo! Bueno, casi todo. De hecho, hay algo que puede molestar a un lector curioso. Nada de esto funciona en PHP estándar compilado con opciones predeterminadas. Para disfrutar del subproceso múltiple, debe tener habilitado ZTS (Zend Thread Safety) en su PHP.

configuración PHP

A continuación, PHP con ZTS. No preste atención a una diferencia tan grande en el tiempo de ejecución en comparación con PHP sin ZTS (37,65 frente a 265,05 segundos), no intenté reducirlo a un denominador común configuración PHP. En el caso sin ZTS, tengo habilitado XDebug, por ejemplo.


Como puede ver, cuando se utilizan 2 subprocesos, la velocidad de ejecución del programa es aproximadamente 1,5 veces mayor que en el caso del código lineal. Cuando se usan 4 hilos, 3 veces.


Puede observar que aunque el procesador es de 8 núcleos, el tiempo de ejecución del programa se mantuvo casi sin cambios si se utilizaron más de 4 subprocesos. Parece que esto se debe a que mi procesador tiene 4 núcleos físicos, para mayor claridad he representado la placa en forma de diagrama.


Resumen

En PHP, es posible trabajar de manera bastante elegante con subprocesos múltiples utilizando la extensión pthreads. Esto da un aumento notable en la productividad.

Etiquetas: Agregar etiquetas

A veces es necesario realizar varias acciones simultáneamente, por ejemplo, verificar cambios en una tabla de la base de datos y realizar modificaciones en otra. Además, si una de las operaciones (por ejemplo, verificar cambios) lleva mucho tiempo, es obvio que la ejecución secuencial no garantizará el equilibrio de recursos.

Para resolver este tipo de problema, la programación utiliza subprocesos múltiples: cada operación se coloca en un subproceso separado con una cantidad asignada de recursos y funciona dentro de él. Con este enfoque, todas las tareas se completarán por separado e independientemente.

Aunque PHP no admite subprocesos múltiples, existen varios métodos para emularlo, sobre los cuales hablaremos abajo.

1. Ejecutar varias copias del script: una copia por operación

//mujer.php if (!isset($_GET["thread"])) ( system("wget ​​​​http://localhost/woman.php?thread=make_me_happy"); system("wget ​​​​http: //localhost/ woman.php?thread=make_me_rich"); ) elseif ($_GET["thread"] == "make_me_happy") ( make_her_happy(); ) elseif ($_GET["thread"] == "make_me_rich" ) ( buscar_otro_uno( ); )

Cuando ejecutamos este script sin parámetros, automáticamente ejecuta dos copias de sí mismo, con ID de operación ("thread=make_me_happy" y "thread=make_me_rich"), que inician la ejecución de las funciones necesarias.

De esta manera logramos el resultado deseado: se realizan dos operaciones simultáneamente, pero esto, por supuesto, no es un subproceso múltiple, sino simplemente una muleta para realizar tareas simultáneamente.

2. Path of the Jedi: usando la extensión PCNTL

PCNTL es una extensión que le permite trabajar completamente con procesos. Además de la gestión, admite el envío de mensajes, comprobar el estado y establecer prioridades. Así es como se ve el script anterior usando PCNTL:

$pid = pcntl_fork(); if ($pid == 0) ( make_her_happy(); ) elseif ($pid > 0) ( $pid2 = pcntl_fork(); if ($pid2 == 0) ( find_another_one(); ) )

Parece bastante confuso, repasémoslo línea por línea.

En la primera línea, “bifurcamos” el proceso actual (la bifurcación copia un proceso conservando los valores de todas las variables), dividiéndolo en dos procesos (actual y secundario) que se ejecutan en paralelo.

Para entender dónde estamos este momento, en un proceso hijo o madre, la función pcntl_fork devuelve 0 para el hijo y el ID del proceso para la madre. Por lo tanto, en la segunda línea, miramos $pid, si es cero, entonces estamos en el proceso hijo - estamos ejecutando la función, de lo contrario, estamos en la madre (línea 4), luego creamos otro proceso y realizar la tarea de manera similar.

Proceso de ejecución del script:

Por lo tanto, el script crea 2 procesos secundarios más, que son sus copias y contienen las mismas variables con valores similares. Y utilizando el identificador devuelto por la función pcntl_fork, descubrimos en qué hilo estamos actualmente y realizamos las acciones necesarias.

Parece, desarrolladores PHP El paralelismo rara vez se utiliza. No hablaré de la simplicidad del código síncrono; la programación de un solo subproceso es, por supuesto, más simple y clara, pero a veces un pequeño uso del paralelismo puede aportar un aumento notable en el rendimiento.

En este artículo, veremos cómo se puede lograr el subproceso múltiple en PHP usando la extensión pthreads. Para hacer esto, necesitará tener instalada la versión ZTS (Zend Thread Safety) de PHP 7.x, junto con extensión instalada pthreads v3. (En el momento de escribir este artículo, en PHP 7.1, los usuarios deberán realizar la instalación desde la rama master en el repositorio de pthreads; consulte la extensión de terceros).

Una pequeña aclaración: pthreads v2 está destinado a PHP 5.x y ya no es compatible, pthreads v3 es para PHP 7.x y se está desarrollando activamente.

Después de semejante digresión, ¡vamos directo al grano!

Procesamiento de tareas únicas

A veces desea procesar tareas únicas en forma de subprocesos múltiples (por ejemplo, ejecutar alguna tarea vinculada a E/S). En tales casos, puede utilizar la clase Thread para crear un nuevo hilo y ejecutar algún procesamiento en un hilo separado.

Por ejemplo:

$tarea = nueva clase extiende Thread ( $respuesta privada; ejecución de función pública() ( $content = file_get_contents("http://google.com"); preg_match("~ (.+)~", $contenido, $coincidencias); $this->respuesta = $coincidencias; ) ); $tarea->iniciar() && $tarea->join(); var_dump($tarea->respuesta); // cadena (6) "Google"

Aquí el método de ejecución es nuestro procesamiento, que se ejecutará dentro de un nuevo hilo. Cuando se llama a Thread::start, se genera un nuevo hilo y se llama al método de ejecución. Luego unimos el hilo secundario al hilo principal llamando a Thread::join, que se bloqueará hasta que el hilo secundario haya terminado de ejecutarse. Esto asegura que la tarea termine de ejecutarse antes de que intentemos imprimir el resultado (que se almacena en $tarea->respuesta).

Puede que no sea deseable contaminar una clase con responsabilidades adicionales asociadas con la lógica de flujo (incluida la responsabilidad de definir un método de ejecución). Podemos distinguir dichas clases heredándolas de la clase Threaded. Luego se pueden ejecutar dentro de otro hilo:

La tarea de clase extiende Threaded ( public $respuesta; función pública someWork() ($content = file_get_contents("http://google.com"); preg_match("~ (.+) ~", $content, $matches); $ esto->respuesta = $coincidencias; ) ) $tarea = nueva tarea; $thread = nueva clase($tarea) extiende Thread ( $tarea privada; función pública __construct($tarea roscada) ( $this->tarea = $tarea; ) función pública ejecutar() ( $this->tarea->someWork( ); ) ); $hilo->iniciar() && $hilo->unir(); var_dump($tarea->respuesta);

Cualquier clase que deba ejecutarse en un hilo separado debe heredar de la clase Threaded. Esto se debe a que proporciona las capacidades necesarias para realizar procesamiento en diferentes subprocesos, así como seguridad implícita e interfaces útiles (como la sincronización de recursos).

Echemos un vistazo a la jerarquía de clases que ofrece la extensión pthreads:

Grupo volátil de trabajadores de subprocesos con subprocesos (implementa transitable y coleccionable)

Ya cubrimos y aprendimos los conceptos básicos de las clases Thread y Threaded, ahora echemos un vistazo a las otras tres (Worker, Volatile y Pool).

Reutilizar hilos

Iniciar un nuevo hilo para cada tarea que debe paralelizarse es bastante costoso. Esto se debe a que se debe implementar una arquitectura sin nada común en pthreads para lograr subprocesos múltiples dentro de PHP. Lo que significa que todo el contexto de ejecución de la instancia actual del intérprete PHP (incluyendo cada clase, interfaz, rasgo y función) debe copiarse para cada hilo creado. Debido a que esto tiene un impacto notable en el rendimiento, la transmisión siempre debe reutilizarse siempre que sea posible. Los subprocesos se pueden reutilizar de dos maneras: usando trabajadores o usando grupos.

La clase Worker se utiliza para realizar una serie de tareas de forma sincrónica dentro de otro hilo. Esto se hace creando una nueva instancia de Worker (que crea un nuevo hilo) y luego empujando las tareas a la pila de ese hilo separado (usando Worker::stack).

He aquí un pequeño ejemplo:

La tarea de clase extiende Threaded ( valor $ privado; función pública __construct(int $i) ( $this->value = $i; ) función pública run() ( usleep(250000); echo "Tarea: ($this->value) \n"; ) ) $trabajador = nuevo Trabajador(); $trabajador->inicio(); for ($i = 0; $i pila(nueva Tarea($i)); ) while ($trabajador->recoger()); $trabajador->apagado();

En el ejemplo anterior, 15 tareas para un nuevo objeto $worker se colocan en la pila mediante el método Worker::stack y luego se procesan en el orden en que fueron enviadas. El método Worker::collect, como se muestra arriba, se utiliza para limpiar tareas tan pronto como terminan de ejecutarse. Con él, dentro de un bucle while, bloqueamos el hilo principal hasta que todas las tareas en la pila se completen y borren, antes de llamar a Worker::shutdown. Terminar a un trabajador antes de tiempo (es decir, mientras todavía quedan tareas por completar) seguirá bloqueando el hilo principal hasta que todas las tareas hayan completado su ejecución, solo que las tareas no serán recolectadas como basura (lo que implica pérdidas de memoria).

La clase Worker proporciona varios otros métodos relacionados con su pila de tareas, incluido Worker::unstack para eliminar la última tarea apilada y Worker::getStacked para obtener el número de tareas en la pila de ejecución. La pila de un trabajador contiene solo las tareas que deben ejecutarse. Una vez que se ha completado una tarea en la pila, se elimina y se coloca en una pila separada (interna) para la recolección de basura (usando el método Worker::collect).

Otra forma de reutilizar un hilo en múltiples tareas es usar un grupo de hilos (a través de la clase Pool). Un grupo de subprocesos utiliza un grupo de trabajadores para permitir la ejecución de tareas. simultáneamente, en el que el factor de concurrencia (el número de subprocesos del grupo con el que opera) se establece cuando se crea el grupo.

Adaptemos el ejemplo anterior para utilizar un grupo de trabajadores:

La tarea de clase extiende Threaded ( valor $ privado; función pública __construct(int $i) ( $this->value = $i; ) función pública run() ( usleep(250000); echo "Tarea: ($this->value) \n"; ) ) $piscina = nueva piscina(4); para ($i = 0; $i envío(nueva tarea($i)); ) mientras ($pool->collect()); $piscina->apagar();

Existen algunas diferencias notables cuando se utiliza una piscina en comparación con un trabajador. En primer lugar, no es necesario iniciar el grupo manualmente; comienza a ejecutar tareas tan pronto como estén disponibles. En segundo lugar, nosotros enviar tareas a la piscina, no ponerlos en una pila. Además, la clase Pool no hereda de Threaded y, por lo tanto, no se puede pasar a otros subprocesos (a diferencia de Worker).

Cómo buena práctica Para los trabajadores y los grupos, siempre debe limpiar sus tareas tan pronto como se completen y luego finalizarlas manualmente. Los subprocesos creados utilizando la clase Thread también deben adjuntarse al subproceso principal.

pthreads y (in)mutabilidad

La última clase que tocaremos es Volatile, una nueva incorporación a pthreads v3. La inmutabilidad se ha convertido en un concepto importante en pthreads porque sin ella, el rendimiento se ve afectado significativamente. Por lo tanto, de forma predeterminada, las propiedades de las clases Threaded que son en sí mismas objetos Threaded ahora son inmutables y, por lo tanto, no se pueden sobrescribir después de su asignación inicial. Actualmente se prefiere la mutabilidad explícita para tales propiedades, y aún se puede lograr usando la nueva clase Volatile.

Veamos un ejemplo que demostrará las nuevas restricciones de inmutabilidad:

Class Task extiende Threaded // una clase Threaded (función pública __construct() ($this->data = new Threaded(); // $this->data no se puede sobrescribir, ya que es una propiedad Threaded de una clase Threaded)) $tarea = nueva clase(nueva Tarea()) extiende Thread ( // una clase Threaded, ya que Thread extiende la función pública Threaded __construct($tm) ( $this->threadedMember = $tm; var_dump($this->threadedMember-> datos); // objeto(Threaded)#3 (0) () $this->threadedMember = new StdClass(); // no válido, ya que la propiedad es un miembro Threaded de una clase Threaded ) );

Las propiedades roscadas de las clases volátiles, por otro lado, son mutables:

Class Task extiende Volatile (función pública __construct() ($this->data = new Threaded(); $this->data = new StdClass(); // válido, ya que estamos en una clase volátil) ) $task = new class(new Task()) extiende Thread ( función pública __construct($vm) ( $this->volatileMember = $vm; var_dump($this->volatileMember->data); // object(stdClass)#4 (0) () // aún no es válido, ya que Volatile extiende Threaded, por lo que la propiedad sigue siendo un miembro Threaded de una clase Threaded $this->volatileMember = new StdClass(); ) );

Podemos ver que la clase Volatile anula la inmutabilidad impuesta por la clase Threaded principal para proporcionar la capacidad de cambiar las propiedades de Threaded (así como unset()).

Hay otro tema de discusión para cubrir el tema de la variabilidad y la clase volátil: las matrices. En pthreads, las matrices se convierten automáticamente en objetos volátiles cuando se asignan a una propiedad de la clase Threaded. Esto se debe a que simplemente no es seguro manipular una serie de múltiples contextos PHP.

Veamos un ejemplo nuevamente para entender mejor algunas cosas:

$matriz = ; $tarea = nueva clase($matriz) extiende Thread ( $datos privados; función pública __construct(matriz $matriz) ( $this->data = $matriz; ) función pública ejecutar() ( $this->data = 4; $ esto->datos = 5; print_r($esto->datos); ) ); $tarea->iniciar() && $tarea->unirse(); /* Salida: Objeto volátil ( => 1 => 2 => 3 => 4 => 5) */

Vemos que los objetos volátiles se pueden tratar como si fueran matrices porque admiten operaciones de matriz como (como se muestra arriba) el operador subconjunto(). Sin embargo, las clases volátiles no admiten funciones de matriz básicas como array_pop y array_shift. En cambio, la clase Threaded nos proporciona operaciones como métodos integrados.

A modo de demostración:

$datos = nueva clase extiende Volátil ( público $a = 1; público $b = 2; público $c = 3; ); var_dump($datos); var_dump($datos->pop()); var_dump($datos->shift()); var_dump($datos); /* Salida: objeto(clase@anonimo)#1 (3) ( ["a"]=> int(1) ["b"]=> int(2) ["c"]=> int(3) ) int(3) int(1) objeto(clase@anónimo)#1 (1) ( ["b"]=> int(2) ) */

Otras operaciones admitidas incluyen Threaded::chunk y Threaded::merge .

Sincronización

En la última sección de este artículo, veremos la sincronización en pthreads. La sincronización es un método que le permite controlar el acceso a recursos compartidos.

Por ejemplo, implementemos un contador simple:

$contador = nueva clase extiende Thread (público $i = 0; función pública ejecutar() (para ($i = 0; $i i; ) ) ); $contador->inicio(); para ($i = 0; $i i; ) $contador->join(); var_dump($contador->i); // imprimirá un número del 10 al 20

Sin el uso de sincronización, el resultado no es determinista. Varios subprocesos escriben en la misma variable sin acceso controlado, lo que significa que se perderán las actualizaciones.

Arreglemos esto para que obtengamos la salida correcta de 20 agregando tiempo:

$contador = nueva clase extiende Thread ( public $i = 0; función pública ejecutar() ( $this->synchronized(function () ( for ($i = 0; $i i; ) )); ) ); $contador->inicio(); $contador->sincronizado(función ($contador) (para ($i = 0; $i i; ) ), $contador); $contador->unirse(); var_dump($contador->i); //int(20)

Los bloques de código sincronizados también pueden comunicarse entre sí utilizando los métodos Threaded::wait y Threaded::notify (o Threaded::notifyAll).

Aquí hay un incremento alternativo en dos bucles while sincronizados:

$contador = nueva clase extiende Thread ( public $cond = 1; función pública ejecutar() ($this->synchronized(function () ( for ($i = 0; $i notify(); if ($this->cond === 1) ( $esto->cond = 2; $esto->espera(); ) ) )); ) ); $contador->inicio(); $contador->sincronizado(función ($contador) ( if ($contador->cond !== 2) ( $contador->wait(); // espera a que el otro comience primero ) for ($i = 10; $notifico(); if ($contador->cond === 2) ( $contador->cond = 1; $contador->espera(); ) ) ), $contador); $contador->unirse(); /* Salida: int(0) int(10) int(1) int(11) int(2) int(12) int(3) int(13) int(4) int(14) int(5) int( 15) entero(6) entero(16) entero(7) entero(17) entero(8) entero(18) entero(9) entero(19) */

Es posible que observe condiciones adicionales que se han colocado en torno a la llamada a Threaded::wait . Estas condiciones son críticas porque permiten que la devolución de llamada sincronizada se reanude cuando se recibe una notificación y la condición especificada es verdadera. Esto es importante porque las notificaciones pueden provenir de lugares distintos a cuando se llama a Threaded::notify. Por lo tanto, si las llamadas al método Threaded::wait no estuvieran incluidas en condiciones, ejecutaremos falsas llamadas de atención, lo que dará lugar a un comportamiento del código impredecible.

Conclusión

Analizamos las cinco clases del paquete pthreads (Threaded, Thread, Worker, Volatile y Pool) y cómo se usa cada clase. También analizamos el nuevo concepto de inmutabilidad en pthreads y brindamos una breve descripción general de las capacidades de sincronización admitidas. Con estos conceptos básicos implementados, ahora podemos comenzar a ver cómo se pueden usar pthreads en casos del mundo real. Este será el tema de nuestro próximo post.

Si te interesa la traducción del próximo post házmelo saber: comenta en las redes sociales. redes, vota a favor y comparte la publicación con colegas y amigos.

  • Programación,
  • Programación paralela
  • Recientemente probé pthreads y me sorprendió gratamente: es una extensión que agrega la capacidad de trabajar con múltiples subprocesos reales en PHP. Sin emulación, sin magia, sin falsificaciones: todo es real.



    Estoy considerando tal tarea. Hay un conjunto de tareas que deben completarse rápidamente. PHP tiene otras herramientas para resolver este problema, no se mencionan aquí, el artículo trata sobre pthreads.



    ¿Qué son los subprocesos?

    ¡Eso es todo! Bueno, casi todo. De hecho, hay algo que puede molestar a un lector curioso. Nada de esto funciona en PHP estándar compilado con opciones predeterminadas. Para disfrutar del subproceso múltiple, debe tener habilitado ZTS (Zend Thread Safety) en su PHP.

    configuración PHP

    A continuación, PHP con ZTS. No importa la gran diferencia en el tiempo de ejecución en comparación con PHP sin ZTS (37,65 frente a 265,05 segundos), no intenté generalizar la configuración de PHP. En el caso sin ZTS, tengo habilitado XDebug, por ejemplo.


    Como puede ver, cuando se utilizan 2 subprocesos, la velocidad de ejecución del programa es aproximadamente 1,5 veces mayor que en el caso del código lineal. Cuando se usan 4 hilos, 3 veces.


    Puede observar que aunque el procesador es de 8 núcleos, el tiempo de ejecución del programa se mantuvo casi sin cambios si se utilizaron más de 4 subprocesos. Parece que esto se debe a que mi procesador tiene 4 núcleos físicos, para mayor claridad he representado la placa en forma de diagrama.


    Resumen

    En PHP, es posible trabajar de manera bastante elegante con subprocesos múltiples utilizando la extensión pthreads. Esto da un aumento notable en la productividad.

    Etiquetas:

    • PHP
    • hilos
    Agregar etiquetas

    Recientemente probé pthreads y me sorprendió gratamente: es una extensión que agrega la capacidad de trabajar con múltiples subprocesos reales en PHP. Sin emulación, sin magia, sin falsificaciones: todo es real.



    Estoy considerando tal tarea. Hay un conjunto de tareas que deben completarse rápidamente. PHP tiene otras herramientas para resolver este problema, no se mencionan aquí, el artículo trata sobre pthreads.



    ¿Qué son los subprocesos?

    ¡Eso es todo! Bueno, casi todo. De hecho, hay algo que puede molestar a un lector curioso. Nada de esto funciona en PHP estándar compilado con opciones predeterminadas. Para disfrutar del subproceso múltiple, debe tener habilitado ZTS (Zend Thread Safety) en su PHP.

    configuración PHP

    A continuación, PHP con ZTS. No importa la gran diferencia en el tiempo de ejecución en comparación con PHP sin ZTS (37,65 frente a 265,05 segundos), no intenté generalizar la configuración de PHP. En el caso sin ZTS, tengo habilitado XDebug, por ejemplo.


    Como puede ver, cuando se utilizan 2 subprocesos, la velocidad de ejecución del programa es aproximadamente 1,5 veces mayor que en el caso del código lineal. Cuando se usan 4 hilos, 3 veces.


    Puede observar que aunque el procesador es de 8 núcleos, el tiempo de ejecución del programa se mantuvo casi sin cambios si se utilizaron más de 4 subprocesos. Parece que esto se debe a que mi procesador tiene 4 núcleos físicos, para mayor claridad he representado la placa en forma de diagrama.


    Resumen

    En PHP, es posible trabajar de manera bastante elegante con subprocesos múltiples utilizando la extensión pthreads. Esto da un aumento notable en la productividad.



    
    Arriba