├── .gitignore ├── Manager.php ├── README.md ├── Worker.php ├── WorkerPipe.php └── example.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* -------------------------------------------------------------------------------- /Manager.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com) 11 | * @license http://www.opensource.org/licenses/mit-license.php MIT 12 | * @link http://docblox-project.org 13 | */ 14 | 15 | /** 16 | * Manager class for Parallel processes. 17 | * 18 | * This class will manage the workers and make sure all processes are executed 19 | * in parallel and not too many at the same time. 20 | * 21 | * @category DocBlox 22 | * @package Parallel 23 | * @author Mike van Riel 24 | * @license http://www.opensource.org/licenses/mit-license.php MIT 25 | * @link http://docblox-project.org 26 | */ 27 | class DocBlox_Parallel_Manager extends ArrayObject 28 | { 29 | /** @var int The maximum number of processes to run simultaneously */ 30 | protected $process_limit = 2; 31 | 32 | /** @var boolean Tracks whether this manager is currently executing */ 33 | protected $is_running = false; 34 | 35 | /** 36 | * Tries to autodetect the optimal number of process by counting the number 37 | * of processors. 38 | * 39 | * @param array $input Input for the array object. 40 | * @param int $flags flags for the array object. 41 | * @param string $iterator_class Iterator class for this array object. 42 | */ 43 | public function __construct( 44 | $input = array(), $flags = 0, $iterator_class = "ArrayIterator" 45 | ) { 46 | parent::__construct($input, $flags, $iterator_class); 47 | 48 | if (is_readable('/proc/cpuinfo')) { 49 | $processors = 0; 50 | exec("cat /proc/cpuinfo | grep processor | wc -l", $processors); 51 | $this->setProcessLimit(reset($processors)); 52 | } 53 | } 54 | 55 | /** 56 | * Adds a worker to to the queue. 57 | * 58 | * This method will prepare a worker to be executed in parallel once the 59 | * execute method is invoked. 60 | * A fluent interface is provided so that you can chain multiple workers 61 | * in one call. 62 | * 63 | * Example: 64 | * 65 | * $cb1 = function() { var_dump('a'); sleep(1); }; 66 | * $cb2 = function() { var_dump('b'); sleep(1); }; 67 | * 68 | * $mgr = new DocBlox_Parallel_Manager(); 69 | * $mgr->setProcessLimit(2) 70 | * ->addWorker(new DocBlox_Parallel_Worker($cb1)) 71 | * ->addWorker(new DocBlox_Parallel_Worker($cb2)) 72 | * ->execute(); 73 | * 74 | * @param int $index The key for this worker. 75 | * @param DocBlox_Parallel_Worker $newval The worker to add onto the queue. 76 | * 77 | * @see DocBlox_Parallel_Manager::execute() 78 | * 79 | * @throws RuntimeException if this method is invoked while the 80 | * manager is busy executing tasks. 81 | * @throws InvalidArgumentException if the provided element is not of type 82 | * DocBlox_Parallel_Worker. 83 | * 84 | * @return void 85 | */ 86 | public function offsetSet($index, $newval) 87 | { 88 | if (!$newval instanceof DocBlox_Parallel_Worker) { 89 | throw new InvalidArgumentException( 90 | 'Provided element must be of type DocBlox_Parallel_Worker' 91 | ); 92 | } 93 | if ($this->isRunning()) { 94 | throw new RuntimeException( 95 | 'Workers may not be added during execution of the manager' 96 | ); 97 | } 98 | 99 | parent::offsetSet($index, $newval); 100 | } 101 | 102 | /** 103 | * Convenience method to make the addition of workers explicit and allow a 104 | * fluent interface. 105 | * 106 | * @param DocBlox_Parallel_Worker $worker The worker to add onto the queue. 107 | * 108 | * @return self 109 | */ 110 | public function addWorker(DocBlox_Parallel_Worker $worker) 111 | { 112 | $this[] = $worker; 113 | 114 | return $this; 115 | } 116 | 117 | /** 118 | * Sets how many processes at most to execute at the same time. 119 | * 120 | * A fluent interface is provided so that you can chain multiple workers 121 | * in one call. 122 | * 123 | * @param int $process_limit The limit, minimum of 1 124 | * 125 | * @see DocBlox_Parallel_Manager::addWorker() for an example 126 | * 127 | * @return self 128 | */ 129 | public function setProcessLimit($process_limit) 130 | { 131 | if ($process_limit < 1) { 132 | throw new InvalidArgumentException( 133 | 'Number of simultaneous processes may not be less than 1' 134 | ); 135 | } 136 | 137 | $this->process_limit = $process_limit; 138 | 139 | return $this; 140 | } 141 | 142 | /** 143 | * Returns the current limit on the amount of processes that can be 144 | * executed at the same time. 145 | * 146 | * @return int 147 | */ 148 | public function getProcessLimit() 149 | { 150 | return $this->process_limit; 151 | } 152 | 153 | /** 154 | * Returns whether the manager is executing the workers. 155 | * 156 | * @return boolean 157 | */ 158 | public function isRunning() 159 | { 160 | return $this->is_running; 161 | } 162 | 163 | /** 164 | * Executes each worker. 165 | * 166 | * This method loops through the list of workers and tries to fork as 167 | * many times as the ProcessLimit dictates at the same time. 168 | * 169 | * @return void 170 | */ 171 | public function execute() 172 | { 173 | /** @var int[] $processes */ 174 | $processes = $this->startExecution(); 175 | 176 | /** @var DocBlox_Parallel_Worker $worker */ 177 | foreach ($this as $worker) { 178 | 179 | // if requirements are not met, execute workers in series. 180 | if (!$this->checkRequirements()) { 181 | $worker->execute(); 182 | continue; 183 | } 184 | 185 | $this->forkAndRun($worker, $processes); 186 | } 187 | 188 | $this->stopExecution($processes); 189 | } 190 | 191 | /** 192 | * Notifies manager that execution has started, checks requirements and 193 | * returns array for child processes. 194 | * 195 | * If forking is not available because library requirements are not met 196 | * than the list of workers is processed in series and a E_USER_NOTICE is 197 | * triggered. 198 | * 199 | * @return int[] 200 | */ 201 | protected function startExecution() 202 | { 203 | $this->is_running = true; 204 | 205 | // throw a E_USER_NOTICE if the requirements are not met. 206 | if (!$this->checkRequirements()) { 207 | trigger_error( 208 | 'The PCNTL extension is not available, running workers in series ' 209 | . 'instead of parallel', 210 | E_USER_NOTICE 211 | ); 212 | } 213 | 214 | return array(); 215 | } 216 | 217 | /** 218 | * Waits for all processes to have finished and notifies the manager that 219 | * execution has stopped. 220 | * 221 | * @param int[] &$processes List of running processes. 222 | * 223 | * @return void 224 | */ 225 | protected function stopExecution(array &$processes) 226 | { 227 | // starting of processes has ended but some processes might still be 228 | // running wait for them to finish 229 | while (!empty($processes)) { 230 | pcntl_waitpid(array_shift($processes), $status); 231 | } 232 | 233 | /** @var DocBlox_Parallel_Worker $worker */ 234 | foreach ($this as $worker) { 235 | $worker->pipe->push(); 236 | } 237 | 238 | $this->is_running = false; 239 | } 240 | 241 | /** 242 | * Forks the current process and calls the Worker's execute method OR 243 | * handles the parent process' execution. 244 | * 245 | * This is the really tricky part of the forking mechanism. Here we invoke 246 | * {@link http://www.php.net/manual/en/function.pcntl-fork.php pcntl_fork} 247 | * and either execute the forked process or deal with the parent's process 248 | * based on in which process we are. 249 | * 250 | * To fully understand what is going on here it is recommended to read the 251 | * PHP manual page on 252 | * {@link http://www.php.net/manual/en/function.pcntl-fork.php pcntl_fork} 253 | * and associated articles. 254 | * 255 | * If there are more workers than may be ran simultaneously then this method 256 | * will wait until a slot becomes available and then starts the next worker. 257 | * 258 | * @param DocBlox_Parallel_Worker $worker The worker to process. 259 | * @param int[] &$processes The list of running processes. 260 | * 261 | * @throws RuntimeException if we are unable to fork. 262 | * 263 | * @return void 264 | */ 265 | protected function forkAndRun( 266 | DocBlox_Parallel_Worker $worker, array &$processes 267 | ) { 268 | $worker->pipe = new DocBlox_Parallel_WorkerPipe($worker); 269 | 270 | // fork the process and register the PID 271 | $pid = pcntl_fork(); 272 | 273 | switch ($pid) { 274 | case -1: 275 | throw new RuntimeException('Unable to establish a fork'); 276 | case 0: // Child process 277 | $worker->execute(); 278 | 279 | $worker->pipe->pull(); 280 | 281 | // Kill -9 this process to prevent closing of shared file handlers. 282 | // Not doing this causes, for example, MySQL connections to be cleaned. 283 | posix_kill(getmypid(), SIGKILL); 284 | default: // Parent process 285 | // Keep track if the worker children 286 | $processes[] = $pid; 287 | 288 | if (count($processes) >= $this->getProcessLimit()) { 289 | pcntl_waitpid(array_shift($processes), $status); 290 | } 291 | break; 292 | } 293 | } 294 | 295 | /** 296 | * Returns true when all requirements are met. 297 | * 298 | * @return bool 299 | */ 300 | protected function checkRequirements() 301 | { 302 | return (bool)(extension_loaded('pcntl')); 303 | } 304 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Parallel 2 | ======== 3 | 4 | This is a library for introducing Parallelization into your project. 5 | See the `example.php` file for an example how to use this library. 6 | 7 | Theory of Operation 8 | ------------------- 9 | 10 | This library will enable the developer to execute a given amount of tasks 11 | (workers) in parallel. This is achieved by adding workers onto a manager, 12 | optionally defining how many processes to run simultaneously and then execute 13 | the manager. 14 | 15 | Under Linux this library will try to detect the number of processors and allow 16 | a maximum number of processes to run equal to the number of processors. If this 17 | cannot be determined or the user is running Windows then a default of 2 is used. 18 | 19 | Requirements and graceful degradation 20 | ------------------------------------- 21 | 22 | Parallelization has several requirements. But to allow distribution, without 23 | adding several requirements to your application, will this library execute the 24 | given tasks in serie if the requirements are not met. And throw a E_USER_NOTICE 25 | php error that explains to the user that dependencies are missing. 26 | 27 | The requirements for this library are: 28 | 29 | * A *NIX compatible operating system 30 | * Scripts must not run from an apache module 31 | * the PCNTL PHP extension (http://php.net/manual/en/book.pcntl.php) 32 | 33 | Workers 34 | ------- 35 | 36 | Workers are basically wrappers around callback functions or methods. As such you 37 | can use anything in your existing project and parallelize it. 38 | 39 | Do note that each parallel process is a duplicate of the original. This means 40 | that, for example, if you pass an object (or other reference) and change that, 41 | that the changes that you have made do not carry over to the caller. 42 | 43 | The return value of the given callback is stored as result on the worker and 44 | can be read using the `getResult()` method. 45 | 46 | Any exception that is thrown will result in an error, where the `getReturnCode()` 47 | method will return the exception code (be warned: this may be 0!) and the 48 | `getError()` method will return the exception message. 49 | 50 | Errors and exceptions 51 | --------------------- 52 | 53 | if a task throws an exception it is caught and registered as an error. The 54 | exception's code is used as error number, where the message is used as error 55 | message. 56 | 57 | By using this, instead of dying, you can continue execution of the other parallel 58 | processes and handle errors yourself after all processes have been executed. 59 | 60 | Examples 61 | -------- 62 | 63 | ### Fluent interface 64 | 65 | $mgr = new DocBlox_Parallel_Manager(); 66 | $mgr 67 | ->addWorker(new DocBlox_Parallel_Worker(function() { sleep(1); return 'a'; })) 68 | ->addWorker(new DocBlox_Parallel_Worker(function() { sleep(1); return 'b'; })) 69 | ->addWorker(new DocBlox_Parallel_Worker(function() { sleep(1); return 'c'; })) 70 | ->addWorker(new DocBlox_Parallel_Worker(function() { sleep(1); return 'd'; })) 71 | ->addWorker(new DocBlox_Parallel_Worker(function() { sleep(1); return 'e'; })) 72 | ->execute(); 73 | 74 | /** @var DocBlox_Parallel_Worker $worker */ 75 | foreach ($mgr as $worker) { 76 | var_dump($worker->getResult()); 77 | } 78 | 79 | ### Array interface 80 | 81 | $mgr = new DocBlox_Parallel_Manager(); 82 | $mgr[] = new DocBlox_Parallel_Worker(function() { sleep(1); return 'f'; }); 83 | $mgr[] = new DocBlox_Parallel_Worker(function() { sleep(1); return 'g'; }); 84 | $mgr[] = new DocBlox_Parallel_Worker(function() { sleep(1); return 'h'; }); 85 | $mgr[] = new DocBlox_Parallel_Worker(function() { sleep(1); return 'i'; }); 86 | $mgr[] = new DocBlox_Parallel_Worker(function() { sleep(1); return 'j'; }); 87 | $mgr->execute(); 88 | 89 | /** @var DocBlox_Parallel_Worker $worker */ 90 | foreach ($mgr as $worker) { 91 | var_dump($worker->getResult()); 92 | } 93 | 94 | TODO 95 | ---- 96 | 97 | * Improve docs 98 | * More intelligent process slots; currently only the oldest in a 'set' of slots 99 | is waited on but if this runs for a longer time then the other slots than 100 | those will not be filled as long as the first slot is occupied. 101 | * Last parts of IPC (Inter-Process Communication), to be able to return 102 | information from Workers to the Manager. 103 | 104 | * STDOUT 105 | * STDERR 106 | 107 | -------------------------------------------------------------------------------- /Worker.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com) 11 | * @license http://www.opensource.org/licenses/mit-license.php MIT 12 | * @link http://docblox-project.org 13 | */ 14 | 15 | /** 16 | * Class that represents the execution of a single task within a parallelized 17 | * frame. 18 | * 19 | * @category DocBlox 20 | * @package Parallel 21 | * @author Mike van Riel 22 | * @license http://www.opensource.org/licenses/mit-license.php MIT 23 | * @link http://docblox-project.org 24 | */ 25 | class DocBlox_Parallel_Worker 26 | { 27 | /** @var callback the task to execute for this worker */ 28 | protected $task = null; 29 | 30 | /** @var mixed[] A list of argument to pass to the task */ 31 | protected $arguments = array(); 32 | 33 | /** @var int The return code to tell the parent process how it went */ 34 | protected $return_code = -1; 35 | 36 | /** @var mixed The result of the given task */ 37 | protected $result = ''; 38 | 39 | /** @var string The error message, if an error occurred */ 40 | protected $error = ''; 41 | 42 | /** 43 | * Creates the worker and sets the task to execute optionally including 44 | * the arguments that need to be passed to the task. 45 | * 46 | * @param callback $task The task to invoke upon execution. 47 | * @param mixed[] $arguments The arguments to provide to the task. 48 | */ 49 | function __construct($task, array $arguments = array()) 50 | { 51 | $this->setTask($task); 52 | $this->arguments = $arguments; 53 | } 54 | 55 | /** 56 | * Returns the list of arguments as provided int he constructor. 57 | * 58 | * @see DocBlox_Parallel_Worker::__construct() 59 | * 60 | * @return mixed[] 61 | */ 62 | public function getArguments() 63 | { 64 | return $this->arguments; 65 | } 66 | 67 | /** 68 | * Returns the task as provided in the constructor. 69 | * 70 | * @see DocBlox_Parallel_Worker::__construct() 71 | * 72 | * @return callback 73 | */ 74 | public function getTask() 75 | { 76 | return $this->task; 77 | } 78 | 79 | /** 80 | * Returns the available return code. 81 | * 82 | * This method may return -1 if no return code is available yet. 83 | * 84 | * @return int 85 | */ 86 | public function getReturnCode() 87 | { 88 | return $this->return_code; 89 | } 90 | 91 | /** 92 | * Sets the return code for this worker. 93 | * 94 | * Recommended is to use the same codes as are used with 95 | * {@link http://www.gnu.org/software/bash/manual/html_node/Exit-Status.html 96 | * exit codes}. 97 | * 98 | * In short: 0 means that the task succeeded and a any other positive value 99 | * indicates an error condition. 100 | * 101 | * @param int $return_code Recommended to be a positive number 102 | * 103 | * @throw InvalidArgumentException if the code is not a number or negative 104 | * 105 | * @return void 106 | */ 107 | public function setReturnCode($return_code) 108 | { 109 | if (!is_numeric($return_code) || ($return_code < 0)) { 110 | throw new InvalidArgumentException( 111 | 'Expected the return code to be a positive number' 112 | ); 113 | } 114 | 115 | $this->return_code = $return_code; 116 | } 117 | 118 | /** 119 | * Returns the error message associated with the return code. 120 | * 121 | * @return string 122 | */ 123 | public function getError() 124 | { 125 | return $this->error; 126 | } 127 | 128 | /** 129 | * Sets the error message. 130 | * 131 | * @param string $error The error message. 132 | * 133 | * @return void 134 | */ 135 | public function setError($error) 136 | { 137 | $this->error = $error; 138 | } 139 | 140 | /** 141 | * Returns the result for this task run. 142 | * 143 | * @return null|mixed 144 | */ 145 | public function getResult() 146 | { 147 | return $this->result; 148 | } 149 | 150 | /** 151 | * Sets the result for this task run. 152 | * 153 | * @param mixed $result The value that is returned by the task; can be anything. 154 | * 155 | * @return void 156 | */ 157 | public function setResult($result) 158 | { 159 | $this->result = $result; 160 | } 161 | 162 | /** 163 | * Invokes the task with the given arguments and processes the output. 164 | * 165 | * @return void. 166 | */ 167 | public function execute() 168 | { 169 | $this->setReturnCode(0); 170 | try { 171 | $this->setResult( 172 | call_user_func_array($this->getTask(), $this->getArguments()) 173 | ); 174 | } catch (Exception $e) { 175 | $this->setError($e->getMessage()); 176 | $this->setReturnCode($e->getCode()); 177 | } 178 | } 179 | 180 | /** 181 | * Sets the task for this worker and validates whether it is callable. 182 | * 183 | * @param callback $task The task to execute when the execute method 184 | * is invoked. 185 | * 186 | * @throws InvalidArgumentException if the given argument is not a callback. 187 | * 188 | * @see DocBlox_Parallel_Worker::__construct() 189 | * @see DocBlox_Parallel_Worker::execute() 190 | * 191 | * @return void 192 | */ 193 | protected function setTask($task) 194 | { 195 | if (!is_callable($task)) { 196 | throw new InvalidArgumentException( 197 | 'Worker task is not a callable object' 198 | ); 199 | } 200 | 201 | $this->task = $task; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /WorkerPipe.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com) 11 | * @license http://www.opensource.org/licenses/mit-license.php MIT 12 | * @link http://docblox-project.org 13 | */ 14 | 15 | /** 16 | * Class that represents a named pipe for a Worker. 17 | * 18 | * This class manages the named pipe for a worker and is able to push and pull 19 | * specific data to facilitate IPC (interprocess communication). 20 | * 21 | * @category DocBlox 22 | * @package Parallel 23 | * @author Mike van Riel 24 | * @license http://www.opensource.org/licenses/mit-license.php MIT 25 | * @link http://docblox-project.org 26 | */ 27 | class DocBlox_Parallel_WorkerPipe 28 | { 29 | /** @var DocBlox_Parallel_Worker worker class that is associated */ 30 | protected $worker; 31 | 32 | /** @var string Path to the pipe */ 33 | protected $path; 34 | 35 | /** 36 | * Initializes the named pipe. 37 | * 38 | * @param DocBlox_Parallel_Worker $worker Associated worker. 39 | */ 40 | public function __construct(DocBlox_Parallel_Worker $worker) 41 | { 42 | $this->worker = $worker; 43 | 44 | $this->path = tempnam(sys_get_temp_dir(), 'dpm_'); 45 | posix_mkfifo($this->path, 0750); 46 | } 47 | 48 | /** 49 | * If the named pipe was not cleaned up, do so now. 50 | */ 51 | public function __destruct() 52 | { 53 | if (file_exists($this->path)) { 54 | $this->release(); 55 | } 56 | } 57 | 58 | /** 59 | * Pull the worker data into the named pipe. 60 | * 61 | * @return void 62 | */ 63 | public function pull() 64 | { 65 | $this->writePipeContents(); 66 | } 67 | 68 | /** 69 | * Push the worker data back onto the worker and release the pipe. 70 | * 71 | * @return void 72 | */ 73 | public function push() 74 | { 75 | list($result, $error, $return_code) = $this->readPipeContents(); 76 | $this->release(); 77 | 78 | $this->worker->setResult($result); 79 | $this->worker->setError($error); 80 | $this->worker->setReturnCode($return_code); 81 | } 82 | 83 | /** 84 | * Convenience method to show relation to readPipeContents. 85 | * 86 | * @return void 87 | */ 88 | protected function writePipeContents() 89 | { 90 | // push the gathered data onto a name pipe 91 | $pipe = fopen($this->path, 'w'); 92 | fwrite( 93 | $pipe, serialize( 94 | array( 95 | $this->worker->getResult(), 96 | $this->worker->getError(), 97 | $this->worker->getReturnCode() 98 | ) 99 | ) 100 | ); 101 | fclose($pipe); 102 | } 103 | 104 | /** 105 | * Returns the unserialized contents of the pipe. 106 | * 107 | * @return array 108 | */ 109 | protected function readPipeContents() 110 | { 111 | $pipe = fopen($this->path, 'r+'); 112 | $result = unserialize(fread($pipe, filesize($this->path))); 113 | fclose($pipe); 114 | 115 | return $result; 116 | } 117 | 118 | /** 119 | * Releases the pipe. 120 | * 121 | * @return void 122 | */ 123 | protected function release() 124 | { 125 | unlink($this->path); 126 | } 127 | } -------------------------------------------------------------------------------- /example.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com) 11 | * @license http://www.opensource.org/licenses/mit-license.php MIT 12 | * @link http://docblox-project.org 13 | */ 14 | 15 | /** Include the manager as we do not autoload */ 16 | require_once 'Manager.php'; 17 | 18 | /** Include the worker as we do not autoload */ 19 | require_once 'Worker.php'; 20 | 21 | /** Include the worker's pipe as we do not autoload */ 22 | require_once 'WorkerPipe.php'; 23 | 24 | // ----------------------------------------------------------------------------- 25 | // method 1: using a fluent interface and the addWorker helper. 26 | // ----------------------------------------------------------------------------- 27 | 28 | $mgr = new DocBlox_Parallel_Manager(); 29 | $mgr 30 | ->addWorker(new DocBlox_Parallel_Worker(function() { sleep(1); return 'a'; })) 31 | ->addWorker(new DocBlox_Parallel_Worker(function() { sleep(1); return 'b'; })) 32 | ->addWorker(new DocBlox_Parallel_Worker(function() { sleep(1); return 'c'; })) 33 | ->addWorker(new DocBlox_Parallel_Worker(function() { sleep(1); return 'd'; })) 34 | ->addWorker(new DocBlox_Parallel_Worker(function() { sleep(1); return 'e'; })) 35 | ->execute(); 36 | 37 | /** @var DocBlox_Parallel_Worker $worker */ 38 | foreach ($mgr as $worker) { 39 | var_dump($worker->getResult()); 40 | } 41 | 42 | // ----------------------------------------------------------------------------- 43 | // method 2: using the manager as worker array 44 | // ----------------------------------------------------------------------------- 45 | 46 | $mgr = new DocBlox_Parallel_Manager(); 47 | $mgr[] = new DocBlox_Parallel_Worker(function() { sleep(1); return 'f'; }); 48 | $mgr[] = new DocBlox_Parallel_Worker(function() { sleep(1); return 'g'; }); 49 | $mgr[] = new DocBlox_Parallel_Worker(function() { sleep(1); return 'h'; }); 50 | $mgr[] = new DocBlox_Parallel_Worker(function() { sleep(1); return 'i'; }); 51 | $mgr[] = new DocBlox_Parallel_Worker(function() { sleep(1); return 'j'; }); 52 | $mgr->execute(); 53 | 54 | /** @var DocBlox_Parallel_Worker $worker */ 55 | foreach ($mgr as $worker) { 56 | var_dump($worker->getResult()); 57 | } 58 | --------------------------------------------------------------------------------