Exécuter la tâche PHP de manière asynchrone

Je travaille sur une application web un peu grande, et le backend est principalement en PHP. Il y a plusieurs endroits dans le code où je dois effectuer une tâche, mais je ne veux pas que l'utilisateur attende le résultat. Par exemple, lors de la création d'un nouveau compte, je dois leur envoyer un e-mail de bienvenue. Mais quand ils cliquent sur le bouton "Terminer L'enregistrement", Je ne veux pas les faire attendre jusqu'à ce que l'e-mail soit réellement envoyé, je veux juste commencer le processus et renvoyer un message à l'utilisateur loin.

Jusqu'à présent, dans certains endroits, j'ai utilisé ce qui ressemble à un hack avec exec(). Fondamentalement, faire des choses comme:

exec("doTask.php $arg1 $arg2 $arg3 >/dev/null 2>&1 &");

Qui semble fonctionner, mais je me demande s'il y a un meilleur moyen. J'envisage d'écrire un système qui met en file d'attente des tâches dans une table MySQL, et un script PHP distinct de longue durée qui interroge cette table une fois par seconde, et exécute toutes les nouvelles tâches qu'il trouve. Cela aurait également l'avantage de me permettre de diviser les tâches entre plusieurs travailleurs machines l'avenir, si j'ai besoin.

Est - ce que je réinvente la roue? Existe-t-il une meilleure solution que le hack exec() ou la file D'attente MySQL?

126
demandé sur davr 0000-00-00 00:00:00

15 réponses

J'ai utilisé l'approche de file d'attente, et cela fonctionne bien car vous pouvez reporter ce traitement jusqu'à ce que la charge de votre serveur soit inactive, vous permettant de gérer votre charge assez efficacement si vous pouvez partitionner facilement les "tâches qui ne sont pas urgentes".

Rouler le vôtre n'est pas trop compliqué, voici quelques autres options à vérifier:

Une autre approche, peut-être plus simple, consiste à utiliser ignore_user_abort - Une fois que vous avez envoyé la page à l'utilisateur, vous pouvez faire votre final traitement sans crainte d'une résiliation prématurée, bien que cela ait pour effet de prolonger le chargement de la page du point de vue de l'utilisateur.

71
répondu Paul Dixon 2017-06-30 11:28:54

Lorsque vous voulez simplement exécuter une ou plusieurs requêtes HTTP sans avoir à attendre la réponse, il existe également une solution PHP simple.

Dans le script appelant:

$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

Sur le script appelé.php, vous pouvez invoquer ces fonctions PHP dans les premières lignes:

ignore_user_abort(true);
set_time_limit(0);

Cela entraîne la poursuite de l'exécution du script sans limite de temps lorsque la connexion HTTP est fermée.

19
répondu Markus 2011-06-29 21:05:25

Une autre façon de fork processus est via curl. Vous pouvez configurer vos tâches internes en tant que service web. Par exemple:

Ensuite, dans vos scripts accédés par l'utilisateur, faites des appels au service:

$service->addTask('t1', $data); // post data to URL via curl

Votre service peut garder une trace de la file d'attente des tâches avec mysql ou ce que vous voulez le point est: tout est enveloppé dans le service et votre script ne fait que consommer des URL. Cela vous libère pour déplacer le service vers une autre machine/serveur si nécessaire (c'est-à-dire facilement évolutif).

L'ajout d'une autorisation http ou d'un schéma d'autorisation personnalisé (comme les services web D'Amazon) vous permet d'ouvrir vos tâches pour être consommées par d'autres personnes/services (si vous le souhaitez) et vous pouvez aller plus loin et Ajouter un service de surveillance pour garder une trace de la file d'attente et de la tâche statut.

, Il prend un peu de travail, mais il y a beaucoup d'avantages.

17
répondu rojoca 2009-05-13 23:31:59

J'ai utilisé Beanstalkd pour un projet, et prévu de nouveau. J'ai trouvé que c'était un excellent moyen d'exécuter des processus asynchrones.

Quelques choses que j'ai faites avec:

  • redimensionnement de L'Image - et avec une file d'attente légèrement chargée passant à un script PHP basé sur CLI, le redimensionnement de grandes images (2 Mo+) fonctionnait très bien, mais essayer de redimensionner les mêmes images dans une instance mod_php rencontrait régulièrement des problèmes d'espace mémoire (j'ai limité le processus PHP à 32 Mo, et le redimensionnement a pris plus que cela)
  • contrôles Dans un avenir proche-beanstalkd a des retards à sa disposition ( rendre ce travail disponible pour fonctionner seulement après X secondes) - donc je peux déclencher 5 ou 10 contrôles pour un événement, un peu plus tard dans le temps

J'ai écrit un système basé sur Zend-Framework pour décoder une url "agréable", donc par exemple, pour redimensionner une image, il appellerait QueueTask('/image/resize/filename/example.jpg'). L'URL a d'abord été décodée en un tableau (module, contrôleur, action, Paramètres), puis convertie en JSON pour injection dans le file d'attente.

Un script CLI de longue durée a ensuite récupéré le travail dans la file d'attente, l'a exécuté (via Zend_Router_Simple), et si nécessaire, a mis des informations dans memcached pour que le site web PHP puisse le récupérer comme requis quand cela a été fait.

Une ride que j'ai également mise en place était que le script cli ne fonctionnait que pour 50 boucles avant de redémarrer, mais s'il voulait redémarrer comme prévu, il le ferait immédiatement (en cours d'exécution via un script bash). S'il y avait un problème et que j'ai fait exit(0) (la valeur par défaut pour exit; ou die();), Il ferait d'abord une pause de quelques secondes.

7
répondu Alister Bulman 2009-05-18 10:16:22

Si c'est juste une question de fournir des tâches coûteuses, en cas de php-fpm est pris en charge, pourquoi ne pas utiliser fastcgi_finish_request() fonction?

Cette fonction vide toutes les données de réponse au client et termine la requête. Cela permet d'effectuer des tâches chronophages sans laisser la connexion au client ouverte.

Vous n'utilisez pas vraiment l'asynchronicité de cette façon:

  1. faites d'abord tout votre code principal.
  2. Exécuter fastcgi_finish_request().
  3. marque tous les gros trucs.

Encore une fois php-fpm est nécessaire.

7
répondu Denys Gorobchenko 2016-12-01 22:07:52

Voici une classe simple que j'ai codée pour mon application web. Il permet de bifurquer des scripts PHP et d'Autres scripts. Fonctionne sous UNIX et 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);
        }
    }
}
5
répondu Andrew Moore 2009-05-13 20:44:08

C'est la même méthode que j'utilise depuis quelques années maintenant et je n'ai rien vu ou trouvé de mieux. Comme les gens l'ont dit, PHP est mono-thread, donc il n'y a pas grand-chose d'autre que vous pouvez faire.

J'ai effectivement ajouté un niveau supplémentaire à l'obtenir et stocker l'id de processus. Cela me permet de rediriger vers une autre page et d'avoir l'utilisateur Assis sur cette page, en utilisant AJAX pour vérifier si le processus est terminé(process id n'existe plus). Ceci est utile pour les cas où le la longueur du script provoquerait le délai d'expiration du navigateur, mais l'utilisateur doit attendre que ce script se termine avant l'étape suivante. (Dans mon cas, il traitait de gros fichiers ZIP avec des fichiers de type CSV qui ajoutent jusqu'à 30 000 enregistrements à la base de données, après quoi l'utilisateur doit confirmer certaines informations.)

J'ai également utilisé un processus similaire pour la génération de rapports. Je ne suis pas sûr d'utiliser le "traitement d'arrière-plan" pour quelque chose comme un e-mail, à moins qu'il y ait un vrai problème avec un SMTP lent. Au lieu de cela, je pourrais utiliser une table comme une file d'attente, puis avoir un processus qui s'exécute chaque minute pour envoyer les e-mails dans la file d'attente. Vous devez être warry d'envoyer des e-mails deux fois ou d'autres problèmes similaires. Je considérerais également un processus de file d'attente similaire pour d'autres tâches.

4
répondu Darryl Hein 2009-05-13 17:02:01

PHP Amultithreading, ce n'est tout simplement pas activé par défaut, il y a une extension appelée pthreads qui fait exactement cela. Vous aurez besoin de php compilé avec ZTS cependant. (Thread-Safe) Liens:

Exemples

Un Autre tutoriel

Pthreads PECL Extension

3
répondu Omar A. Shaban 2015-02-25 11:59:35

C'est une bonne idée d'utiliser cURL comme suggéré par rojoca.

Voici un exemple. Vous pouvez surveiller le texte.txt pendant l'exécution du script en arrière-plan:

<?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);
}

?>
2
répondu Kjeld 2013-05-29 19:07:20

Malheureusement, PHP n'a aucune sorte de capacités de threading natif. Donc, je pense que dans ce cas, vous n'avez pas le choix d'utiliser une sorte de code personnalisé pour faire ce que vous voulez faire.

Si vous recherchez sur le net des threads PHP, certaines personnes ont trouvé des moyens de simuler des threads sur PHP.

1
répondu Peter D 2009-05-13 16:20:27

Si vous définissez L'en-tête HTTP Content-Length dans votre réponse "merci D'avoir enregistré", le navigateur doit fermer la connexion après la réception du nombre d'octets spécifié. Cela laisse le processus côté serveur en cours d'exécution (en supposant que ignore_user_abort est défini) afin qu'il puisse finir de fonctionner sans faire attendre l'utilisateur final.

Bien sûr, vous devrez calculer la taille du contenu de votre réponse avant de rendre les en-têtes, mais c'est assez facile pour les réponses courtes (écrire sortie à une chaîne, appelez strlen (), Call header (), render string).

Cette approche a l'avantage de ne pas vous forcer à gérer une file d'attente "frontale", et bien que vous ayez peut-être besoin de faire un peu de travail sur le back-end pour empêcher les processus enfants HTTP de marcher les uns sur les autres, c'est quelque chose que vous deviez déjà faire, de toute façon.

1
répondu Peter 2011-08-05 23:09:43

Si vous ne voulez pas L'ActiveMQ complet, je recommande de considérer RabbitMQ . RabbitMQ est une messagerie légère qui utilise la norme AMQP .

Je recommande également de regarder dans php-amqplib - une bibliothèque client AMQP populaire pour accéder aux courtiers de messages basés sur AMQP.

1
répondu phpPhil 2015-04-13 00:11:10

Je pense que vous devriez essayer cette technique, il aidera à appeler autant de pages que vous le souhaitez Toutes les pages s'exécuteront à la fois indépendamment sans attendre que chaque réponse de page soit asynchrone.

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
    ?>

PS:si vous voulez envoyer des paramètres d'url comme boucle puis suivez cette réponse :https://stackoverflow.com/a/41225209/6295712

0
répondu Hassan Saeed 2017-05-23 12:10:08

Frayer de nouveaux processus sur le serveur en utilisant exec() ou directement sur un autre serveur en utilisant curl ne s'adapte pas très bien, si nous optons pour exec, vous remplissez essentiellement votre serveur avec des processus longs qui peuvent être gérés par d'autres serveurs Non web, et l'utilisation de curl lie un autre serveur à moins

J'ai utilisé l'allemand dans quelques situations et je le trouve mieux pour ce genre de cas d'utilisation. Je peux utiliser un seul serveur de file d'attente de travail pour fondamentalement, gérer la file d'attente de tous les travaux devant être effectués par le serveur et spin up serveurs de travail, chacun d'entre eux peut exécuter autant d'instances du processus de travail que nécessaire, et augmenter le nombre de serveurs de travail au besoin et les faire tourner vers le bas lorsqu'ils ne sont pas nécessaires. Il me permet également d'arrêter les processus de travail entièrement en cas de besoin et de mettre en file d'attente les travaux jusqu'à ce que les travailleurs reviennent en ligne.

0
répondu Chris Rutherfurd 2017-12-31 06:56:02

PHP est un langage mono-thread, donc il n'y a pas de moyen officiel de démarrer un processus asynchrone avec lui autre que d'utiliser exec ou popen. Il y a un billet de blog à ce sujet ici . Votre idée pour une file D'attente dans MySQL est également une bonne idée.

Votre exigence spécifique ici est d'envoyer un e-mail à l'utilisateur. Je suis curieux de savoir pourquoi vous essayez de le faire de manière asynchrone puisque l'envoi d'un e-mail est une tâche assez triviale et rapide à effectuer. Je suppose que si vous envoyez des tonnes de email et votre FAI vous bloque sur des soupçons de spamming, cela pourrait être une raison de faire la queue, mais à part cela, je ne peux pas penser à une raison de le faire de cette façon.

-4
répondu Marc W 2009-05-13 16:21:03