Non-blocking IO

El uso de las extensiones RxPHP abre muchas posibilidades. Sus implementaciones observables, operadores e suscriptores/observadores son ciertamente poderosas. Lo que no proporcionan, sin embargo, es asincronía. Aquí es donde entra en juego la biblioteca React, proporcionando una capa de abstracción de I/O sin bloqueo controlada por eventos. Antes de tocar React, expongamos primero un ejemplo trivial de I/O bloqueadas versus no bloqueantes en PHP.

Creamos un pequeño script de baliza que simplemente generará una salida estándar (stdout) con el tiempo. Luego, crearemos un script que lea desde la entrada estándar (stdin) y veremos cómo se comporta cuando la lectura se realiza en el flujo bloqueado y flujo no bloqueado.
Comenzamos creando el archivo beacon.php con el siguiente contenido:

<?php
$now = time();
while ($now + $argv[1] > time()) {
echo 'signal ', microtime(), PHP_EOL;
usleep(200000); // 0.2s
}

El uso de $argv [1] sugiere que el archivo debe ejecutarse desde la consola. Usando $argv [1], proporcionamos una cantidad de segundos que deseamos que se ejecute el script. Dentro del bucle, tenemos una señal … salida, seguida de unos breves 0.2 segundos de suspensión del script.
Con nuestro script de baliza en su lugar, sigamos adelante y creemos un archivo index.php con el siguiente contenido:

<?php
// stream_set_blocking(STDIN, 0);
// stream_set_blocking(STDIN, 1); // default
echo 'start', PHP_EOL;
while (($line = fgets(STDIN)) !== false) {
echo $line;
}
echo 'end', PHP_EOL;

Además de dos salidas de inicio/fin obvias, utilizamos la función fgets() para leer desde la entrada estándar. El método stream_set_blocking() queda deliberadamente comentado por el momento. Tenga en cuenta que las dos secuencias de comandos no están relacionadas entre sí. En ningún momento index.php hace referencia al archivo beacon.php. Esto se debe a que usaremos la consola y su canalización (|) para unir la salida estándar del script beacon.php a una entrada estándar consumida por index.php:

php beacon.php 2 | php index.php

La salida resultante se muestra aquí:

No hay nada malo con esta salida; esto es lo que esperábamos. Primero vemos aparecer la cadena de inicio, luego varias ocurrencias de señal … y, finalmente, la cadena de final.
Sin embargo, ahí está el truco, toda la señal … los bits que son extraídos por la función fgets() de stdout son un ejemplo de bloqueo de IO. Si bien es posible que no lo percibamos como tal en este pequeño ejemplo, fácilmente podríamos imaginar un script de baliza enviando resultados desde un archivo muy grande o una conexión de base de datos lenta. Nuestro script index.php simplemente colgaría su ejecución bloqueada durante ese tiempo o, mejor dicho, esperaría el tiempo (($ line = fgets(STDIN) … línea para resolver.
¿Cómo podemos resolver el problema? Primero, debemos entender que esto no es realmente un problema técnico como tal. No hay nada malo en esperar para recibir datos. No importa cuánto resumamos las cosas, siempre habrá alguien o algo que necesite esperar datos en alguna parte. El truco es colocar el bit en algún lugar en el lugar correcto, por lo que no se interpone en el camino de la experiencia del usuario. Las promesas de JavaScript y las devoluciones de llamada son un ejemplo de dónde podemos querer colocar eso en algún lugar. Echemos un vistazo a la simple llamada AJAX realizada por la biblioteca JavaScript jQuery:

console.log('start-time: ' + Date.now());
$.ajax({
url: 'http://foggyline.net/',
success: function (result) {
console.log('result-time: ' + Date.now())
console.log(result)
}
});
console.log('end-time: ' + Date.now());

La siguiente captura de pantalla muestra el resultado resultante:

Observe cómo se han emitido la hora de inicio y la hora de finalización antes de la hora de resultado.
El JavaScript no bloqueó la ejecución en la línea $ .ajax ({… , como PHP hizo en su tiempo while (($ line = fgets (STDIN) … en el ejemplo anterior. Esto se debe a que el tiempo de ejecución de JavaScript es fundamentalmente diferente a PHP. La naturaleza asincrónica de JavaScript se basa en fragmentos de código para dividirse y ejecutarse por separado, luego actualizar lo que se necesita a través del mecanismo de devolución de llamada, una funcionalidad hecha posible por el modelo de concurrencia basado en el bucle de eventos de JavaScript y el mecanismo de cola de mensajes. La devolución de llamada en este caso fue la función anónima asignada a la propiedad de éxito de la llamada al método ajax(). Una vez que la llamada AJAX se ejecutó con éxito, llamó a la función de éxito asignada, que a su vez resultó la última en la salida, ya que la llamada AJAX tarda en ejecutarse.
Ahora, volvamos a nuestro pequeño ejemplo de PHP y modifiquemos el archivo index.php eliminando el comentario que colocamos delante de stream_set_blocking(STDIN, 0); .
Ejecutar el comando nuevamente, con la tubería (|) esta vez, ahora da como resultado una salida muy similar a la siguiente:

Esta vez, la línea while (($ line = fgets (STDIN) … no bloqueó la ejecución al esperar que beacon.php finalice. El truco reside en la función stream_set_blocking(), ya que nos permite controlar la transmisión modo de bloqueo, que por defecto está configurado para bloquear I/O.
Avancemos y hagamos un ejemplo más parecido a PHP, esta vez sin usar la tubería de la consola. Dejaremos el archivo beacon.php como está, pero modifique el archivo index.php de la siguiente manera:

<?php
echo 'start', PHP_EOL;
$process = proc_open('php beacon.php 2', [
['pipe', 'r'], // STDIN
['pipe', 'w'], // STDOUT
['file', './signals.log', 'a'] //STDERR
], $pipes);
//stream_set_blocking($pipes[1], 1); // Blocking I/O
//stream_set_blocking($pipes[1], 0); // Non-blocking I/O
while (proc_get_status($process)['running']) {
usleep(100000); // 0.1s
if ($signal = fgets($pipes[1])) {
echo $signal;
} else {
echo '--- beacon lost ---', PHP_EOL;
}
}
fclose($pipes[1]);
proc_close($process);
echo 'end', PHP_EOL;

Comenzamos con una función proc_open(), que nos permite ejecutar un comando y abrir punteros de archivo para entrada, salida y error estándar. El argumento ‘php beacon.php 2’ hace más o menos lo que hizo nuestro comando de consola, en lo que respecta a la parte del comando que queda del carácter de canalización. La forma en que captamos una salida del script beacon es usando la función fgets(). Sin embargo, no lo estamos haciendo directamente, lo estamos haciendo a través del ciclo while aquí, mientras que la condición es el estado de ejecución del proceso. En otras palabras, mientras el proceso se esté ejecutando, verifique si hay una nueva salida del proceso recién creado o no. Si hay una salida, muéstrela; si no, muestre el mensaje — baliza perdida —. La siguiente captura de pantalla muestra la salida resultante con I/O predeterminada (bloqueo):

Si ahora eliminamos el comentario delante de stream_set_blocking($ pipes [1], 0) ;, la salida resultante cambia a esto:

El resultado aquí muestra la relación sin bloqueo entre la baliza y nuestro script en ejecución. Al desbloquear la transmisión, pudimos utilizar la función fgets(), que normalmente bloquearía el script para verificar periódicamente la entrada estándar durante el tiempo que el proceso se esté ejecutando. En pocas palabras, ahora podemos leer el resultado de un subproceso, al tiempo que podemos inicializar algunos subprocesos más en el camino. Aunque el ejemplo en sí está muy lejos de la conveniencia del ejemplo de promesa/devolución de llamada jQuery, es un primer paso hacia las complejidades detrás de las I/O bloqueantes y no bloqueantes, como
afecta la forma en que escribimos nuestro código. Aquí es donde llegaremos a apreciar el papel de los observables RxPHP y los bucles de eventos React.

Comparte