thread - Ejecute la tarea PHP asincrónicamente
php thread (14)
Trabajo en una aplicación web algo grande, y el backend está principalmente en PHP. Hay varios lugares en el código donde debo completar alguna tarea, pero no quiero que el usuario espere el resultado. Por ejemplo, al crear una nueva cuenta, necesito enviarles un correo electrónico de bienvenida. Pero cuando presionen el botón ''Finalizar registro'', no quiero que esperen hasta que se envíe el correo electrónico, solo quiero comenzar el proceso y devolverle un mensaje al usuario de inmediato.
Hasta ahora, en algunos lugares he estado usando lo que parece un truco con exec (). Básicamente haciendo cosas como:
exec("doTask.php $arg1 $arg2 $arg3 >/dev/null 2>&1 &");
Lo cual parece funcionar, pero me pregunto si hay una mejor manera. Estoy pensando en escribir un sistema que ponga en cola las tareas en una tabla MySQL, y un script PHP de larga ejecución separado que consulte esa tabla una vez por segundo y ejecute las nuevas tareas que encuentre. Esto también tendría la ventaja de permitirme dividir las tareas entre varias máquinas de trabajadores en el futuro si fuera necesario.
¿Estoy reinventando la rueda? ¿Hay una solución mejor que el hack exec () o la cola MySQL?
Aquí hay una clase simple que codifiqué para mi aplicación web. Permite bifurcar scripts PHP y otros scripts. Funciona en UNIX y Windows.
class BackgroundProcess {
static function open($exec, $cwd = null) {
if (!is_string($cwd)) {
$cwd = @getcwd();
}
@chdir($cwd);
if (strtoupper(substr(PHP_OS, 0, 3)) == ''WIN'') {
$WshShell = new COM("WScript.Shell");
$WshShell->CurrentDirectory = str_replace(''/'', ''//', $cwd);
$WshShell->Run($exec, 0, false);
} else {
exec($exec . " > /dev/null 2>&1 &");
}
}
static function fork($phpScript, $phpExec = null) {
$cwd = dirname($phpScript);
@putenv("PHP_FORCECLI=true");
if (!is_string($phpExec) || !file_exists($phpExec)) {
if (strtoupper(substr(PHP_OS, 0, 3)) == ''WIN'') {
$phpExec = str_replace(''/'', ''//', dirname(ini_get(''extension_dir''))) . ''/php.exe'';
if (@file_exists($phpExec)) {
BackgroundProcess::open(escapeshellarg($phpExec) . " " . escapeshellarg($phpScript), $cwd);
}
} else {
$phpExec = exec("which php-cli");
if ($phpExec[0] != ''/'') {
$phpExec = exec("which php");
}
if ($phpExec[0] == ''/'') {
BackgroundProcess::open(escapeshellarg($phpExec) . " " . escapeshellarg($phpScript), $cwd);
}
}
} else {
if (strtoupper(substr(PHP_OS, 0, 3)) == ''WIN'') {
$phpExec = str_replace(''/'', ''//', $phpExec);
}
BackgroundProcess::open(escapeshellarg($phpExec) . " " . escapeshellarg($phpScript), $cwd);
}
}
}
Creo que deberías probar esta técnica, será útil llamar tantas páginas como quieras, todas las páginas se ejecutarán a la vez de forma independiente sin esperar a que cada página responda como asincrónica.
cornjobpage.php // mainpage
<?php
post_async("http://localhost/projectname/testpage.php", "Keywordname=testValue");
//post_async("http://localhost/projectname/testpage.php", "Keywordname=testValue2");
//post_async("http://localhost/projectname/otherpage.php", "Keywordname=anyValue");
//call as many as pages you like all pages will run at once independently without waiting for each page response as asynchronous.
?>
<?php
/*
* Executes a PHP page asynchronously so the current page does not have to wait for it to finish running.
*
*/
function post_async($url,$params)
{
$post_string = $params;
$parts=parse_url($url);
$fp = fsockopen($parts[''host''],
isset($parts[''port''])?$parts[''port'']:80,
$errno, $errstr, 30);
$out = "GET ".$parts[''path'']."?$post_string"." HTTP/1.1/r/n";//you can use POST instead of GET if you like
$out.= "Host: ".$parts[''host'']."/r/n";
$out.= "Content-Type: application/x-www-form-urlencoded/r/n";
$out.= "Content-Length: ".strlen($post_string)."/r/n";
$out.= "Connection: Close/r/n/r/n";
fwrite($fp, $out);
fclose($fp);
}
?>
testpage.php
<?
echo $_REQUEST["Keywordname"];//case1 Output > testValue
?>
PD: si desea enviar parámetros de url como bucle, siga esta respuesta: https://.com/a/41225209/6295712
Cuando solo desea ejecutar una o varias solicitudes HTTP sin tener que esperar la respuesta, también hay una solución PHP simple.
En el script de llamada:
$socketcon = fsockopen($host, 80, $errno, $errstr, 10);
if($socketcon) {
$socketdata = "GET $remote_house/script.php?parameters=... HTTP 1.1/r/nHost: $host/r/nConnection: Close/r/n/r/n";
fwrite($socketcon, $socketdata);
fclose($socketcon);
}
// repeat this with different parameters as often as you like
En el script.php llamado, puede invocar estas funciones de PHP en las primeras líneas:
ignore_user_abort(true);
set_time_limit(0);
Esto hace que el script continúe ejecutándose sin límite de tiempo cuando se cierra la conexión HTTP.
Desafortunadamente, PHP no tiene ningún tipo de capacidades nativas de enhebrado. Entonces, creo que en este caso no tienes más remedio que usar algún tipo de código personalizado para hacer lo que quieres hacer.
Si busca en la red cosas de enhebrado de PHP, algunas personas han encontrado formas de simular hilos en PHP.
Es una gran idea usar cURL como lo sugiere rojoca.
Aquí hay un ejemplo. Puede supervisar text.txt mientras el script se ejecuta en segundo plano:
<?php
function doCurl($begin)
{
echo "Do curl<br />/n";
$url = ''http://''.$_SERVER[''SERVER_NAME''].$_SERVER[''REQUEST_URI''];
$url = preg_replace(''//?.*/'', '''', $url);
$url .= ''?begin=''.$begin;
echo ''URL: ''.$url.''<br>'';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($ch);
echo ''Result: ''.$result.''<br>'';
curl_close($ch);
}
if (empty($_GET[''begin''])) {
doCurl(1);
}
else {
while (ob_get_level())
ob_end_clean();
header(''Connection: close'');
ignore_user_abort();
ob_start();
echo ''Connection Closed'';
$size = ob_get_length();
header("Content-Length: $size");
ob_end_flush();
flush();
$begin = $_GET[''begin''];
$fp = fopen("text.txt", "w");
fprintf($fp, "begin: %d/n", $begin);
for ($i = 0; $i < 15; $i++) {
sleep(1);
fprintf($fp, "i: %d/n", $i);
}
fclose($fp);
if ($begin < 10)
doCurl($begin + 1);
}
?>
Este es el mismo método que he usado durante un par de años y no he visto ni encontrado nada mejor. Como han dicho las personas, PHP tiene un único hilo, por lo que no hay mucho más que pueda hacer.
De hecho, he agregado un nivel extra a esto y eso es obtener y almacenar la identificación del proceso. Esto me permite redirigir a otra página y hacer que el usuario se siente en esa página, usando AJAX para verificar si el proceso está completo (la identificación del proceso ya no existe). Esto es útil para los casos en que la longitud de la secuencia de comandos haría que el navegador agote el tiempo de espera, pero el usuario debe esperar a que la secuencia de comandos se complete antes del siguiente paso. (En mi caso, estaba procesando archivos ZIP grandes con CSV como archivos que agregan hasta 30 000 registros a la base de datos después del cual el usuario necesita confirmar cierta información).
También utilicé un proceso similar para generar informes. No estoy seguro de utilizar el "procesamiento en segundo plano" para algo como un correo electrónico, a menos que haya un problema real con un SMTP lento. En su lugar, podría usar una tabla como cola y luego tener un proceso que se ejecuta cada minuto para enviar los correos electrónicos dentro de la cola. Debería estar preocupado de enviar correos electrónicos dos veces u otros problemas similares. Consideraría un proceso de cola similar para otras tareas también.
He usado Beanstalkd para un proyecto, y planeé hacerlo de nuevo. He encontrado que es una forma excelente de ejecutar procesos asíncronos.
Un par de cosas que he hecho con él son:
- Cambio de tamaño de la imagen, y con una cola ligeramente cargada que pasa a una secuencia de comandos PHP basada en CLI, el cambio de tamaño de imágenes grandes (2mb +) funcionó bien, pero tratando de cambiar el tamaño de las mismas imágenes en una instancia mod_php regularmente se encontraba con problemas de espacio de memoria (I limitó el proceso de PHP a 32MB, y el cambio de tamaño tomó más que eso)
- comprobaciones en el futuro cercano - beanstalkd tiene retrasos disponibles (haga que este trabajo esté disponible para ejecutarse solo después de X segundos) - para poder disparar 5 o 10 comprobaciones para un evento, un poco más tarde en el tiempo
Escribí un sistema basado en Zend-Framework para decodificar una url ''agradable'', por lo que, por ejemplo, para cambiar el tamaño de una imagen llamaría QueueTask(''/image/resize/filename/example.jpg'')
. La URL se decodificó primero en una matriz (módulo, controlador, acción, parámetros) y luego se convirtió en JSON para su inyección a la cola misma.
Una secuencia de comandos cli de larga ejecución recogió el trabajo de la cola, lo ejecutó (a través de Zend_Router_Simple) y, si es necesario, puso la información en memcached para que el sitio PHP lo recoja cuando sea necesario.
Una de las arrugas que también puse fue que el cli-script solo se ejecutaba en 50 bucles antes de reiniciar, pero si quería reiniciar como estaba previsto, lo hacía de inmediato (se ejecutaba a través de un script bash). Si hubo un problema y salí exit(0)
(el valor predeterminado para exit;
o die();
) primero se pausaría por un par de segundos.
He utilizado el enfoque de cola, y funciona bien, ya que puede posponer ese procesamiento hasta que la carga de su servidor esté inactiva, lo que le permite administrar su carga de forma bastante efectiva si puede dividir fácilmente "tareas que no son urgentes".
Rodar por su cuenta no es demasiado complicado, aquí hay algunas otras opciones para ver:
- GearMan : esta respuesta se escribió en 2009, y desde entonces GearMan tiene una opción popular, vea los comentarios a continuación.
- ActiveMQ si desea una cola de mensajes de código abierto completa.
- ZeroMQ : esta es una biblioteca de sockets muy buena que facilita la escritura de código distribuido sin tener que preocuparse demasiado por la programación del zócalo. Podría usarlo para la creación de colas de mensajes en un único host: simplemente tendría que presionar su aplicación web para que una cola que una aplicación de consola que se ejecuta continuamente consumiría en la próxima oportunidad adecuada.
- beanstalkd : solo encontré esto mientras escribía esta respuesta, pero parece interesante
- dropr es un proyecto de cola de mensajes basado en PHP, pero no se ha mantenido activamente desde septiembre de 2010
- php-enqueue es un contenedor mantenido recientemente (2017) en una variedad de sistemas de cola
- Finalmente, una publicación de blog sobre el uso de memcached para la espera de mensajes
Otro enfoque, quizás más simple, es usar ignore_user_abort : una vez que hayas enviado la página al usuario, puedes hacer tu procesamiento final sin temor a una finalización prematura, aunque esto tiene el efecto de parecer que prolonga la carga de la página del usuario perspectiva.
Otra forma de fork procesos es a través de curl. Puede configurar sus tareas internas como un servicio web. Por ejemplo:
Luego, en los scripts a los que accede el usuario, realiza llamadas al servicio:
$service->addTask(''t1'', $data); // post data to URL via curl
Su servicio puede realizar un seguimiento de la cola de tareas con mysql o lo que quiera, el punto es: todo está incluido en el servicio y su script solo consume URL. Esto lo libera para mover el servicio a otra máquina / servidor si es necesario (es decir, fácilmente escalable).
Agregar una autorización HTTP o un esquema de autorización personalizado (como los servicios web de Amazon) le permite abrir sus tareas para ser consumido por otras personas / servicios (si lo desea) y puede llevarlo más allá y agregar un servicio de monitoreo en la parte superior para realizar un seguimiento de cola y estado de la tarea.
- http: // dominio / queue? task = t1
- http: // dominio / queue? task = t2
- http: // dominio / queue / t1 / 100931
Requiere un poco de trabajo de configuración pero hay muchos beneficios.
PHP es un lenguaje de un único subproceso, por lo que no hay una forma oficial de iniciar un proceso asincrónico con él que no sea el uso de exec
o popen
. Hay una publicación en el blog sobre eso aquí . Su idea para una cola en MySQL es una buena idea también.
Su requisito específico aquí es enviar un correo electrónico al usuario. Tengo curiosidad sobre por qué estás tratando de hacer eso asincrónicamente ya que enviar un correo electrónico es una tarea bastante trivial y rápida de realizar. Supongo que si está enviando toneladas de correos electrónicos y su ISP lo está bloqueando por sospecha de spam, esa podría ser una razón para hacer cola, pero aparte de eso, no puedo pensar en ninguna razón para hacerlo de esta manera.
PHP tiene multihilo, simplemente no está habilitado por defecto, hay una extensión llamada pthreads que hace exactamente eso. Sin embargo, necesitarás php compilado con ZTS. (Thread Safe) Enlaces:
Si configura el encabezado HTTP de longitud del contenido en la respuesta "Gracias por registrar", el navegador debe cerrar la conexión después de que se haya recibido el número especificado de bytes. Esto deja el proceso del lado del servidor en ejecución (suponiendo que se establece ignore_user_abort) para que pueda terminar de funcionar sin hacer que el usuario final espere.
Por supuesto, tendrá que calcular el tamaño de su contenido de respuesta antes de representar los encabezados, pero eso es bastante fácil para respuestas cortas (escribir salida en una cadena, llamar strlen (), call header (), render string).
Este enfoque tiene la ventaja de no obligarlo a administrar una cola de "interfaz", y aunque es posible que necesite hacer algún trabajo en la parte de atrás para evitar que los procesos hijo HTTP se pisen, eso es algo que ya debe hacer. , de todas formas.
Si no quieres el ActiveMQ completo, te recomiendo que consideres RabbitMQ . RabbitMQ es un mensaje ligero que usa el estándar AMQP .
Recomiendo también buscar en php-amqplib , una popular biblioteca cliente de AMQP para acceder a los corredores de mensajes basados en AMQP.
Si solo se trata de proporcionar tareas costosas, en caso de que se admita php-fpm, ¿por qué no utilizar la función fastcgi_finish_request()
?
Esta función vacía todos los datos de respuesta al cliente y finaliza la solicitud. Esto permite realizar tareas que consumen mucho tiempo sin dejar abierta la conexión al cliente.
Realmente no usa asincronicidad de esta manera:
- Primero crea tu código principal.
- Ejecute
fastcgi_finish_request()
. - Haz todas las cosas pesadas.
Una vez más php-fpm es necesario.