[php5] multi-threading : accélération du temps de travail d'un script.

Contenu du snippet

L'article rédigé par Martin Roest (http://www.ibuildings.com/blog/archives/1539-Boost-performance-with-parallel-processing.html) nous montre à quel point il peut-être facile de jouer avec plusieurs processus en même temps, même en PHP.

J'ai personnellement été très séduit par la simplicité, mais je trouve que l'on pouvais encore faire plus simple : une petite classe :)

L'utilité d'un tel code se trouve sur des scripts de traitement, tel qu'un remaniement de base de donnée, une modifications sur plusieurs fichiers, etc. Par contre, il est important de savoir que la gestion des processus tel qu'il est utilisé ici ne fonctionne pas sous Windows, et qu'il n'est pas conseillé d'appeler ce script depuis un environnement type Apache.

En gros et pour faire simple, ce type de script s'utilise en ligne de commande (php-cli) et dans un environnement Unix, type cron.

Pour vous prouver l'efficacité de mes dires, voici un code bateau qui utilise 5 processus simultanés pour exécuter un script :

Source / Exemple :


<?php
/**


class ProcessManager {
	/**

  • Contain the Processus Id of the current processus
  • @var Integer $_iPid
  • /
private $_iPid; /**
  • Contain the priority for the current processus
  • @var Integer $_iPriority
  • /
private $_iPriority = 0; /**
  • Contain a list of all the childrens
  • (in case the current processus is the father)
  • @var Array $_aChildrens
  • /
private $_aChildrens = array (); /**
  • Contain the number of max allowed childrens
  • @var Integer $_iMaxChildrens
  • /
private $_iMaxChildrens = 2; /**
  • Constructor
  • Test if this application can be used, set the MaxChildren value,
  • retrieve his Process ID and set the signals
  • @param Integer $iMaxChildrens (optional)
  • @return ProcessManager
  • /
public function __construct ($iMaxChildrens = 2) { if (!function_exists ('pcntl_fork')) throw new Exception ('Your configuration does not include pcntl functions.'); if (!is_int ($iMaxChildrens) || $iMaxChildrens < 1) throw new Exception ('Childrens must be an Integer'); $this->_iMaxChildrens = $iMaxChildrens; $this->_iPid = getmypid (); // Setting up the signal handlers $this->addSignal (SIGTERM, array ($this, 'signalHandler')); $this->addSignal (SIGQUIT, array ($this, 'signalHandler')); $this->addSignal (SIGINT, array ($this, 'signalHandler')); } public function __destruct () { foreach ($this->_aChildrens as $iChildrensPid) pcntl_waitpid ($iChildrensPid, $iStatus); } /**
  • Fork a Processus
  • @return void
  • /
public function fork ($mFunction, $aParams = array ()) { if (!is_string ($mFunction) && !is_array ($mFunction)) throw new Exception ('Function given must be a String or an Array'); if (!is_array ($aParams)) throw new Exception ('Parameters must be an Array'); $iPid = pcntl_fork (); if ($iPid === -1) throw new Exception ('Unable to fork.'); elseif ($iPid > 0) { // We are in the parent process $this->_aChildrens[] = $iPid; if (count ($this->_aChildrens) >= $this->_iMaxChildrens) { pcntl_waitpid (array_shift ($this->_aChildrens), $iStatus); } } elseif ($iPid === 0) { // We are in the child process call_user_func_array ($mFunction, $aParams); exit (0); } } /**
  • Add a new signal that will be called to the given function with some optionnals parameters
  • @param Integer $iSignal
  • @param Mixed $mFunction
  • @param Array $aParams[optional]
  • @return void
  • /
public function addSignal ($iSignal, $mFunction) { if (!is_int ($iSignal)) throw new Exception ('Signal must be an Integer.'); if (!is_string ($mFunction) && !is_array ($mFunction)) throw new Exception ('Function to callback must be a String or an Array.'); if (!pcntl_signal ($iSignal, $mFunction)) throw new Exception ('Unable to set up the signal.'); } /**
  • The default signal handler, to avoid Zombies
  • @param Integer $iSignal
  • @return void
  • /
public function signalHandler ($iSignal = SIGTERM) { switch ($iSignal) { case SIGTERM: // Finish exit (0); break; case SIGQUIT: // Quit case SIGINT: // Stop from the keyboard case SIGKILL: // Kill exit (1); break; } } /**
  • Set the number of max childrens
  • @param Integer $iMaxChildren
  • @return void
  • /
public function setMaxChildren ($iMaxChildren) { if (!is_int ($iMaxChildrens) || $iMaxChildrens < 1) throw new Exception ('Childrens must be an Integer'); $this->_iMaxChildrens = $iMaxChildrens; } /**
  • Return the current number of MaxChildrens
  • @return Integer
  • /
public function getMaxChildrens () { return self::$_iMaxChildrens; } /**
  • Set the priority of the current processus.
  • @param Integer $iPriority
  • @param Integer $iProcessIdentifier[optional]
  • @return void
  • /
public function setPriority ($iPriority, $iProcessIdentifier = PRIO_PROCESS) { if (!is_int ($iPriority) || $iPriority < -20 || $iPriority > 20) throw new Exception ('Invalid priority.'); if ($iProcessIdentifier != PRIO_PROCESS || $iProcessIdentifier != PRIO_PGRP || $iProcessIdentifier != PRIO_USER) throw new Exception ('Invalid Process Identifier type.'); if (!pcntl_setpriority ($iPriority, $this->_iPid, $iProcessIdentifier)) throw new Exception ('Unable to set the priority.'); self::$_iPriority = $iPriority; } /**
  • Get the priority of the current processus.
  • @return Integer
  • /
public function getPriority () { return self::$_iPriority; } /**
  • Return the PID of the current process
  • @return Integer
  • /
public function getMyPid () { return $this->_iPid; } } ?> <?php ///////////////////////////////////////////////////////////////////////////////////////////// ?> <?php /**
  • Voici un exemple de ce que cela pourrait donner :
  • Dans un terminal sous linux, appelez le, et vous devriez voir la façon dont la méthode "doBigWork" est appelée (5 fois par 5 fois jusqu'à 12) (5, 5, 2)
  • /
require_once ('ProcessManager.php'); function doBigWork ($iWork) { echo 'Sleeping for Work N° '.$iWork."\n"; sleep (20); } try { // We instanciate the ProcessManager with 5 childs $oPM = new ProcessManager (5); } catch (Exception $oE) { die ('Your configuration does not support "pcntl" methods.'); } for ($i = 0; $i < 12; $i++) { // It could happen that the script couldn't fork a process. In that case, an Exception would be raised try { $oPM->fork ('doBigWork', array ($i)); } catch (Exception $oE) { echo 'Using non forked way :'."\n"; doBigWork ($i); } } ?>

Conclusion :


Au final, sans utiliser plusieurs processus, ce code aurait pris 12*20 = 240 secondes.
Avec 5 enfants, le temps de travail est divisé par ... 5, soit 48 secondes ! Quand même !

Bien entendu, vous pouvez augmenter le nombre d'enfant, tout dépendra des ressources que consomment votre fonction de travail (histoire de ne pas tuer votre machine (je l'ai fait pendant les tests :p)).

Une dernière modification qui serait sympathique, c'est d'inclure les fonctions lambdas dans la méthode fork, au lieu de l'appel à une méthode en utilisant le call_user_func_array. Mais ma configuration actuelle de Php n'est pas encore en 5.3, donc je ne peux ni jouer avec les closures, ni avec les fonctions lambdas :p Peut-être plus tard ? :)

A voir également

Vous n'êtes pas encore membre ?

inscrivez-vous, c'est gratuit et ça prend moins d'une minute !

Les membres obtiennent plus de réponses que les utilisateurs anonymes.

Le fait d'être membre vous permet d'avoir un suivi détaillé de vos demandes et codes sources.

Le fait d'être membre vous permet d'avoir des options supplémentaires.