├── .gitignore ├── LICENSE ├── README.md └── SimpleFork.php /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | .*.swp 3 | .*.swo 4 | ._* 5 | .DS_Store 6 | /Debug/ 7 | /ImgCache/ 8 | /Backup_rar/ 9 | /Debug/ 10 | /debug/ 11 | /upload/ 12 | /avatar/ 13 | /.idea/ 14 | /.vagrant/ 15 | node_modules 16 | Vagrantfile 17 | *.orig 18 | *.aps 19 | *.APS 20 | *.chm 21 | *.exp 22 | *.pdb 23 | *.rar 24 | .smbdelete* 25 | *.sublime* 26 | .sass-cache 27 | config.rb 28 | config.codekit 29 | npm-debug.log 30 | *.log 31 | *.iml 32 | config/local_loader.php 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 SegmentFault 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SimpleFork 2 | 3 | 一个最精简的php多进程控制库。它不依赖任何扩展以及其它的库,可以让你方便地利用系统的多个cpu来完成一些异步任务。我们封装了主进程和子进程之间的通信,以及日志打印,还有错误处理。 4 | 5 | ## 如何使用 6 | 7 | ### 安装 8 | 9 | 因为只有一个文件,你可以将其拷贝到任意位置。 10 | 11 | ### 使用 12 | 13 | #### 循环调用的时候(比如从任务队列获取任务常驻内存) 14 | 15 | ```php 16 | $sf = new SimpleFork(2, 'my-process'); // 2代表子进程数, 'my-process'是进程的名字 17 | 18 | $sf->master(function ($sf) { 19 | // 主进程的方法请包裹在master里 20 | while ($sf->loop(100)) { // 100为等待的毫秒数 21 | $sf->submit('http://www.google.cn/', function ($data) { // 使用submit方法将其提交到一个空闲的进程,如果没有空闲的,系统会自动等待 22 | echo $data; 23 | }); 24 | } 25 | })->slave(function ($url, $sf) { 26 | $sf->log('fetch %s', $url); // 使用内置的log方法,子进程的log也会被打印到主进程里 27 | return http_request($url); // 直接返回数据,主进程将在回调中收到 28 | }); 29 | ``` 30 | 31 | #### 一次调用的时候(比如有一个很大的工作需要分片处理) 32 | 33 | ```php 34 | $sf = new SimpleFork(5, 'map-reduce'); 35 | 36 | $sf->master(function ($sf) { 37 | $sf->submit([0, 10000]); 38 | $sf->submit([10000, 20000]); 39 | $sf->submit([20000, 30000]); 40 | $sf->submit([30000, 40000]); 41 | $sf->submit([40000, 50000]); 42 | 43 | $sf->wait(); // 等待所有任务执行完毕, 可以带一个timeout参数代表超时时间毫秒数, 超过后将强行终止还没完成的任务并返回 44 | })->slave(function ($params, $sf) { 45 | list ($from, $to) = $params; 46 | file_read_by_line($from, $to, 'example.txt'); 47 | }); 48 | ``` 49 | -------------------------------------------------------------------------------- /SimpleFork.php: -------------------------------------------------------------------------------- 1 | 8 | * @license MIT License 9 | */ 10 | class SimpleFork { 11 | /** 12 | * @var array 13 | */ 14 | private $_processes = []; 15 | 16 | /** 17 | * @var bool 18 | */ 19 | private $_isForked = false; 20 | 21 | /** 22 | * @var string 23 | */ 24 | private $_cmd; 25 | 26 | /** 27 | * @var string 28 | */ 29 | private $_name; 30 | 31 | /** 32 | * @var int 33 | */ 34 | private $_limit = 2; 35 | 36 | /** 37 | * @var int 38 | */ 39 | private $_busy = 0; 40 | 41 | /** 42 | * @var callable 43 | */ 44 | private $_masterHandler = NULL; 45 | 46 | /** 47 | * @var callable 48 | */ 49 | private $_slaveHandler = NULL; 50 | 51 | /** 52 | * @var string 53 | */ 54 | private $_prefix = NULL; 55 | 56 | /** 57 | * SimpleFork constructor. 58 | * @param int $limit 59 | * @param string $name 60 | */ 61 | public function __construct($limit = 2, $name = 'SimpleFork', $prefix = NULL) 62 | { 63 | $opt = getopt('m:'); 64 | $this->_name = $name; 65 | $this->_limit = $limit; 66 | $this->_prefix = $prefix; 67 | 68 | if (isset($opt['m']) && $opt['m'] == 'slave') { 69 | $this->_isForked = true; 70 | } 71 | } 72 | 73 | /** 74 | * @param callable $masterHandler 75 | * @return $this 76 | */ 77 | public function master(callable $masterHandler) 78 | { 79 | if (!$this->_isForked) { 80 | $this->_masterHandler = $masterHandler; 81 | $this->createMaster($this->_limit); 82 | } 83 | 84 | return $this; 85 | } 86 | 87 | /** 88 | * @param callable $slaveHandler 89 | * @return $this 90 | */ 91 | public function slave(callable $slaveHandler) 92 | { 93 | if ($this->_isForked) { 94 | $this->_slaveHandler = $slaveHandler; 95 | $this->createSlave(); 96 | } 97 | 98 | return $this; 99 | } 100 | 101 | /** 102 | * submit task 103 | * 104 | * @param $data 105 | * @param callable $cb 106 | */ 107 | public function submit($data = NULL, $cb = NULL) 108 | { 109 | if (!$this->_isForked) { 110 | $process = &$this->getAvailableProcess(); 111 | $process['cb'] = $cb; 112 | $data = json_encode($data); 113 | $length = strlen($data); 114 | $length = str_pad($length . '', 8, ' ', STR_PAD_RIGHT); 115 | 116 | // write head 117 | fwrite($process['pipes'][0], $length . $data); 118 | } 119 | } 120 | 121 | /** 122 | * @param int $sleep 123 | * @return bool 124 | */ 125 | public function loop($sleep = 0) 126 | { 127 | if (!$this->_isForked) { 128 | if ($sleep > 0) { 129 | usleep($sleep * 1000); 130 | } 131 | 132 | $this->check(); 133 | return true; 134 | } 135 | 136 | return false; 137 | } 138 | 139 | /** 140 | * @param int $timeout 141 | */ 142 | public function wait($timeout = 0) 143 | { 144 | $start = microtime(true); 145 | 146 | while (true) { 147 | $this->check(); 148 | $interval = (microtime(true) - $start) * 1000; 149 | 150 | if ($this->_busy == 0) { 151 | return; 152 | } 153 | 154 | // timeout 155 | if ($timeout > 0 && $interval >= $timeout) { 156 | $this->killallBusyProcesses(); 157 | return; 158 | } 159 | 160 | usleep(10000); 161 | } 162 | } 163 | 164 | /** 165 | * @param $str 166 | */ 167 | public function log($str) 168 | { 169 | $args = func_get_args(); 170 | $line = count($args) > 1 ? call_user_func_array('sprintf', $args) : $str; 171 | 172 | $line = date('Y-m-d H:i:s') . ' [' . ($this->_isForked ? 'slave' : 'master') 173 | . ':' . getmypid() . '] ' . $line; 174 | 175 | error_log($line . "\n", 3, $this->_isForked ? 'php://stderr' : 'php://stdout'); 176 | } 177 | 178 | /** 179 | * create master handlers 180 | * @param $limit 181 | */ 182 | private function createMaster($limit) 183 | { 184 | $this->_cmd = $this->getCmd(); 185 | 186 | for ($i = 0; $i < $limit; $i ++) { 187 | $this->_processes[] = $this->createProcess(); 188 | } 189 | 190 | @cli_set_process_title($this->_name . ':' . 'master'); 191 | 192 | if (!empty($this->_masterHandler)) { 193 | call_user_func($this->_masterHandler, $this); 194 | } 195 | } 196 | 197 | /** 198 | * create slave handlers 199 | */ 200 | private function createSlave() 201 | { 202 | @cli_set_process_title($this->_name . ':' . 'slave'); 203 | file_put_contents('php://stdout', str_pad(getmypid(), 5, ' ', STR_PAD_LEFT)); 204 | 205 | while (true) { 206 | $fp = @fopen('php://stdin', 'r'); 207 | $recv = @fread($fp, 8); 208 | $size = intval(rtrim($recv)); 209 | $data = @fread($fp, $size); 210 | @fclose($fp); 211 | 212 | if (!empty($data)) { 213 | if (!empty($this->_slaveHandler)) { 214 | $data = json_decode($data, true); 215 | $resp = call_user_func($this->_slaveHandler, $data, $this); 216 | echo json_encode($resp); 217 | } 218 | } else { 219 | usleep(100000); 220 | } 221 | } 222 | } 223 | 224 | /** 225 | * @return array 226 | */ 227 | private function createProcess() 228 | { 229 | $desc = [ 230 | ['pipe', 'r'], 231 | ['pipe', 'w'], 232 | ['pipe', 'w'] 233 | ]; 234 | 235 | $res = proc_open($this->_cmd, $desc, $pipes, getcwd()); 236 | $pid = ltrim(stream_get_contents($pipes[1], 5)); 237 | 238 | $process = [ 239 | 'res' => $res, 240 | 'pipes' => $pipes, 241 | 'status'=> true, 242 | 'pid' => $pid, 243 | 'cb' => NULL 244 | ]; 245 | 246 | stream_set_blocking($pipes[1], 0); 247 | stream_set_blocking($pipes[2], 0); 248 | 249 | $this->log('start ' . $pid); 250 | 251 | return $process; 252 | } 253 | 254 | 255 | /** 256 | * @return int|string 257 | */ 258 | private function check() 259 | { 260 | $index = -1; 261 | 262 | foreach ($this->_processes as $key => &$process) { 263 | $this->checkProcessAlive($process); 264 | 265 | if (!$process['status']) { 266 | echo stream_get_contents($process['pipes'][2]); 267 | $result = stream_get_contents($process['pipes'][1]); 268 | 269 | if (!empty($result)) { 270 | $process['status'] = true; 271 | $this->_busy --; 272 | 273 | if (!empty($process['cb'])) { 274 | $process['cb'](json_decode($result, true)); 275 | } 276 | } 277 | } 278 | 279 | if ($process['status'] && $index < 0) { 280 | $index = $key; 281 | } 282 | } 283 | 284 | return $index; 285 | } 286 | 287 | /** 288 | * @param $process 289 | */ 290 | private function checkProcessAlive(&$process) 291 | { 292 | $status = proc_get_status($process['res']); 293 | 294 | if (!$status['running']) { 295 | echo stream_get_contents($process['pipes'][2]); 296 | 297 | $this->killProcess($process); 298 | $this->log('close ' . $process['pid']); 299 | 300 | if (!$process['status']) { 301 | $this->_busy --; 302 | } 303 | 304 | $process = $this->createProcess(); 305 | } 306 | } 307 | 308 | /** 309 | * kill process 310 | * 311 | * @param $process 312 | */ 313 | private function killProcess($process) 314 | { 315 | if (function_exists('posix_kill')) { 316 | posix_kill($process['pid'], 9); 317 | } else { 318 | @proc_terminate($process['res']); 319 | } 320 | } 321 | 322 | /** 323 | * kill all 324 | */ 325 | private function killallBusyProcesses() 326 | { 327 | foreach ($this->_processes as &$process) { 328 | if (!$process['status']) { 329 | $this->killProcess($process); 330 | $this->log('close ' . $process['pid']); 331 | $process = $this->createProcess(); 332 | $this->_busy --; 333 | } 334 | } 335 | } 336 | 337 | /** 338 | * @return null 339 | */ 340 | private function &getAvailableProcess() 341 | { 342 | $available = NULL; 343 | 344 | while (true) { 345 | $index = $this->check(); 346 | 347 | if (isset($this->_processes[$index])) { 348 | $this->_processes[$index]['status'] = false; 349 | $this->_busy ++; 350 | return $this->_processes[$index]; 351 | } 352 | 353 | // sleep 50 msec 354 | usleep(50000); 355 | } 356 | } 357 | 358 | /** 359 | * @return string 360 | */ 361 | private function getCmd() 362 | { 363 | $prefix = empty($this->_prefix) ? (isset($_SERVER['_']) ? $_SERVER['_'] : '/usr/bin/env php') : $this->_prefix; 364 | return $prefix . ' ' . $_SERVER['PHP_SELF'] . ' -mslave'; 365 | } 366 | } 367 | --------------------------------------------------------------------------------