├── .gitignore ├── .travis.yml ├── Exceptions └── Exception.php ├── LICENSE.md ├── README.md ├── Tests └── ThreadTest.php ├── Thread.php ├── ThreadPool.php ├── composer.json ├── examples ├── example.php └── speed_test.php └── phpunit.xml.dist /.gitignore: -------------------------------------------------------------------------------- 1 | # Composer and packages 2 | composer.phar 3 | composer.lock 4 | vendor/ 5 | 6 | # PhpUnit 7 | phpunit.xml 8 | code_coverage/ 9 | 10 | # Ignore IDE settings 11 | *.iml 12 | *.ipr 13 | *.iws 14 | .idea/ 15 | 16 | # Ignore temp work dirs and files 17 | _/ 18 | *~ 19 | ~* 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.3.3 5 | - 5.3 6 | - 5.4 7 | - 5.5 8 | 9 | matrix: 10 | allow_failures: 11 | - php: 5.5 12 | 13 | before_script: 14 | - sudo apt-get -qq install libevent-dev 15 | - sh -c " if [ \"\$(php --re libevent | grep 'does not exist')\" != '' ]; then 16 | (yes '' | pecl install libevent-beta) 17 | fi" 18 | - composer install 19 | 20 | script: phpunit --configuration phpunit.xml.dist 21 | -------------------------------------------------------------------------------- /Exceptions/Exception.php: -------------------------------------------------------------------------------- 1 | 12 | * @license MIT 13 | */ 14 | class Exception extends BaseExeption {} 15 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2013 Amal Samally and AzaGroup 4 | https://github.com/Anizoptera/AzaThread 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is furnished 11 | to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AzaThread. We've moved! 2 | ======================= 3 | 4 | Go to the new repo: [Anizoptera/AzaThread](https://github.com/Anizoptera/AzaThread). 5 | 6 | Simple and powerful threads emulation component for PHP (based on forks). 7 | 8 | 9 | License 10 | ------- 11 | 12 | MIT, see [LICENSE.md](LICENSE.md) 13 | 14 | 15 | Links 16 | ----- 17 | 18 | [AzaThread — многопоточность для PHP с блэкджеком](http://habrahabr.ru/blogs/php/134501/) 19 | -------------------------------------------------------------------------------- /Tests/ThreadTest.php: -------------------------------------------------------------------------------- 1 | processThread($debug); 54 | 55 | // Sync thread with big data 56 | $this->processThread($debug, true); 57 | 58 | // Sync thread with childs 59 | $this->processThread($debug, false, true); 60 | 61 | // Sync thread with childs and big data 62 | $this->processThread($debug, true, true); 63 | 64 | // Sync events 65 | $this->processThreadEvent($debug); 66 | 67 | // Sync pool 68 | $this->processPool($debug); 69 | 70 | // Sync pool with big data 71 | $this->processPool($debug, true); 72 | 73 | // Sync pool with childs 74 | $this->processPool($debug, false, true); 75 | 76 | // Sync pool with childs and big data 77 | $this->processPool($debug, true, true); 78 | 79 | // Sync pool events 80 | $this->processPoolEvent($debug); 81 | 82 | 83 | Thread::$useForks = self::$defForks; 84 | } 85 | 86 | /** 87 | * Tests threads in asynchronous mode 88 | */ 89 | public function testAsync() 90 | { 91 | if (!Thread::$useForks) { 92 | $this->markTestSkipped( 93 | 'You need LibEvent, PCNTL and POSIX support' 94 | .' with CLI sapi to fully test Threads' 95 | ); 96 | return; 97 | } 98 | 99 | $ipcModes = array( 100 | Thread::IPC_IGBINARY => 'igbinary_serialize', 101 | Thread::IPC_SERIALIZE => false, 102 | ); 103 | $sockModes = array(true, false); 104 | $defDataMode = Thread::$ipcDataMode; 105 | $defSocketMode = Socket::$useSockets; 106 | 107 | 108 | $debug = false; 109 | Thread::$useForks = true; 110 | 111 | 112 | foreach ($sockModes as $sockMode) { 113 | Socket::$useSockets = $sockMode; 114 | 115 | foreach ($ipcModes as $mode => $check) { 116 | if ($check && !function_exists($check)) { 117 | continue; 118 | } 119 | 120 | Thread::$ipcDataMode = $mode; 121 | 122 | // Async thread 123 | $this->processThread($debug); 124 | 125 | // Async thread with big data 126 | $this->processThread($debug, true); 127 | 128 | // Async thread with childs 129 | $this->processThread($debug, false, true); 130 | 131 | // Async thread with childs and big data 132 | $this->processThread($debug, true, true); 133 | 134 | // Async events 135 | $this->processThreadEvent($debug); 136 | 137 | // Async errorable thread 138 | $this->processThreadErrorable($debug); 139 | 140 | // Async pool 141 | $this->processPool($debug); 142 | 143 | // Async pool with big data 144 | $this->processPool($debug, true); 145 | 146 | // Async pool with childs 147 | $this->processPool($debug, false, true); 148 | 149 | // Async pool with childs and big data 150 | $this->processPool($debug, true, true); 151 | 152 | // Async pool events 153 | $this->processPoolEvent($debug, true); 154 | 155 | // Async errorable pool 156 | $this->processPoolErrorable($debug); 157 | } 158 | } 159 | 160 | 161 | Thread::$useForks = self::$defForks; 162 | Thread::$ipcDataMode = $defDataMode; 163 | Socket::$useSockets = $defSocketMode; 164 | } 165 | 166 | 167 | /** 168 | * Thread 169 | * 170 | * @param bool $debug 171 | * @param bool $bigResult 172 | * @param bool $withChild 173 | */ 174 | function processThread($debug, $bigResult = false, $withChild = false) 175 | { 176 | $num = 10; 177 | 178 | if ($debug) { 179 | echo '-----------------------', PHP_EOL, 180 | "Thread test: ", (Thread::$useForks ? 'Async' : 'Sync'), PHP_EOL, 181 | '-----------------------', PHP_EOL; 182 | } 183 | 184 | /** @var $thread Thread */ 185 | $thread = $withChild 186 | ? new TestThreadWithChilds($debug) 187 | : new TestThreadReturnFirstArgument($debug); 188 | 189 | // You can override preforkWait property 190 | // to TRUE to not wait thread at first time manually 191 | $thread->wait(); 192 | 193 | for ($i = 0; $i < $num; $i++) { 194 | $value = $bigResult ? str_repeat($i, 100000) : $i; 195 | $thread->run($value)->wait(); 196 | $state = $thread->getState(); 197 | $this->assertEquals(Thread::STATE_WAIT, $state); 198 | $sucess = $thread->getSuccess(); 199 | $this->assertTrue($sucess); 200 | $result = $thread->getResult(); 201 | $this->assertEquals($value, $result); 202 | } 203 | 204 | $thread->cleanup(); 205 | } 206 | 207 | /** 208 | * Thread, random errors 209 | * 210 | * @param bool $debug 211 | */ 212 | function processThreadErrorable($debug) 213 | { 214 | $num = 10; 215 | 216 | if ($debug) { 217 | echo '-----------------------', PHP_EOL, 218 | "Thread errorable test: ", (Thread::$useForks ? 'Async' : 'Sync'), PHP_EOL, 219 | '-----------------------', PHP_EOL; 220 | } 221 | 222 | $thread = new TestThreadReturnArgErrors($debug); 223 | 224 | $i = $j = 0; 225 | $value = $i; 226 | 227 | // You can override preforkWait property 228 | // to TRUE to not wait thread at first time manually 229 | $thread->wait(); 230 | 231 | while ($num > $i) { 232 | $j++; 233 | $thread->run($value, $j)->wait(); 234 | $state = $thread->getState(); 235 | $this->assertEquals(Thread::STATE_WAIT, $state); 236 | if ($thread->getSuccess()) { 237 | $result = $thread->getResult(); 238 | $this->assertEquals($value, $result); 239 | $value = ++$i; 240 | } 241 | } 242 | 243 | $this->assertEquals($num*2, $j); 244 | 245 | $thread->cleanup(); 246 | } 247 | 248 | /** 249 | * Thread, events 250 | * 251 | * @param bool $debug 252 | */ 253 | function processThreadEvent($debug) 254 | { 255 | $events = 15; 256 | $num = 3; 257 | 258 | if ($debug) { 259 | echo '-----------------------', PHP_EOL, 260 | "Thread events test: ", (Thread::$useForks ? 'Async' : 'Sync'), PHP_EOL, 261 | '-----------------------', PHP_EOL; 262 | } 263 | 264 | $thread = new TestThreadEvents($debug); 265 | 266 | $test = $this; 267 | $arg = mt_rand(12, 987); 268 | $last = 0; 269 | $cb = function($event, $e_data, $e_arg) use ($arg, $test, &$last) { 270 | /** @var $test TestCase */ 271 | $test->assertEquals($arg, $e_arg); 272 | $test->assertEquals(TestThreadEvents::EV_PROCESS, $event); 273 | $test->assertEquals($last++, $e_data); 274 | }; 275 | $thread->bind(TestThreadEvents::EV_PROCESS, $cb, $arg); 276 | 277 | // You can override preforkWait property 278 | // to TRUE to not wait thread at first time manually 279 | $thread->wait(); 280 | 281 | for ($i = 0; $i < $num; $i++) { 282 | $last = 0; 283 | $thread->run($events)->wait(); 284 | $state = $thread->getState(); 285 | $this->assertEquals(Thread::STATE_WAIT, $state); 286 | $sucess = $thread->getSuccess(); 287 | $this->assertTrue($sucess); 288 | } 289 | 290 | $this->assertEquals($events, $last); 291 | 292 | $thread->cleanup(); 293 | } 294 | 295 | 296 | /** 297 | * Pool 298 | * 299 | * @param bool $debug 300 | * @param bool $bigResult 301 | * @param bool $withChild 302 | * 303 | * @throws Exception 304 | */ 305 | function processPool($debug, $bigResult = false, $withChild = false) 306 | { 307 | $num = 100; 308 | $threads = 4; 309 | 310 | if ($debug) { 311 | echo '-----------------------', PHP_EOL, 312 | "Thread pool test: ", (Thread::$useForks ? 'Async' : 'Sync'), PHP_EOL, 313 | '-----------------------', PHP_EOL; 314 | } 315 | 316 | $thread = $withChild 317 | ? 'TestThreadWithChilds' 318 | : 'TestThreadReturnFirstArgument'; 319 | $thread = __NAMESPACE__ . '\\' . $thread; 320 | 321 | $pool = new ThreadPool($thread, $threads, null, $debug); 322 | 323 | $jobs = array(); 324 | 325 | $i = 0; 326 | $left = $num; 327 | $maxI = ceil($num * 1.5); 328 | $worked = array(); 329 | do { 330 | while ($pool->hasWaiting() && $left > 0) { 331 | $arg = mt_rand(100, 2000); 332 | if ($bigResult) { 333 | $arg = str_repeat($arg, 10000); 334 | } 335 | if (!$threadId = $pool->run($arg)) { 336 | throw new Exception('Pool slots error'); 337 | } 338 | $this->assertTrue(!isset($jobs[$threadId])); 339 | $jobs[$threadId] = $arg; 340 | $worked[$threadId] = true; 341 | $left--; 342 | } 343 | if ($results = $pool->wait()) { 344 | foreach ($results as $threadId => $res) { 345 | $num--; 346 | $this->assertTrue(isset($jobs[$threadId])); 347 | $this->assertEquals($jobs[$threadId], $res); 348 | unset($jobs[$threadId]); 349 | } 350 | } 351 | $i++; 352 | } while ($num > 0 && $i < $maxI); 353 | 354 | $this->assertEquals(0, $num); 355 | 356 | $this->assertEquals( 357 | $pool->threadsCount, count($worked), 358 | 'Worked threads count is not equals to real threads count' 359 | ); 360 | 361 | $pool->cleanup(); 362 | $this->assertEquals(0, $pool->threadsCount); 363 | $this->assertEmpty($pool->threads); 364 | $this->assertEmpty($pool->waiting); 365 | $this->assertEmpty($pool->working); 366 | $this->assertEmpty($pool->initializing); 367 | $this->assertEmpty($pool->failed); 368 | $this->assertEmpty($pool->results); 369 | } 370 | 371 | /** 372 | * Pool, events 373 | * 374 | * @param bool $debug 375 | * @param bool $async 376 | * 377 | * @throws Exception 378 | */ 379 | function processPoolEvent($debug, $async = false) 380 | { 381 | $events = 5; 382 | $num = 15; 383 | $threads = 3; 384 | 385 | if ($debug) { 386 | echo '-----------------------', PHP_EOL, 387 | "Thread pool events test: ", (Thread::$useForks ? 'Async' : 'Sync'), PHP_EOL, 388 | '-----------------------', PHP_EOL; 389 | } 390 | 391 | $pool = new ThreadPool( 392 | __NAMESPACE__ . '\TestThreadEvents', 393 | $threads, null, $debug 394 | ); 395 | 396 | $test = $this; 397 | $arg = mt_rand(12, 987); 398 | $jobs = $worked = array(); 399 | $cb = function($event, $threadId, $e_data, $e_arg) use ($arg, $test, &$jobs, &$async) { 400 | /** @var $test TestCase */ 401 | $test->assertEquals($arg, $e_arg); 402 | $test->assertEquals(TestThreadEvents::EV_PROCESS, $event); 403 | if ($async) { 404 | $test->assertTrue(isset($jobs[$threadId])); 405 | $test->assertEquals($jobs[$threadId]++, $e_data); 406 | } else { 407 | if (!isset($jobs[$threadId])) { 408 | $jobs[$threadId] = 0; 409 | } 410 | $test->assertEquals($jobs[$threadId]++, $e_data); 411 | } 412 | }; 413 | $pool->bind(TestThreadEvents::EV_PROCESS, $cb, $arg); 414 | 415 | 416 | $i = 0; 417 | $left = $num; 418 | $maxI = ceil($num * 1.5); 419 | do { 420 | while ($pool->hasWaiting() && $left > 0) { 421 | if (!$threadId = $pool->run($events)) { 422 | throw new Exception('Pool slots error'); 423 | } 424 | if ($async) { 425 | $this->assertTrue(!isset($jobs[$threadId])); 426 | $jobs[$threadId] = 0; 427 | } 428 | $worked[$threadId] = true; 429 | $left--; 430 | } 431 | if ($results = $pool->wait()) { 432 | foreach ($results as $threadId => $res) { 433 | $num--; 434 | $this->assertTrue(isset($jobs[$threadId])); 435 | $this->assertEquals($events, $jobs[$threadId]); 436 | unset($jobs[$threadId]); 437 | } 438 | } 439 | $i++; 440 | } while ($num > 0 && $i < $maxI); 441 | 442 | $this->assertEquals(0, $num); 443 | 444 | $this->assertEquals( 445 | $pool->threadsCount, count($worked), 446 | 'Worked threads count is not equals to real threads count' 447 | ); 448 | 449 | $pool->cleanup(); 450 | $this->assertEquals(0, $pool->threadsCount); 451 | $this->assertEmpty($pool->threads); 452 | $this->assertEmpty($pool->waiting); 453 | $this->assertEmpty($pool->working); 454 | $this->assertEmpty($pool->initializing); 455 | $this->assertEmpty($pool->failed); 456 | $this->assertEmpty($pool->results); 457 | } 458 | 459 | /** 460 | * Pool, errors 461 | * 462 | * @param bool $debug 463 | * 464 | * @throws Exception 465 | */ 466 | function processPoolErrorable($debug) 467 | { 468 | $num = 10; 469 | $threads = 2; 470 | 471 | if ($debug) { 472 | echo '-----------------------', PHP_EOL, 473 | "Errorable thread pool test: ", (Thread::$useForks ? 'Async' : 'Sync'), PHP_EOL, 474 | '-----------------------', PHP_EOL; 475 | } 476 | 477 | $pool = new ThreadPool( 478 | __NAMESPACE__ . '\TestThreadReturnArgErrors', 479 | $threads, null, $debug 480 | ); 481 | 482 | $jobs = $worked = array(); 483 | 484 | $i = 0; 485 | $j = 0; 486 | $left = $num; 487 | $maxI = ceil($num * 2.5); 488 | do { 489 | while ($pool->hasWaiting() && $left > 0) { 490 | $arg = mt_rand(1000000, 20000000); 491 | if (!$threadId = $pool->run($arg, $j)) { 492 | throw new Exception('Pool slots error'); 493 | } 494 | $this->assertTrue(!isset($jobs[$threadId])); 495 | $jobs[$threadId] = $arg; 496 | $worked[$threadId] = true; 497 | $left--; 498 | $j++; 499 | } 500 | if ($results = $pool->wait($failed)) { 501 | foreach ($results as $threadId => $res) { 502 | $num--; 503 | $this->assertTrue(isset($jobs[$threadId])); 504 | $this->assertEquals($jobs[$threadId], $res); 505 | unset($jobs[$threadId]); 506 | } 507 | } 508 | if ($failed) { 509 | foreach ($failed as $threadId) { 510 | $this->assertTrue(isset($jobs[$threadId])); 511 | unset($jobs[$threadId]); 512 | $left++; 513 | } 514 | } 515 | $i++; 516 | } while ($num > 0 && $i < $maxI); 517 | 518 | $this->assertEquals(0, $num); 519 | 520 | $this->assertEquals( 521 | $pool->threadsCount, count($worked), 522 | 'Worked threads count is not equals to real threads count' 523 | ); 524 | 525 | $pool->cleanup(); 526 | $this->assertEquals(0, $pool->threadsCount); 527 | $this->assertEmpty($pool->threads); 528 | $this->assertEmpty($pool->waiting); 529 | $this->assertEmpty($pool->working); 530 | $this->assertEmpty($pool->initializing); 531 | $this->assertEmpty($pool->failed); 532 | $this->assertEmpty($pool->results); 533 | } 534 | } 535 | 536 | 537 | 538 | /** 539 | * Test thread 540 | */ 541 | class TestThreadReturnFirstArgument extends Thread 542 | { 543 | /** 544 | * Main processing. 545 | * 546 | * @return mixed 547 | */ 548 | protected function process() 549 | { 550 | return $this->getParam(0); 551 | } 552 | } 553 | 554 | /** 555 | * Test thread 556 | */ 557 | class TestThreadReturnArgErrors extends Thread 558 | { 559 | /** 560 | * Main processing. 561 | * 562 | * @return mixed 563 | */ 564 | protected function process() 565 | { 566 | if (1 & (int)$this->getParam(1)) { 567 | // Emulate terminating 568 | posix_kill($this->pid, SIGKILL); 569 | exit; 570 | } 571 | return $this->getParam(0); 572 | } 573 | } 574 | 575 | /** 576 | * Test thread 577 | */ 578 | class TestThreadNothing extends Thread 579 | { 580 | /** 581 | * Main processing. 582 | * 583 | * @return mixed 584 | */ 585 | protected function process() 586 | { 587 | } 588 | } 589 | 590 | /** 591 | * Test thread 592 | */ 593 | class TestThreadWithChilds extends Thread 594 | { 595 | 596 | 597 | /** 598 | * Main processing. 599 | * 600 | * @return mixed 601 | */ 602 | protected function process() 603 | { 604 | $res = `echo 1`; 605 | return $this->getParam(0); 606 | } 607 | } 608 | 609 | /** 610 | * Test thread 611 | */ 612 | class TestThreadReturn extends Thread 613 | { 614 | /** 615 | * Main processing. 616 | * 617 | * @return mixed 618 | */ 619 | protected function process() 620 | { 621 | return array(123456789, 'abcdefghigklmnopqrstuvwxyz', 123.7456328); 622 | } 623 | } 624 | 625 | /** 626 | * Test thread 627 | */ 628 | class TestThreadWork extends Thread 629 | { 630 | /** 631 | * Main processing. 632 | * 633 | * @return mixed 634 | */ 635 | protected function process() 636 | { 637 | $r = null; 638 | $i = 1000; 639 | while ($i--) { 640 | $r = mt_rand(0, PHP_INT_MAX) * mt_rand(0, PHP_INT_MAX); 641 | } 642 | return $r; 643 | } 644 | } 645 | 646 | /** 647 | * Test thread 648 | */ 649 | class TestThreadEvents extends Thread 650 | { 651 | const EV_PROCESS = 'process'; 652 | 653 | 654 | /** 655 | * Main processing. 656 | * 657 | * @return mixed 658 | */ 659 | protected function process() 660 | { 661 | $events = $this->getParam(0); 662 | for ($i = 0; $i < $events; $i++) { 663 | $this->trigger(self::EV_PROCESS, $i); 664 | } 665 | } 666 | } 667 | -------------------------------------------------------------------------------- /Thread.php: -------------------------------------------------------------------------------- 1 | 30 | * @license MIT 31 | */ 32 | abstract class Thread 33 | { 34 | #region Constants 35 | 36 | // Thread states 37 | const STATE_TERM = 1; 38 | const STATE_INIT = 2; 39 | const STATE_WAIT = 3; 40 | const STATE_WORK = 4; 41 | 42 | // Types of IPC packets 43 | const P_STATE = 0x01; 44 | const P_JOB = 0x02; 45 | const P_EVENT = 0x04; 46 | const P_DATA = 0x08; 47 | const P_SERIAL = 0x10; 48 | 49 | // Types of IPC data transfer modes 50 | const IPC_IGBINARY = 1; // Igbinary serialization (8th, 6625 jps) 51 | const IPC_SERIALIZE = 2; // Native PHP serialization (8th, 6501 jps) 52 | 53 | // Timer names 54 | const TIMER_BASE = 'thread:base:'; 55 | const TIMER_WAIT = 'thread:wait:'; 56 | 57 | // Debug prefixes 58 | const D_INIT = 'INIT: '; 59 | const D_WARN = 'WARN: '; 60 | const D_INFO = 'INFO: '; 61 | const D_IPC = 'IPC: '; // IPC 62 | const D_CLEAN = 'CLEAN: '; // Cleanup 63 | 64 | #endregion 65 | 66 | 67 | #region Public static settings 68 | 69 | /** 70 | * IPC Data transfer mode (see self::IPC_*) 71 | */ 72 | public static $ipcDataMode = self::IPC_SERIALIZE; 73 | 74 | /** 75 | * Whether threads will use forks 76 | */ 77 | public static $useForks = false; 78 | 79 | #endregion 80 | 81 | 82 | #region Internal static properties 83 | 84 | /** 85 | * All started threads count 86 | * 87 | * @var int 88 | */ 89 | private static $threadsCount = 0; 90 | 91 | /** 92 | * All threads 93 | * 94 | * @var Thread[] 95 | */ 96 | private static $threads = array(); 97 | 98 | /** 99 | * Threads by types 100 | */ 101 | private static $threadsByClasses = array(); 102 | 103 | /** 104 | * Threads marks by child PIDs (PID => thread id) 105 | * 106 | * @var int[] 107 | */ 108 | private static $threadsByPids = array(); 109 | 110 | /** 111 | * Array of waiting threads (id => id) 112 | * 113 | * @var int[] 114 | */ 115 | private static $waitingThreads = array(); 116 | 117 | /** 118 | * Signal events 119 | * 120 | * @var Event[] 121 | */ 122 | private static $eventsSignals = array(); 123 | 124 | /** 125 | * Event base 126 | * 127 | * @var EventBase 128 | */ 129 | private static $base; 130 | 131 | #endregion 132 | 133 | 134 | #region Internal properties 135 | 136 | /** 137 | * Pipes pair (master, worker) 138 | * 139 | * @var ASocket[] 140 | */ 141 | private $pipes; 142 | 143 | /** 144 | * Master event 145 | * 146 | * @var EventBuffer 147 | */ 148 | private $masterEvent; 149 | 150 | /** 151 | * Master read buffer 152 | */ 153 | private $masterBuffer = ''; 154 | 155 | /** 156 | * Currently receiving packet in child 157 | * 158 | * @var array|null 159 | */ 160 | private $masterPacket; 161 | 162 | /** 163 | * Child event 164 | * 165 | * @var EventBuffer 166 | */ 167 | private $childEvent; 168 | 169 | /** 170 | * Child read buffer 171 | */ 172 | private $childBuffer = ''; 173 | 174 | /** 175 | * Currently receiving packet in child 176 | * 177 | * @var array|null 178 | */ 179 | private $childPacket; 180 | 181 | /** 182 | * Timer events names 183 | * 184 | * @var string[] 185 | */ 186 | private $eventsTimers = array(); 187 | 188 | /** 189 | * Event listeners 190 | */ 191 | private $listeners = array(); 192 | 193 | /** 194 | * Thread process name (if not empty) 195 | * 196 | * @var bool|string 197 | */ 198 | private $processName = false; 199 | 200 | /** 201 | * Thread state 202 | * 203 | * @var int 204 | */ 205 | private $state; 206 | 207 | /** 208 | * Whether waiting loop is enabled 209 | */ 210 | private $waiting = false; 211 | 212 | /** 213 | * Whether event locking is enabled 214 | */ 215 | private $eventLocking = false; 216 | 217 | /** 218 | * Cleaning flag 219 | */ 220 | private $cleaning = false; 221 | 222 | #endregion 223 | 224 | 225 | #region Internal protected properties 226 | 227 | /** 228 | * Owner thread pool 229 | * 230 | * @var ThreadPool 231 | */ 232 | protected $pool; 233 | 234 | /** 235 | * Internal thread id 236 | * 237 | * @var int 238 | */ 239 | protected $id; 240 | 241 | /** 242 | * Current process pid 243 | * 244 | * @var int 245 | */ 246 | protected $pid; 247 | 248 | /** 249 | * Parent process pid 250 | * 251 | * @var int 252 | */ 253 | protected $parent_pid; 254 | 255 | /** 256 | * Child process pid 257 | * 258 | * @var int 259 | */ 260 | protected $child_pid; 261 | 262 | /** 263 | * Whether child is already forked 264 | */ 265 | protected $isForked = false; 266 | 267 | /** 268 | * Whether current process is child 269 | */ 270 | protected $isChild = false; 271 | 272 | /** 273 | * Arguments for the processing 274 | */ 275 | protected $params = array(); 276 | 277 | /** 278 | * Last processing result 279 | */ 280 | protected $result; 281 | 282 | /** 283 | * The status of the success of the task 284 | */ 285 | protected $success = false; 286 | 287 | #endregion 288 | 289 | 290 | #region Settings. Overwrite on child class to set. 291 | 292 | /** 293 | * File for shared memory key generation. 294 | * Thread class file by default. 295 | */ 296 | protected $file; 297 | 298 | /** 299 | * Whether the thread will wait for next tasks 300 | */ 301 | protected $multitask = true; 302 | 303 | /** 304 | * Whether to listen for signals in master. 305 | * SIGCHLD is always listened. 306 | */ 307 | protected $listenMasterSignals = true; 308 | 309 | /** 310 | * Perform pre-fork, to avoid wasting resources later 311 | */ 312 | protected $prefork = true; 313 | 314 | /** 315 | * Wait for the preforking child 316 | */ 317 | protected $preforkWait = false; 318 | 319 | /** 320 | * Worker initialization timeout (in seconds). 321 | * Set it to less than one, to disable. 322 | */ 323 | protected $timeoutMasterInitWait = 3; 324 | 325 | /** 326 | * Maximum master timeout to wait for the job results (in seconds). 327 | * Set it to less than one, to disable. 328 | */ 329 | protected $timeoutMasterResultWait = 5; 330 | 331 | /** 332 | * Maximum worker job waiting timeout. After it spawned child will die. 333 | * Set it to less than one, to disable. 334 | */ 335 | protected $timeoutWorkerJobWait = -1; 336 | 337 | /** 338 | * Worker interval for master checks (in seconds). 339 | */ 340 | protected $intervalWorkerChecks = 15; 341 | 342 | /** 343 | * Maximum worker pipe read size in bytes. 344 | * 128kb by default 345 | */ 346 | protected $childReadSize = 0x20000; 347 | 348 | /** 349 | * Maximum master pipe read size in bytes. 350 | * 128kb by default 351 | */ 352 | protected $masterReadSize = 0x20000; 353 | 354 | /** 355 | * Whether to show debugging information 356 | */ 357 | public $debug = false; 358 | 359 | #endregion 360 | 361 | 362 | 363 | /** 364 | * Initializes base parameters 365 | * 366 | * @throw Exception if can't wait for the preforked thread 367 | * 368 | * @param bool $debug Whether to show debugging information 369 | * @param string $pName Thread worker process name 370 | * @param ThreadPool $pool Thread pool 371 | */ 372 | public function __construct($debug = false, $pName = null, $pool = null) 373 | { 374 | $this->id = $id = ++self::$threadsCount; 375 | $class = get_called_class(); 376 | 377 | self::$threadsByClasses[$class][$id] = 378 | self::$threads[$id] = $this; 379 | 380 | $debug && $this->debug = true; 381 | $pool && $this->pool = $pool; 382 | $pName && $this->processName = $pName; 383 | 384 | $this->pid = 385 | $this->parent_pid = 386 | $this->child_pid = posix_getpid(); 387 | 388 | $this->setState(self::STATE_INIT); 389 | 390 | $forks = self::$useForks; 391 | 392 | if ($debug = $this->debug) { 393 | $message = 'Thread of type "'.get_called_class().'" created.'; 394 | $this->debug(self::D_INIT . $message); 395 | if (!$forks) { 396 | $debug && $this->debug( 397 | self::D_WARN . 'Sync mode (you need Forks and LibEvent support' 398 | .' and CLI sapi to use threads asynchronously)' 399 | ); 400 | } 401 | } 402 | 403 | // Forks preparing 404 | $base = self::$base; 405 | if ($forks) { 406 | // Init shared master event base 407 | if (!$base) { 408 | self::$base = $base = Base::getEventBase(); 409 | $debug && $this->debug( 410 | self::D_INIT . 'Master event base initialized' 411 | ); 412 | } 413 | 414 | // Master signals 415 | if (!self::$eventsSignals) { 416 | if ($this->listenMasterSignals) { 417 | $this->registerEventSignals(); 418 | } else { 419 | $signo = SIGCHLD; 420 | $e = new Event(); 421 | $e->setSignal($signo, array(get_called_class(), '_mEvCbSignal')) 422 | ->setBase($base) 423 | ->add(); 424 | self::$eventsSignals[$signo] = $e; 425 | $debug && $this->debug( 426 | self::D_INIT . 'Master SIGCHLD event signal handler initialized' 427 | ); 428 | } 429 | } 430 | 431 | // Master timer 432 | $timer_name = self::TIMER_BASE . $this->id; 433 | $base->timerAdd( 434 | $timer_name, 0, 435 | array($this, '_mEvCbTimer'), 436 | null, false 437 | ); 438 | $this->eventsTimers[] = $timer_name; 439 | $debug && $this->debug( 440 | self::D_INIT . "Master timer ($timer_name) added" 441 | ); 442 | } 443 | 444 | // On load hook 445 | $this->onLoad(); 446 | 447 | // Preforking 448 | if ($forks && $this->prefork) { 449 | $debug && $this->debug(self::D_INFO . 'Preforking'); 450 | if ($this->forkThread()) { 451 | // Parent 452 | if (($interval = $this->timeoutMasterInitWait) > 0) { 453 | $timer_name = self::TIMER_BASE . $this->id; 454 | $base->timerStart( 455 | $timer_name, 456 | $interval, 457 | self::STATE_INIT 458 | ); 459 | $debug && $this->debug( 460 | self::D_INFO . "Master timer ($timer_name) started for INIT ($interval sec)" 461 | ); 462 | } 463 | $this->preforkWait && $this->wait(); 464 | } else { 465 | // Child 466 | $this->evWorkerLoop(true); 467 | $debug && $this->debug( 468 | self::D_INFO . 'Preforking: end of loop, exiting' 469 | ); 470 | $this->shutdown(); 471 | } 472 | } else { 473 | $this->setState(self::STATE_WAIT); 474 | } 475 | 476 | // On fork hook 477 | $forks || $this->onFork(); 478 | } 479 | 480 | 481 | /** 482 | * Destruction 483 | */ 484 | public function __destruct() 485 | { 486 | $this->debug(self::D_INFO . 'Destructor'); 487 | $this->cleanup(); 488 | } 489 | 490 | /** 491 | * Thread cleanup 492 | */ 493 | public function cleanup() 494 | { 495 | if ($this->cleaning) { 496 | return; 497 | } 498 | $this->cleaning = true; 499 | $this->state = self::STATE_TERM; 500 | 501 | ($debug = $this->debug) && $this->debug( 502 | self::D_CLEAN . 'Cleanup' 503 | ); 504 | 505 | $id = $this->id; 506 | $class = get_called_class(); 507 | $isMaster = !$this->isChild; 508 | 509 | // Threads pool 510 | if ($pool = &$this->pool) { 511 | unset( 512 | $pool->threads[$id], 513 | $pool->waiting[$id], 514 | $pool->working[$id], 515 | $pool->initializing[$id] 516 | ); 517 | $pool->threadsCount--; 518 | $this->pool = null; 519 | } 520 | 521 | // Child process 522 | $base = self::$base; 523 | if ($isMaster && $this->isForked) { 524 | $this->stopWorker(true); 525 | $debug && $this->debug( 526 | self::D_CLEAN . 'Worker process terminated' 527 | ); 528 | 529 | // Check for non-triggered events 530 | $base && $base->resource && $base->loop(EVLOOP_NONBLOCK); 531 | } 532 | 533 | // Threads storage 534 | unset(self::$threads[$id]); 535 | 536 | // Events 537 | if ($base && $base->resource) { 538 | foreach ($this->eventsTimers as $t) { 539 | $base->timerDelete($t); 540 | } 541 | } 542 | if (!$isMaster || !self::$threads) { 543 | foreach (self::$eventsSignals as $ev) { 544 | $ev->free(); 545 | } 546 | self::$eventsSignals = array(); 547 | } 548 | $this->eventsTimers = array(); 549 | $debug && $this->debug( 550 | self::D_CLEAN . 'All events freed' 551 | ); 552 | 553 | // Master event 554 | if ($this->masterEvent) { 555 | $this->masterEvent->free(); 556 | $this->masterEvent = null; 557 | $debug && $this->debug( 558 | self::D_CLEAN . 'Master event freed' 559 | ); 560 | } 561 | 562 | // Child event 563 | if ($this->childEvent) { 564 | $this->childEvent->free(); 565 | $this->childEvent = null; 566 | $debug && $this->debug( 567 | self::D_CLEAN . 'Child event freed' 568 | ); 569 | } 570 | 571 | // Pipes 572 | if ($this->pipes) { 573 | $this->pipes[1]->close(); 574 | // It's already closed after forking 575 | $isMaster && $this->pipes[0]->close(); 576 | $this->pipes = null; 577 | $debug && $this->debug( 578 | self::D_CLEAN . 'Pipes destructed' 579 | ); 580 | } 581 | 582 | // Last master thread cleanup 583 | if ($isMaster && !self::$threads) { 584 | self::$base = null; 585 | } 586 | 587 | // Child cleanup 588 | else { 589 | // Event base cleanup 590 | self::$base = null; 591 | Base::cleanEventBase(); 592 | } 593 | 594 | // Threads storage 595 | unset(self::$threadsByClasses[$class][$id]); 596 | if (empty(self::$threadsByClasses[$class])) { 597 | unset(self::$threadsByClasses[$class]); 598 | } 599 | } 600 | 601 | 602 | /** 603 | * Thread forking 604 | * 605 | * @throws Exception 606 | * 607 | * @return bool TRUE in parent, FALSE in child 608 | */ 609 | private function forkThread() 610 | { 611 | // Checks 612 | if (!self::$useForks) { 613 | throw new Exception( 614 | "Can't fork thread. Forks are not supported." 615 | ); 616 | } else if ($this->isForked) { 617 | throw new Exception( 618 | "Can't fork thread. It is already forked." 619 | ); 620 | } 621 | 622 | // Worker pipes 623 | $debug = $this->debug; 624 | if (!$this->pipes) { 625 | $this->pipes = Socket::pair(); 626 | $debug && $this->debug( 627 | self::D_INIT . 'Pipes initialized' 628 | ); 629 | } 630 | 631 | // Forking 632 | $debug && $this->debug( 633 | self::D_INIT . 'Forking' 634 | ); 635 | $this->isForked = true; 636 | $pid = Base::fork(); 637 | 638 | // In parent 639 | if ($pid) { 640 | self::$threadsByPids[$pid] = $this->id; 641 | $this->child_pid = $pid; 642 | $debug && $this->debug( 643 | self::D_INIT . "Forked: parent ({$this->pid})" 644 | ); 645 | 646 | // Master event 647 | if (!$this->masterEvent) { 648 | $this->masterEvent = $ev = new EventBuffer( 649 | $this->pipes[0]->resource, 650 | array($this, '_mEvCbRead'), 651 | null, 652 | function(){} 653 | ); 654 | $ev->setBase(self::$base)->setPriority()->enable(EV_READ); 655 | $debug && $this->debug( 656 | self::D_INIT . 'Master event initialized' 657 | ); 658 | } 659 | 660 | return true; 661 | } 662 | 663 | // In child 664 | $this->isChild = true; 665 | $this->pid = 666 | $this->child_pid = 667 | $pid = posix_getpid(); 668 | $debug && $this->debug( 669 | self::D_INIT . "Forked: child ($pid)" 670 | ); 671 | 672 | // Closing master pipe 673 | // It is not needed in the child 674 | $this->pipes[0]->close(); 675 | unset($this->pipes[0]); 676 | 677 | // Cleanup parent events 678 | $this->eventsTimers = self::$eventsSignals = array(); 679 | 680 | // Child event 681 | if (!$this->childEvent) { 682 | $this->childEvent = $ev = new EventBuffer( 683 | $this->pipes[1]->resource, 684 | array($this, '_wEvCbRead'), 685 | null, 686 | function(){} 687 | ); 688 | $ev->setBase(self::$base)->setPriority()->enable(EV_READ); 689 | $debug && $this->debug( 690 | self::D_INIT . 'Worker event initialized' 691 | ); 692 | } 693 | 694 | // Process name 695 | if ($name = $this->processName) { 696 | $name .= ' (aza-php): worker'; 697 | Base::setProcessTitle($name); 698 | $debug && $this->debug( 699 | self::D_INIT . "Child process name is changed to: $name" 700 | ); 701 | } 702 | 703 | return false; 704 | } 705 | 706 | /** 707 | * Starts processing 708 | * 709 | * @return Thread 710 | * 711 | * @throws Exception 712 | */ 713 | public function run() 714 | { 715 | if (!$this->isWaiting()) { 716 | throw new Exception( 717 | "Can't run thread. It is not in waiting state." 718 | ." You need to use 'wait' method on thread instance" 719 | ." after each run and before first run if 'preforkWait'" 720 | ." property is not overrided to TRUE and you don't use pool." 721 | ); 722 | } 723 | 724 | ($debug = $this->debug) && $this->debug(self::D_INFO . 'Job start'); 725 | $this->setState(self::STATE_WORK); 726 | $this->result = null; 727 | $this->success = false; 728 | 729 | $args = func_get_args(); 730 | 731 | // Emulating thread with fork 732 | if (self::$useForks) { 733 | // Thread is alive 734 | if ($this->isAlive()) { 735 | $debug && $this->debug( 736 | self::D_INFO . "Child is already running ({$this->child_pid})" 737 | ); 738 | $this->sendPacketToChild(self::P_JOB, $args ?: null); 739 | $this->startMasterWorkTimeout(); 740 | } 741 | // Forking 742 | else { 743 | if ($this->forkThread()) { 744 | // Parent 745 | $this->startMasterWorkTimeout(); 746 | } else { 747 | // Child 748 | $this->setParams($args); 749 | $res = $this->process(); 750 | $this->setResult($res); 751 | $this->multitask && $this->evWorkerLoop(); 752 | $debug && $this->debug( 753 | self::D_INFO . 'Simple end of work, exiting' 754 | ); 755 | $this->shutdown(); 756 | } 757 | } 758 | } 759 | // Forkless compatibility 760 | else { 761 | $this->setParams($args); 762 | $res = $this->process(); 763 | $this->setResult($res); 764 | $debug && $this->debug( 765 | self::D_INFO . 'Sync job ended' 766 | ); 767 | } 768 | 769 | return $this; 770 | } 771 | 772 | /** 773 | * Prepares and starts worker event loop 774 | * 775 | * @param bool $setState Set waiting state 776 | * 777 | * @throws Exception 778 | */ 779 | private function evWorkerLoop($setState = false) 780 | { 781 | ($debug = $this->debug) && $this->debug( 782 | self::D_INIT . "Preparing worker loop" 783 | ); 784 | 785 | if (!$this->isChild) { 786 | throw new Exception( 787 | 'Can\'t start child loop in parent' 788 | ); 789 | } 790 | 791 | $this->registerEventSignals(); 792 | 793 | $base = self::$base; 794 | 795 | // Worker timer to check master process 796 | $timer = self::TIMER_BASE; 797 | $timerCb = array($this, '_wEvCbTimer'); 798 | ($timeout = $this->intervalWorkerChecks) > 0 || $timeout = 15; 799 | $base->timerAdd($timer, $timeout, $timerCb); 800 | $this->eventsTimers[] = $timer; 801 | $debug && $this->debug( 802 | self::D_INIT . "Worker timer ($timer) event initialized " 803 | ."and set to $timeout" 804 | ); 805 | 806 | // Worker wait timer 807 | if (0 < $timeout = $this->timeoutWorkerJobWait) { 808 | $timer = self::TIMER_WAIT; 809 | $base->timerAdd($timer, $timeout, $timerCb); 810 | $this->eventsTimers[] = $timer; 811 | $debug && $this->debug( 812 | self::D_INIT . "Worker wait timer ($timer) event " 813 | ."initialized and set to $timeout" 814 | ); 815 | } 816 | 817 | $setState && $this->setState(self::STATE_WAIT); 818 | 819 | $debug && $this->debug(self::D_INFO . 'Loop (worker) start'); 820 | $base->loop(); 821 | $debug && $this->debug(self::D_INFO . 'Loop (worker) end'); 822 | } 823 | 824 | 825 | 826 | #region Methods for overriding! 827 | 828 | /** 829 | * Hook called after the thread initialization, 830 | * but before forking! 831 | */ 832 | protected function onLoad() {} 833 | 834 | /** 835 | * Hook called after the thread forking (in child process) 836 | */ 837 | protected function onFork() {} 838 | 839 | /** 840 | * Main processing. 841 | * 842 | * Use {@link getParam} method to get processing parameters 843 | * 844 | * @return mixed returned result will be available via 845 | * {@link getResult} in the master process 846 | */ 847 | abstract protected function process(); 848 | 849 | #endregion 850 | 851 | 852 | 853 | #region Master waiting 854 | 855 | /** 856 | * Waits until the thread becomes waiting 857 | * 858 | * @throws Exception 859 | * 860 | * @return Thread 861 | */ 862 | public function wait() 863 | { 864 | if (self::$useForks && !$this->isWaiting()) { 865 | $this->debug( 866 | self::D_INFO . 'Loop (master waiting) start' 867 | ); 868 | $this->waiting = true; 869 | self::evMasterLoop(); 870 | if (!$this->isWaiting()) { 871 | throw new Exception( 872 | 'Could not wait for the thread' 873 | ); 874 | } 875 | } 876 | return $this; 877 | } 878 | 879 | /** 880 | * Waits until one of specified threads becomes waiting 881 | * 882 | * @param int|int[] $threadIds 883 | */ 884 | public static function waitThreads($threadIds) 885 | { 886 | if (self::$useForks && $threadIds) { 887 | self::stDebug( 888 | self::D_INFO . 'Loop (master theads waiting) start' 889 | ); 890 | $threadIds = (array)$threadIds; 891 | $threadIds = array_combine($threadIds, $threadIds); 892 | self::$waitingThreads = $threadIds; 893 | self::evMasterLoop(); 894 | self::$waitingThreads = array(); 895 | } 896 | } 897 | 898 | /** 899 | * Starts master event loop 900 | */ 901 | private static function evMasterLoop() 902 | { 903 | if (!$base = self::$base) { 904 | throw new Exception( 905 | "Can't start loop (master). EventBase is cleaned" 906 | ); 907 | } 908 | 909 | ($debug = self::stGetDebug($thread)) && self::stDebug( 910 | self::D_INFO . 'Loop (master) start', 911 | $thread 912 | ); 913 | 914 | $base->loop(); 915 | 916 | $debug && self::stDebug( 917 | self::D_INFO . 'Loop (master) end', 918 | $thread 919 | ); 920 | } 921 | 922 | /** 923 | * Starts master work timeout 924 | */ 925 | private function startMasterWorkTimeout() 926 | { 927 | if (0 < $interval = $this->timeoutMasterResultWait) { 928 | $timer_name = self::TIMER_BASE . $this->id; 929 | self::$base->timerStart( 930 | $timer_name, $interval, self::STATE_WORK 931 | ); 932 | $this->debug( 933 | self::D_INFO . "Master timer ($timer_name) " 934 | ."started for WORK ($interval sec)" 935 | ); 936 | } 937 | } 938 | 939 | #endregion 940 | 941 | 942 | 943 | #region Event system 944 | 945 | /** 946 | * Connects a listener to a given event. 947 | * 948 | * @see trigger 949 | * 950 | * @param string $event

951 | * An event name. 952 | *

953 | * @param callback $listener

954 | * Callback to be called when the matching event occurs. 955 | *
function(string $event_name, 956 | * mixed $event_data, mixed $event_arg){} 957 | *

958 | * @param mixed $arg

959 | * Additional argument for callback. 960 | *

961 | */ 962 | public function bind($event, $listener, $arg = null) 963 | { 964 | // Bind is allowed only in parent 965 | if (!$this->isChild) { 966 | if (!isset($this->listeners[$event])) { 967 | $this->listeners[$event] = array(); 968 | } 969 | $this->listeners[$event][] = array($listener, $arg); 970 | $this->debug( 971 | self::D_INFO . "New listener binded on event [$event]" 972 | ); 973 | } 974 | } 975 | 976 | /** 977 | * Notifies all listeners of a given event. 978 | * 979 | * @see bind 980 | * 981 | * @param string $event An event name 982 | * @param mixed $data Event data for callback 983 | */ 984 | public function trigger($event, $data = null) 985 | { 986 | ($debug = $this->debug) && $this->debug( 987 | self::D_INFO . "Triggering event [$event]" 988 | ); 989 | 990 | // Child 991 | if ($this->isChild) { 992 | $this->sendPacketToParent( 993 | self::P_EVENT, $event, $data 994 | ); 995 | if (isset($data) && $this->eventLocking) { 996 | $debug && $this->debug( 997 | self::D_INFO . "Locking thread - waiting " 998 | ."for event read confirmation" 999 | ); 1000 | $this->waiting = true; 1001 | } 1002 | } 1003 | // Parent 1004 | else { 1005 | if (!empty($this->listeners[$event])) { 1006 | /** @var $cb callback */ 1007 | foreach ($this->listeners[$event] as $l) { 1008 | list($cb, $arg) = $l; 1009 | if ($cb instanceof \Closure) { 1010 | $cb($event, $data, $arg); 1011 | } else { 1012 | call_user_func( 1013 | $cb, $event, $data, $arg 1014 | ); 1015 | } 1016 | } 1017 | } 1018 | if ($pool = $this->pool) { 1019 | $pool->trigger($event, $this->id, $data); 1020 | } 1021 | } 1022 | } 1023 | 1024 | #endregion 1025 | 1026 | 1027 | 1028 | #region Getters/Setters 1029 | 1030 | /** 1031 | * Returns unique thread id 1032 | * 1033 | * @return int 1034 | */ 1035 | public function getId() 1036 | { 1037 | return $this->id; 1038 | } 1039 | 1040 | /** 1041 | * Returns success of the processing. 1042 | * Processing is not successful if thread 1043 | * dies when worked or working timeout exceeded. 1044 | * 1045 | * @return bool 1046 | */ 1047 | public function getSuccess() 1048 | { 1049 | return $this->success; 1050 | } 1051 | 1052 | /** 1053 | * Returns result 1054 | * 1055 | * @return mixed 1056 | */ 1057 | public function getResult() 1058 | { 1059 | return $this->result; 1060 | } 1061 | 1062 | /** 1063 | * Returns thread state 1064 | * 1065 | * @return int 1066 | */ 1067 | public function getState() 1068 | { 1069 | return $this->state; 1070 | } 1071 | 1072 | /** 1073 | * Returns thread state name 1074 | * 1075 | * @param int $state [optional]

1076 | * Integer state value. Current state will be used instead. 1077 | *

1078 | * 1079 | * @return string 1080 | */ 1081 | public function getStateName($state = null) 1082 | { 1083 | isset($state) || $state = $this->state; 1084 | return self::STATE_WAIT === $state 1085 | ? 'WAIT' 1086 | : (self::STATE_WORK === $state 1087 | ? 'WORK' 1088 | : (self::STATE_INIT === $state 1089 | ? 'INIT' 1090 | : (self::STATE_TERM === $state 1091 | ? 'TERM' 1092 | : 'UNKNOWN'))); 1093 | } 1094 | 1095 | /** 1096 | * Returns processing parameter 1097 | * 1098 | * @param int $index Parameter index 1099 | * @param mixed $default Default value if parameter isn't set 1100 | * 1101 | * @return mixed 1102 | */ 1103 | protected function getParam($index, $default = null) 1104 | { 1105 | return isset($this->params[$index]) 1106 | ? $this->params[$index] 1107 | : $default; 1108 | } 1109 | 1110 | /** 1111 | * Returns result 1112 | * 1113 | * @return mixed 1114 | */ 1115 | private function isWaiting() 1116 | { 1117 | return $this->state === self::STATE_WAIT; 1118 | } 1119 | 1120 | /** 1121 | * Checks if the child process is alive 1122 | * 1123 | * @return bool TRUE if child is alive FALSE otherwise 1124 | */ 1125 | private function isAlive() 1126 | { 1127 | return !self::$useForks 1128 | || $this->isForked 1129 | && 0 === pcntl_waitpid( 1130 | $this->child_pid, $s, WNOHANG 1131 | ); 1132 | } 1133 | 1134 | 1135 | /** 1136 | * Sets params for processing 1137 | * 1138 | * @param array $args 1139 | */ 1140 | private function setParams($args) 1141 | { 1142 | $this->params = $args && is_array($args) 1143 | ? $args 1144 | : array(); 1145 | 1146 | if ($this->debug) { 1147 | $msg = $this->isChild 1148 | ? 'Async processing' 1149 | : 'Processing'; 1150 | if ($args && is_array($args)) { 1151 | $msg .= ' with args'; 1152 | } 1153 | $this->debug(self::D_INFO . $msg); 1154 | } 1155 | } 1156 | 1157 | /** 1158 | * Sets processing result 1159 | * 1160 | * @param mixed $res 1161 | */ 1162 | private function setResult($res = null) 1163 | { 1164 | $this->debug(self::D_INFO . 'Setting result'); 1165 | 1166 | // Send result packet to parent 1167 | if ($this->isChild) { 1168 | $this->sendPacketToParent(self::P_JOB, null, $res); 1169 | } 1170 | 1171 | // Change result 1172 | else { 1173 | if ($pool = $this->pool) { 1174 | $pool->results[ $this->id ] = $res; 1175 | } 1176 | $this->result = $res; 1177 | $this->success = true; 1178 | $this->setState(self::STATE_WAIT); 1179 | } 1180 | } 1181 | 1182 | /** 1183 | * Sets thread state 1184 | * 1185 | * @param int $state One of self::STATE_* 1186 | */ 1187 | private function setState($state) 1188 | { 1189 | $state = (int)$state; 1190 | 1191 | if ($debug = $this->debug) { 1192 | $this->debug( 1193 | self::D_INFO . 'Changing state to: "' 1194 | . $this->getStateName($state) . "\" ($state)" 1195 | ); 1196 | } 1197 | 1198 | // Send state packet to parent 1199 | if ($this->isChild) { 1200 | $this->sendPacketToParent(self::P_STATE, $state); 1201 | } 1202 | 1203 | // Change state 1204 | else { 1205 | $this->state = $state; 1206 | $wait = (self::STATE_WAIT === $state); 1207 | $threadId = $this->id; 1208 | 1209 | // Pool processing 1210 | if ($pool = $this->pool) { 1211 | // Waiting 1212 | if ($wait) { 1213 | if (!$this->success && empty($pool->initializing[$threadId]) 1214 | && isset(self::$waitingThreads[$threadId]) 1215 | ) { 1216 | $pool->failed[$threadId] = $threadId; 1217 | } 1218 | unset( 1219 | $pool->working[$threadId], 1220 | $pool->initializing[$threadId] 1221 | ); 1222 | $pool->waiting[$threadId] = $threadId; 1223 | } 1224 | // Other states 1225 | else { 1226 | unset($pool->waiting[$threadId]); 1227 | if ($state & self::STATE_WORK) { 1228 | $pool->working[$threadId] = $threadId; 1229 | unset($pool->initializing[$threadId]); 1230 | } else if ($state & self::STATE_INIT) { 1231 | $pool->initializing[$threadId] = $threadId; 1232 | unset($pool->working[$threadId]); 1233 | } 1234 | } 1235 | } 1236 | 1237 | // Waiting 1238 | if ($wait && self::$useForks) { 1239 | $base = self::$base; 1240 | $timer_name = self::TIMER_BASE . $threadId; 1241 | $base->timerStop($timer_name); 1242 | $debug && $this->debug( 1243 | self::D_INFO . "Master timer ($timer_name) stopped" 1244 | ); 1245 | 1246 | // One waiting thread 1247 | if ($this->waiting) { 1248 | $this->waiting = false; 1249 | $debug && $this->debug( 1250 | self::D_INFO . 'Loop (master waiting) end' 1251 | ); 1252 | $base->loopExit(); 1253 | } 1254 | 1255 | // Several waiting threads 1256 | else if (isset(self::$waitingThreads[$threadId])) { 1257 | $debug && $this->debug( 1258 | self::D_INFO . 'Loop (master theads waiting) end' 1259 | ); 1260 | $base->loopExit(); 1261 | } 1262 | } 1263 | } 1264 | } 1265 | 1266 | #endregion 1267 | 1268 | 1269 | 1270 | #region Pipe events callbacks 1271 | 1272 | /** 1273 | * Worker read event callback 1274 | * 1275 | * @see evWorkerLoop 1276 | * @see EventBuffer::setCallback 1277 | * 1278 | * @access private 1279 | * 1280 | * @throws Exception 1281 | * 1282 | * @param resource $buf Buffered event 1283 | * @param array $args 1284 | */ 1285 | public function _wEvCbRead($buf, $args) 1286 | { 1287 | ($debug = $this->debug) && $this->debug( 1288 | self::D_INFO . 'Worker pipe read event' 1289 | ); 1290 | 1291 | // Receive packets 1292 | $packets = $this->readPackets( 1293 | $args[0], 1294 | $this->childReadSize, 1295 | $this->childBuffer, 1296 | $this->childPacket 1297 | ); 1298 | if (!$packets) { 1299 | return; 1300 | } 1301 | 1302 | // Restart waiting timeout 1303 | $base = self::$base; 1304 | if ($base->timerExists($timer = self::TIMER_WAIT)) { 1305 | $base->timerStart($timer); 1306 | } 1307 | 1308 | foreach ($packets as $p) { 1309 | $packet = $p['packet']; 1310 | $data = $p['data']; 1311 | 1312 | $debug && $this->debug(self::D_IPC . " => Packet: [$packet]"); 1313 | 1314 | // Job packet 1315 | if ($packet & self::P_JOB) { 1316 | if ($packet & self::P_DATA) { 1317 | $data = $this->peekPacketData($data, $packet); 1318 | $debug && $this->debug( 1319 | self::D_IPC . ' => Packet: job with arguments' 1320 | ); 1321 | } else { 1322 | $debug && $this->debug( 1323 | self::D_IPC . ' => Packet: job' 1324 | ); 1325 | $data = array(); 1326 | } 1327 | $this->setParams($data); 1328 | $this->setResult($this->process()); 1329 | } 1330 | 1331 | // Event read confirmation 1332 | else if ($packet & self::P_EVENT) { 1333 | $debug && $this->debug( 1334 | self::D_IPC . ' => Packet: event read confirmation. Unlocking thread' 1335 | ); 1336 | $this->waiting = false; 1337 | } 1338 | 1339 | // Unknown packet 1340 | else { 1341 | $base->loopBreak(); 1342 | throw new Exception("Unknown packet [$packet]"); 1343 | } 1344 | } 1345 | } 1346 | 1347 | /** 1348 | * Worker timer event callback 1349 | * 1350 | * @see evWorkerLoop 1351 | * @see EventBase::timerAdd 1352 | * 1353 | * @access private 1354 | * 1355 | * @param string $name 1356 | */ 1357 | public function _wEvCbTimer($name) 1358 | { 1359 | $die = false; 1360 | 1361 | // Worker wait 1362 | if ($name === self::TIMER_WAIT) { 1363 | $this->debug( 1364 | self::D_WARN . 'Timeout (worker waiting) exceeded, exiting' 1365 | ); 1366 | $die = true; 1367 | } 1368 | // Worker check 1369 | else if (!Base::getProcessIsAlive($this->parent_pid)) { 1370 | $this->debug( 1371 | self::D_WARN . 'Parent is dead, exiting' 1372 | ); 1373 | $die = true; 1374 | } 1375 | 1376 | if ($die) { 1377 | self::$base->loopBreak(); 1378 | $this->shutdown(); 1379 | } 1380 | } 1381 | 1382 | 1383 | /** 1384 | * Master read event callback 1385 | * 1386 | * @see __construct 1387 | * @see EventBuffer::setCallback 1388 | * 1389 | * @access private 1390 | * 1391 | * @param resource $buf Buffered event 1392 | * @param array $args 1393 | * 1394 | * @throws Exception 1395 | */ 1396 | public function _mEvCbRead($buf, $args) 1397 | { 1398 | ($debug = $this->debug) && $this->debug( 1399 | self::D_INFO . 'Master pipe read event' 1400 | ); 1401 | 1402 | $packets = $this->readPackets( 1403 | $args[0], 1404 | $this->masterReadSize, 1405 | $this->masterBuffer, 1406 | $this->masterPacket 1407 | ); 1408 | if (!$packets) { 1409 | return; 1410 | } 1411 | 1412 | foreach ($packets as $p) { 1413 | $threadId = $this->id; 1414 | $packet = $p['packet']; 1415 | $value = $p['value']; 1416 | $data = $p['data']; 1417 | 1418 | $debug && $this->debug( 1419 | self::D_IPC . " <= Packet: [$packet]" 1420 | ); 1421 | 1422 | if (!isset(self::$threads[$threadId])) { 1423 | self::$base->loopBreak(); 1424 | throw new Exception( 1425 | "Packet [$packet:$value] for unknown thread #$threadId" 1426 | ); 1427 | } 1428 | 1429 | $thread = self::$threads[$threadId]; 1430 | 1431 | // Packet data 1432 | if (self::P_DATA & $packet) { 1433 | $data = $this->peekPacketData($data, $packet); 1434 | $debug && $this->debug( 1435 | self::D_IPC . ' <= Packet: data received' 1436 | ); 1437 | } else { 1438 | $data = null; 1439 | } 1440 | 1441 | // State packet 1442 | if (self::P_STATE & $packet) { 1443 | $debug && $this->debug( 1444 | self::D_IPC . ' <= Packet: state' 1445 | ); 1446 | $thread->setState($value); 1447 | } 1448 | 1449 | // Event packet 1450 | else if (self::P_EVENT & $packet) { 1451 | $debug && $this->debug( 1452 | self::D_IPC . ' <= Packet: event' 1453 | ); 1454 | if ($thread->eventLocking) { 1455 | $debug && $this->debug( 1456 | self::D_IPC . " => Sending event read confirmation" 1457 | ); 1458 | $thread->sendPacketToChild(self::P_EVENT); 1459 | } 1460 | $thread->trigger($value, $data); 1461 | } 1462 | 1463 | // Job packet 1464 | else if (self::P_JOB & $packet) { 1465 | $debug && $this->debug( 1466 | self::D_IPC . ' <= Packet: job ended' 1467 | ); 1468 | $thread->setResult($data); 1469 | } 1470 | 1471 | // Unknown packet 1472 | else { 1473 | self::$base->loopBreak(); 1474 | throw new Exception( 1475 | "Unknown packet [$packet]" 1476 | ); 1477 | } 1478 | } 1479 | } 1480 | 1481 | /** 1482 | * Master timer event callback 1483 | * 1484 | * @see __construct 1485 | * @see EventBase::timerAdd 1486 | * 1487 | * @access private 1488 | * 1489 | * @param string $name Timer name 1490 | * @param mixed $arg Additional timer argument 1491 | * @param int $iteration Iteration number 1492 | * 1493 | * @return bool 1494 | * 1495 | * @throws Exception 1496 | */ 1497 | public function _mEvCbTimer($name, $arg, $iteration) 1498 | { 1499 | $this->debug( 1500 | self::D_WARN . "Master timeout exceeded ({$name} - {$arg})" 1501 | ); 1502 | 1503 | $killed = $this->stopWorker(); 1504 | 1505 | $arg = (int)$arg; 1506 | if (self::STATE_WORK & $arg) { 1507 | if ($killed) { 1508 | throw new Exception( 1509 | "Exceeded timeout: thread work " 1510 | ."({$this->timeoutMasterResultWait} sec.)" 1511 | ); 1512 | } 1513 | } else if (self::STATE_INIT & $arg) { 1514 | throw new Exception( 1515 | "Exceeded timeout: thread initialization " 1516 | ."({$this->timeoutMasterInitWait} sec.)" 1517 | ); 1518 | } else { 1519 | throw new Exception( 1520 | "Unknown timeout ({$name} ({$iteration}) - {$arg})" 1521 | ); 1522 | } 1523 | } 1524 | 1525 | #endregion 1526 | 1527 | 1528 | 1529 | #region Working with data packets 1530 | 1531 | /** 1532 | * Sends packet to parent 1533 | * 1534 | * @param int $packet Integer packet type (see self::P_* constants) 1535 | * @param string $value Packet value (without ":" character) 1536 | * @param mixed $data Mixed packet data 1537 | * 1538 | * @throws Exception 1539 | */ 1540 | private function sendPacketToParent($packet, $value = '', $data = null) 1541 | { 1542 | $debug = $this->debug; 1543 | 1544 | // Waiting for read confirmation 1545 | if ($this->waiting) { 1546 | if ($debug) { 1547 | $this->debug( 1548 | self::D_INFO . "Thread is locked. Waiting for read confirmation" 1549 | ); 1550 | $this->debug( 1551 | self::D_INFO . 'Loop (worker) start once' 1552 | ); 1553 | } 1554 | self::$base->loop(EVLOOP_ONCE); 1555 | $debug && $this->debug( 1556 | self::D_INFO . 'Loop (worker) end once' 1557 | ); 1558 | if ($this->waiting) { 1559 | $error = "Can't send packet to parent. Child is " 1560 | ."waiting for event read confirmation."; 1561 | $debug && $this->debug(self::D_WARN . $error); 1562 | self::$base->loopBreak(); 1563 | throw new Exception($error); 1564 | } 1565 | } 1566 | 1567 | $this->childEvent->write( 1568 | $this->preparePacket( 1569 | $packet, $data, $value, false 1570 | ) 1571 | ); 1572 | } 1573 | 1574 | /** 1575 | * Sends packet to child 1576 | * 1577 | * @param int $packet Integer packet type (see self::P_* constants) 1578 | * @param mixed $data Mixed packet data 1579 | */ 1580 | private function sendPacketToChild($packet, $data = null) 1581 | { 1582 | $this->masterEvent->write( 1583 | $this->preparePacket($packet, $data) 1584 | ); 1585 | } 1586 | 1587 | 1588 | /** 1589 | * Reads packets from pipe with buffered event 1590 | * 1591 | * @throws Exception 1592 | * 1593 | * @param EventBuffer $e

1594 | * Buffered event 1595 | *

1596 | * @param int $maxReadSize

1597 | * Data size to read at once in bytes. 1598 | *

1599 | * @param string $buffer

1600 | * Read buffer. 1601 | *

1602 | * @param null|array $curPacket

1603 | * Read buffer. 1604 | *

1605 | * 1606 | * @return string[] Array of packets 1607 | */ 1608 | private function readPackets($e, $maxReadSize, &$buffer, &$curPacket) 1609 | { 1610 | $debug = $this->debug; 1611 | if (!$curPacket && '' != $buffer) { 1612 | $error = "Unexpected read buffer"; 1613 | $debug && $this->debug(self::D_WARN . $error); 1614 | self::$base->loopBreak(); 1615 | throw new Exception($error); 1616 | } 1617 | 1618 | $buf = ''; 1619 | while ('' != $str = $e->read($maxReadSize)) { 1620 | $buf .= $str; 1621 | } 1622 | if ('' === $buf) { 1623 | return array(); 1624 | } 1625 | $debug && $this->debug( 1626 | self::D_IPC . " Read ".strlen($buf)."b; " 1627 | . strlen($buffer)."b in buffer" 1628 | ); 1629 | $buffer .= $buf; 1630 | unset($str, $buf); 1631 | 1632 | 1633 | $packets = array(); 1634 | do { 1635 | if (!$curPacket) { 1636 | if ("\x80" !== $buffer[0]) { 1637 | $error = "Packet must start with 0x80 character"; 1638 | } else if (strlen($buffer) < 7) { 1639 | $error = "Packet must contain at least 7 characters"; 1640 | } 1641 | if (!empty($error)) { 1642 | $debug && $this->debug(self::D_WARN . $error); 1643 | self::$base->loopBreak(); 1644 | throw new Exception($error); 1645 | } 1646 | $curPacket = unpack( 1647 | 'Cpacket/CvalueLength/NdataLength', 1648 | substr($buffer, 1, 6) 1649 | ); 1650 | $curPacket['value'] = 1651 | $curPacket['data'] = ''; 1652 | $buffer = substr($buffer, 7); 1653 | $debug && $this->debug( 1654 | self::D_IPC . " Packet started [{$curPacket['packet']}]; " 1655 | .($curPacket['dataLength']-$curPacket['valueLength'])."b data;" 1656 | ." {$curPacket['valueLength']}b value" 1657 | ); 1658 | } else { 1659 | $debug && $this->debug( 1660 | self::D_IPC . " Packet continue [{$curPacket['packet']}];" 1661 | ." {$curPacket['dataLength']}b data;" 1662 | ." {$curPacket['valueLength']}b value;" 1663 | ." ".strlen($buffer)."b read" 1664 | ); 1665 | } 1666 | 1667 | if ($dataLen = $curPacket['dataLength']) { 1668 | if (strlen($buffer) < $dataLen) { 1669 | return $packets; 1670 | } 1671 | if ($valLen = $curPacket['valueLength']) { 1672 | $curPacket['value'] = substr($buffer, 0, $valLen); 1673 | } 1674 | $_dataLen = $dataLen; 1675 | if ($dataLen -= $valLen) { 1676 | $curPacket['data'] = substr($buffer, $valLen, $dataLen); 1677 | } 1678 | $buffer = substr($buffer, $_dataLen); 1679 | } else { 1680 | $valLen = 0; 1681 | } 1682 | 1683 | // Debugging 1684 | if ($debug) { 1685 | $rDataLen = strlen($curPacket['data']); 1686 | $rValLen = strlen($curPacket['value']); 1687 | if ($dataLen != $rDataLen) { 1688 | $error = "Packet data length header ({$dataLen})" 1689 | ." does not match the actual length of the data" 1690 | ." ({$rDataLen})"; 1691 | } else if ($valLen != $rValLen) { 1692 | $error = "Packet value length header ({$valLen})" 1693 | ." does not match the actual length of the value" 1694 | ." ({$rValLen})"; 1695 | } 1696 | if (!empty($error)) { 1697 | $this->debug(self::D_WARN . $error); 1698 | self::$base->loopBreak(); 1699 | throw new Exception($error); 1700 | } 1701 | $this->debug( 1702 | self::D_IPC . " Packet completed [{$curPacket['packet']}];" 1703 | ." {$dataLen}b data; {$valLen}b value;" 1704 | ." ".strlen($buffer)."b left in buffer" 1705 | ); 1706 | } 1707 | 1708 | $packets[] = $curPacket; 1709 | $curPacket = null; 1710 | 1711 | } while($buffer); 1712 | 1713 | $debug && $this->debug( 1714 | self::D_IPC . ' Packets received: ' . count($packets) 1715 | ); 1716 | 1717 | return $packets; 1718 | } 1719 | 1720 | /** 1721 | * Peeks packet data 1722 | * 1723 | * @throws Exception 1724 | * 1725 | * @param mixed $data Raw data 1726 | * @param int $packet Packet type 1727 | * 1728 | * @return string 1729 | */ 1730 | private function peekPacketData($data, $packet) 1731 | { 1732 | $mode = self::$ipcDataMode; 1733 | 1734 | // Serialization 1735 | if (($igbinary = self::IPC_IGBINARY === $mode) 1736 | || self::IPC_SERIALIZE === $mode 1737 | ) { 1738 | if (self::P_SERIAL & $packet) { 1739 | // Igbinary/PHP unserialize 1740 | $data = $igbinary 1741 | ? igbinary_unserialize($data) 1742 | : unserialize($data); 1743 | } 1744 | } else { 1745 | $error = "Unknown IPC mode ($mode)."; 1746 | $this->debug(self::D_WARN . $error); 1747 | self::$base->loopBreak(); 1748 | throw new Exception($error); 1749 | } 1750 | 1751 | return $data; 1752 | } 1753 | 1754 | /** 1755 | * Prepares IPC packet 1756 | * 1757 | * @throws Exception 1758 | * 1759 | * @param int $packet Integer packet type (see self::P_* constants) 1760 | * @param string $data Packet data 1761 | * @param string $value Packet value 1762 | * @param bool $toChild Packet is for child 1763 | * 1764 | * @return string 1765 | */ 1766 | private function preparePacket($packet, $data, $value = '', $toChild = true) 1767 | { 1768 | // Prepare data 1769 | $postfix = ''; 1770 | if (isset($data)) { 1771 | $packet |= self::P_DATA; 1772 | $mode = self::$ipcDataMode; 1773 | $postfix = ' (with data)'; 1774 | 1775 | if (($igbinary = self::IPC_IGBINARY === $mode) 1776 | || self::IPC_SERIALIZE === $mode 1777 | ) { 1778 | if (is_scalar($data)) { 1779 | $data = is_bool($data) ? (string)(int)$data : (string)$data; 1780 | } else { 1781 | $packet |= self::P_SERIAL; 1782 | // Igbinary/PHP serialize 1783 | $data = $igbinary 1784 | ? igbinary_serialize($data) 1785 | : serialize($data); 1786 | } 1787 | } else { 1788 | $error = "Unknown IPC mode ($mode)."; 1789 | $this->debug(self::D_WARN . $error); 1790 | self::$base->loopBreak(); 1791 | throw new Exception($error); 1792 | } 1793 | } else { 1794 | $data = ''; 1795 | } 1796 | 1797 | // Check value 1798 | if (0xFF < $valLength = strlen($value = (string)$value)) { 1799 | $error = "Packet value is too long ($valLength). " 1800 | ."Maximum length is 255 characters."; 1801 | $this->debug(self::D_WARN . $error); 1802 | self::$base->loopBreak(); 1803 | throw new Exception($error); 1804 | } 1805 | 1806 | // Build packet 1807 | $dataLen = strlen($data); 1808 | $_packet = "\x80" 1809 | . pack('CCN', $packet, $valLength, $dataLen+$valLength) 1810 | . $value 1811 | . $data; 1812 | 1813 | // Debugging 1814 | if ($this->debug) { 1815 | if ($toChild) { 1816 | $arr = '=>'; 1817 | $n = 'child'; 1818 | } else { 1819 | $arr = '<='; 1820 | $n = 'parent'; 1821 | } 1822 | $this->debug( 1823 | self::D_IPC . " {$arr} Sending packet{$postfix} to {$n} [{$packet}]; " 1824 | . strlen($_packet) ."b length with {$dataLen}b" 1825 | ." in data and {$valLength}b in value" 1826 | ); 1827 | } 1828 | 1829 | return $_packet; 1830 | } 1831 | 1832 | #endregion 1833 | 1834 | 1835 | 1836 | #region Signals handling 1837 | 1838 | /** 1839 | * Sends signal to parent 1840 | * 1841 | * @param int $signo Signal's number 1842 | */ 1843 | protected function sendSignalToParent($signo = SIGUSR1) 1844 | { 1845 | $this->_sendSignal($signo, $this->parent_pid); 1846 | } 1847 | 1848 | /** 1849 | * Sends signal to child 1850 | * 1851 | * @param int $signo Signal's number 1852 | */ 1853 | protected function sendSignalToChild($signo = SIGUSR1) 1854 | { 1855 | $this->_sendSignal($signo, $this->child_pid); 1856 | } 1857 | 1858 | /** 1859 | * Sends signal to child 1860 | * 1861 | * @see sendSignalToParent 1862 | * @see sendSignalToChild 1863 | * 1864 | * @param int $signo Signal's number 1865 | * @param int $pid Target process pid 1866 | */ 1867 | private function _sendSignal($signo, $pid) 1868 | { 1869 | if ($this->debug) { 1870 | $name = Base::signalName($signo); 1871 | if ($pid === $this->child_pid) { 1872 | $arrow = '=>'; 1873 | $n = 'child'; 1874 | } else { 1875 | $arrow = '<='; 1876 | $n = 'parent'; 1877 | } 1878 | $this->debug( 1879 | self::D_IPC . " $arrow Sending signal to the " 1880 | ."$n - $name ($signo) ($this->pid => $pid)" 1881 | ); 1882 | } 1883 | posix_kill($pid, $signo); 1884 | } 1885 | 1886 | 1887 | /** 1888 | * Register signals. 1889 | */ 1890 | private function registerEventSignals() 1891 | { 1892 | if (self::$eventsSignals) { 1893 | throw new Exception( 1894 | 'Signal events are already registered' 1895 | ); 1896 | } 1897 | $base = self::$base; 1898 | $i = 0; 1899 | $cb = $this->isChild 1900 | ? array($this, '_evCbSignal') 1901 | : array(get_called_class(), '_mEvCbSignal'); 1902 | foreach (Base::$signals as $signo => $name) { 1903 | if ($signo === SIGKILL || $signo === SIGSTOP) { 1904 | continue; 1905 | } 1906 | $ev = new Event(); 1907 | $ev->setSignal($signo, $cb) 1908 | ->setBase($base) 1909 | ->add(); 1910 | self::$eventsSignals[$signo] = $ev; 1911 | $i++; 1912 | } 1913 | $this->debug( 1914 | self::D_INIT . "Signals event handlers registered ($i)" 1915 | ); 1916 | } 1917 | 1918 | 1919 | /** 1920 | * Called when a signal caught through libevent. 1921 | * 1922 | * @access private 1923 | * 1924 | * @param null $fd 1925 | * @param int $events 1926 | * @param array $arg 1927 | */ 1928 | public function _evCbSignal($fd, $events, $arg) 1929 | { 1930 | $this->signalHandler($arg[2]); 1931 | } 1932 | 1933 | /** 1934 | * Called when the signal is caught 1935 | * 1936 | * @param int $signo Signal's number 1937 | */ 1938 | private function signalHandler($signo) 1939 | { 1940 | // Settings 1941 | $name = Base::signalName($signo, $found); 1942 | if ($debug = $this->debug) { 1943 | $prefix = $this->isChild ? '=>' : '<='; 1944 | $this->debug( 1945 | self::D_IPC . " {$prefix} Caught $name ($signo) signal" 1946 | ); 1947 | } 1948 | 1949 | // Handler 1950 | if (method_exists($this, $name)) { 1951 | $this->$name($signo); 1952 | } 1953 | // Skipped signals: 1954 | // SIGCHLD - Child processes terminates or stops 1955 | // SIGWINCH - Window size change 1956 | // SIGINFO - Information request 1957 | else if (SIGCHLD === $signo || SIGWINCH === $signo || 28 === $signo) { 1958 | return; 1959 | } 1960 | // Default action - shutdown 1961 | else { 1962 | $debug && $this->debug( 1963 | self::D_INFO . 'Unhandled signal, exiting' 1964 | ); 1965 | self::$base->loopBreak(); 1966 | $this->shutdown(); 1967 | } 1968 | } 1969 | 1970 | 1971 | /** 1972 | * Called when a signal caught in master through libevent. 1973 | * 1974 | * @access private 1975 | * 1976 | * @param null $fd 1977 | * @param int $events 1978 | * @param array $arg 1979 | */ 1980 | public static function _mEvCbSignal($fd, $events, $arg) 1981 | { 1982 | self::mSignalHandler($arg[2]); 1983 | } 1984 | 1985 | /** 1986 | * Called when the signal is caught in master 1987 | * 1988 | * @param int $signo Signal's number 1989 | */ 1990 | private static function mSignalHandler($signo) 1991 | { 1992 | // Settings 1993 | $name = Base::signalName($signo); 1994 | if ($debug = self::stGetDebug($thread)) { 1995 | $prefix = $thread->isChild ? '=>' : '<='; 1996 | self::stDebug( 1997 | self::D_IPC . " {$prefix} Caught $name ($signo) signal", 1998 | $thread 1999 | ); 2000 | } 2001 | $name = "m{$name}"; 2002 | $class = get_called_class(); 2003 | 2004 | // Handler 2005 | if ($exists = method_exists($class, $name)) { 2006 | $class::$name($signo); 2007 | } 2008 | // Skipped signals: 2009 | // SIGWINCH - Window size change 2010 | // SIGINFO - Information request 2011 | else if (SIGWINCH === $signo || 28 === $signo) { 2012 | return; 2013 | } 2014 | // Default action - shutdown 2015 | else { 2016 | $debug && self::stDebug( 2017 | self::D_INFO . 'Unhandled signal, exiting', 2018 | $thread 2019 | ); 2020 | self::$base->loopBreak(); 2021 | exit; 2022 | } 2023 | } 2024 | 2025 | 2026 | /** 2027 | * Master SIGCHLD handler - Child processes terminates or stops. 2028 | */ 2029 | protected static function mSigChld() 2030 | { 2031 | $debug = self::stGetDebug($thread); 2032 | while (0 < $pid = pcntl_waitpid(-1, $status, WNOHANG|WUNTRACED)) { 2033 | $debug && self::stDebug( 2034 | self::D_INFO . "SIGCHLD is for pid #{$pid}", 2035 | $thread 2036 | ); 2037 | if ($pid > 0 && isset(self::$threadsByPids[$pid])) { 2038 | if (isset(self::$threads[$threadId = self::$threadsByPids[$pid]])) { 2039 | $thread = self::$threads[$threadId]; 2040 | $debug && self::stDebug( 2041 | self::D_INFO . "SIGCHLD is for thread #{$threadId}", 2042 | $thread 2043 | ); 2044 | $thread->cleaning || $thread->stopWorker(); 2045 | } 2046 | } 2047 | } 2048 | } 2049 | 2050 | #endregion 2051 | 2052 | 2053 | 2054 | #region Shutdown 2055 | 2056 | 2057 | /** 2058 | * Attempts to stop the thread worker process 2059 | * 2060 | * @param bool $wait 2061 | * @param int $signo - SIGINT|SIGTSTP|SIGTERM|SIGSTOP|SIGKILL 2062 | * 2063 | * @return bool TRUE on success and FALSE otherwise 2064 | */ 2065 | protected function stopWorker($wait = false, $signo = SIGTERM) 2066 | { 2067 | $res = false; 2068 | if ($this->isForked) { 2069 | if ($this->isAlive()) { 2070 | if ($debug = $this->debug) { 2071 | $do = ($signo == SIGSTOP || $signo == SIGKILL) ? 'Kill' : 'Stop'; 2072 | $this->debug( 2073 | self::D_INFO . "$do worker" 2074 | ); 2075 | } 2076 | $this->sendSignalToChild($signo); 2077 | if ($wait) { 2078 | $debug && $this->debug( 2079 | self::D_INFO . 'Waiting for the child' 2080 | ); 2081 | $pid = $this->child_pid; 2082 | if ($signo == SIGSTOP) { 2083 | $i = 15; 2084 | usleep(1000); 2085 | do { 2086 | $st = pcntl_waitpid( 2087 | $pid, $status, WNOHANG|WUNTRACED 2088 | ); 2089 | if ($st) { 2090 | break; 2091 | } 2092 | usleep(100000); 2093 | } while (--$i > 0); 2094 | if (!$st) { 2095 | return $this->stopWorker( 2096 | true, SIGKILL 2097 | ); 2098 | } 2099 | } else { 2100 | pcntl_waitpid( 2101 | $pid, $status, WUNTRACED 2102 | ); 2103 | } 2104 | } 2105 | $res = true; 2106 | } 2107 | $this->isForked = false; 2108 | } 2109 | if (!$this->cleaning) { 2110 | $this->setState(self::STATE_WAIT); 2111 | } 2112 | return $res; 2113 | } 2114 | 2115 | /** 2116 | * Attempts to kill the thread worker process 2117 | * 2118 | * @param bool $wait 2119 | * 2120 | * @return bool TRUE on success and FALSE otherwise 2121 | */ 2122 | protected function killWorker($wait = false) 2123 | { 2124 | return $this->stopWorker($wait, SIGKILL); 2125 | } 2126 | 2127 | 2128 | /** 2129 | * Shutdowns the child process properly. 2130 | * Override if you need custom shutdown logic. 2131 | */ 2132 | protected function shutdown() 2133 | { 2134 | if ($this->isChild) { 2135 | $this->debug(self::D_INFO . 'Child exit'); 2136 | $this->cleanup(); 2137 | 2138 | class_exists('Aza\Kernel\Core', false) 2139 | && Core::stopApplication(true); 2140 | 2141 | exit; 2142 | } 2143 | } 2144 | 2145 | #endregion 2146 | 2147 | 2148 | 2149 | #region Debug 2150 | 2151 | /** 2152 | * Debug logging 2153 | * 2154 | * @param string $message 2155 | */ 2156 | protected function debug($message) 2157 | { 2158 | $this->debug && self::stDebug($message, $this); 2159 | } 2160 | 2161 | /** 2162 | * Static debug logging 2163 | * 2164 | * @param string $message 2165 | * @param self $thread 2166 | */ 2167 | protected static function stDebug($message, $thread = null) 2168 | { 2169 | if ($thread) { 2170 | $id = $thread->id; 2171 | } else if (self::stGetDebug($thread)) { 2172 | $id = '-'; 2173 | } else { 2174 | return; 2175 | } 2176 | 2177 | $time = Base::getTimeForLog(); 2178 | if ($thread) { 2179 | $role = $thread->isChild ? 'W' : '-'; // Master|Worker 2180 | $pid = $thread->pid; 2181 | } else { 2182 | // Unknown (called in destructor or something similar) 2183 | $role = $pid = '~'; 2184 | } 2185 | $message = "{$time} [debug] [T{$id}.{$role}] " 2186 | ."#{$pid}: {$message}"; 2187 | 2188 | if (class_exists('Aza\Kernel\Core', false) && $app = Core::$app) { 2189 | $app->msg($message, Logger::LVL_DEBUG); 2190 | } else { 2191 | echo strip_tags($message), PHP_EOL; 2192 | @ob_flush(); 2193 | @flush(); 2194 | } 2195 | } 2196 | 2197 | /** 2198 | * Returns instance debug status for static calls 2199 | * 2200 | * @throws Exception 2201 | * 2202 | * @param Thread $thread 2203 | * 2204 | * @return bool 2205 | */ 2206 | private static function stGetDebug(&$thread = null) 2207 | { 2208 | static $class, $debug; 2209 | 2210 | isset($class) 2211 | || __CLASS__ === ($class = get_called_class()) 2212 | || $class = key(self::$threadsByClasses); 2213 | 2214 | if (empty(self::$threadsByClasses[$class])) { 2215 | // Couldn't find threads of type $class 2216 | // Called in destructor or something similar 2217 | return $debug; 2218 | } 2219 | 2220 | $thread = reset(self::$threadsByClasses[$class]); 2221 | return $debug = $thread->debug; 2222 | } 2223 | 2224 | #endregion 2225 | } 2226 | 2227 | 2228 | // IPC data transfer mode 2229 | function_exists('igbinary_serialize') 2230 | && Thread::$ipcDataMode = Thread::IPC_IGBINARY; 2231 | 2232 | // Forks 2233 | Thread::$useForks = Base::$hasForkSupport && Base::$hasLibevent; 2234 | -------------------------------------------------------------------------------- /ThreadPool.php: -------------------------------------------------------------------------------- 1 | 15 | * @license MIT 16 | */ 17 | class ThreadPool 18 | { 19 | /** 20 | * All started pools count 21 | * 22 | * @var int 23 | */ 24 | protected static $allPoolsCount = 0; 25 | 26 | /** 27 | * Maximum threads number in pool 28 | */ 29 | protected $maxThreads = 4; 30 | 31 | /** 32 | * Internal pool/thread id 33 | * 34 | * @var int 35 | */ 36 | protected $id; 37 | 38 | /** 39 | * Current pool name 40 | */ 41 | protected $poolName; 42 | 43 | /** 44 | * Thread name 45 | */ 46 | protected $tName; 47 | 48 | /** 49 | * Thread process name 50 | */ 51 | protected $pName; 52 | 53 | /** 54 | * Waiting number 55 | */ 56 | protected $waitNumber; 57 | 58 | /** 59 | * Event listeners 60 | */ 61 | protected $listeners = array(); 62 | 63 | /** 64 | * Threads in pool (id => thread) 65 | * 66 | * @var Thread[] 67 | */ 68 | public $threads = array(); 69 | 70 | /** 71 | * Waiting threads IDs (id => id) 72 | * 73 | * @var int[] 74 | */ 75 | public $waiting = array(); 76 | 77 | /** 78 | * Working threads IDs (id => id) 79 | * 80 | * @var int[] 81 | */ 82 | public $working = array(); 83 | 84 | /** 85 | * Initializing threads IDs (id => id) 86 | * 87 | * @var int[] 88 | */ 89 | public $initializing = array(); 90 | 91 | /** 92 | * Failed threads IDs (id => id) 93 | * 94 | * @var int[] 95 | */ 96 | public $failed = array(); 97 | 98 | /** 99 | * Received results (id => result) 100 | */ 101 | public $results = array(); 102 | 103 | /** 104 | * Current threads count 105 | */ 106 | public $threadsCount = 0; 107 | 108 | /** 109 | * Whether to show debugging information 110 | * 111 | * @var bool 112 | */ 113 | public $debug = false; 114 | 115 | 116 | /** 117 | * Thread pool initialization 118 | * 119 | * @param string $threadName Thread class name 120 | * @param int $maxThreads Maximum threads number in pool 121 | * @param string $pName Thread process name 122 | * @param bool $debug Whether to enable debug mode 123 | * @param string $name Pool name 124 | */ 125 | public function __construct($threadName, $maxThreads = null, 126 | $pName = null, $debug = false, $name = 'base') 127 | { 128 | $debug && $this->debug = true; 129 | 130 | $this->id = ++self::$allPoolsCount; 131 | $this->poolName = $name; 132 | $this->tName = $threadName; 133 | 134 | !Thread::$useForks && $this->maxThreads = 1; 135 | isset($maxThreads) && $this->setMaxThreads($maxThreads); 136 | isset($pName) && $this->pName = $pName; 137 | 138 | $this->debug( 139 | "Pool of '$threadName' threads created." 140 | ); 141 | 142 | $this->createAllThreads(); 143 | } 144 | 145 | /** 146 | * Destruction 147 | */ 148 | public function __destruct() 149 | { 150 | $this->debug('Destructor'); 151 | $this->cleanup(); 152 | } 153 | 154 | 155 | /** 156 | * Pool cleanup 157 | */ 158 | public function cleanup() 159 | { 160 | $this->debug('Cleanup'); 161 | foreach ($this->threads as $thread) { 162 | $thread->cleanup(); 163 | } 164 | } 165 | 166 | 167 | /** 168 | * Creates threads while has free slots 169 | */ 170 | protected function createAllThreads() 171 | { 172 | if (($count = &$this->threadsCount) < ($tMax = $this->maxThreads)) { 173 | do { 174 | /** @var $thread Thread */ 175 | $thread = $this->tName; 176 | $thread = new $thread($this->debug, $this->pName, $this); 177 | $id = $thread->getId(); 178 | $this->threads[$id] = $thread; 179 | $count++; 180 | $this->debug("Thread #$id created"); 181 | } while ($count < $tMax); 182 | } 183 | } 184 | 185 | 186 | /** 187 | * Starts one of free threads 188 | * 189 | * @return int|bool Thread ID or FALSE of no free threads in pool 190 | */ 191 | public function run() 192 | { 193 | $this->createAllThreads(); 194 | if ($this->hasWaiting()) { 195 | $threadId = reset($this->waiting); 196 | $thread = $this->threads[$threadId]; 197 | $args = func_get_args(); 198 | if (($count = count($args)) === 0) { 199 | $thread->run(); 200 | } else if ($count === 1) { 201 | $thread->run($args[0]); 202 | } else if ($count === 2) { 203 | $thread->run($args[0], $args[1]); 204 | } else if ($count === 3) { 205 | $thread->run($args[0], $args[1], $args[2]); 206 | } else { 207 | call_user_func_array(array($thread, 'run'), $args); 208 | } 209 | $this->waitNumber--; 210 | $this->debug("Thread #$threadId started"); 211 | return $threadId; 212 | } 213 | return false; 214 | } 215 | 216 | /** 217 | * Waits for waiting threads in pool 218 | * 219 | * @param array $failed Array of failed threads 220 | * 221 | * @return array|bool Returns array of results or FALSE if no results 222 | * 223 | * @throws Exception 224 | */ 225 | public function wait(&$failed = null) 226 | { 227 | $this->waitNumber = null; 228 | if ($this->results || $this->failed) { 229 | return $this->getResults($failed); 230 | } 231 | if (($w = $this->working) || $this->initializing) { 232 | if ($this->initializing) { 233 | $w += $this->initializing; 234 | } 235 | $this->debug && $this->debug( 236 | 'Waiting for threads: ' . join(', ', $w) 237 | ); 238 | Thread::waitThreads($w); 239 | } else { 240 | throw new Exception( 241 | 'Nothing to wait in pool' 242 | ); 243 | } 244 | return $this->getResults($failed); 245 | } 246 | 247 | 248 | /** 249 | * Returns if pool has waiting threads 250 | * 251 | * @return bool 252 | */ 253 | public function hasWaiting() 254 | { 255 | if ($this->waitNumber === null && $this->waiting) { 256 | $this->waitNumber = count($this->waiting); 257 | return true; 258 | } else { 259 | return $this->waitNumber > 0; 260 | } 261 | } 262 | 263 | 264 | /** 265 | * Returns array of results by threads or false 266 | * 267 | * @param array $failed Array of failed threads 268 | * 269 | * @return array|bool 270 | */ 271 | protected function getResults(&$failed = null) 272 | { 273 | if ($res = $this->results) { 274 | $this->results = array(); 275 | } else { 276 | $res = false; 277 | } 278 | $failed = $this->failed; 279 | $this->failed = array(); 280 | return $res; 281 | } 282 | 283 | /** 284 | * Returns state of all threads in pool 285 | * 286 | * @return array 287 | */ 288 | protected function getState() 289 | { 290 | $state = array(); 291 | foreach ($this->threads as $threadId => $thread) { 292 | $state[$threadId] = $thread->getStateName(); 293 | } 294 | return $state; 295 | } 296 | 297 | 298 | /** 299 | * Connects a listener to a given event. 300 | * 301 | * @see trigger 302 | * 303 | * @param string $event

304 | * An event name. 305 | *

306 | * @param callback $listener

307 | * Callback to be called when the matching event occurs. 308 | *
function(string $event_name, int $thread_id, mixed $event_data, mixed $event_arg){} 309 | *

310 | * @param mixed $arg

311 | * Additional argument for callback. 312 | *

313 | */ 314 | public function bind($event, $listener, $arg = null) 315 | { 316 | if (!isset($this->listeners[$event])) { 317 | $this->listeners[$event] = array(); 318 | } 319 | $this->listeners[$event][] = array($listener, $arg); 320 | $this->debug( 321 | "New listener binded on event [$event]" 322 | ); 323 | } 324 | 325 | /** 326 | * Notifies all listeners of a given event. 327 | * 328 | * @see bind 329 | * 330 | * @param string $event An event name 331 | * @param int $threadId Id of thread that caused the event 332 | * @param mixed $data Event data for callback 333 | */ 334 | public function trigger($event, $threadId, $data = null) 335 | { 336 | $this->debug("Triggering event [$event]"); 337 | if (!empty($this->listeners[$event])) { 338 | /** @var $cb callback */ 339 | foreach ($this->listeners[$event] as $l) { 340 | list($cb, $arg) = $l; 341 | if ($cb instanceof \Closure) { 342 | $cb($event, $threadId, $data, $arg); 343 | } else { 344 | call_user_func( 345 | $cb, $event, $threadId, $data, $arg 346 | ); 347 | } 348 | } 349 | } 350 | } 351 | 352 | 353 | /** 354 | * Sets maximum threads number 355 | * 356 | * @param int $value 357 | */ 358 | public function setMaxThreads($value) 359 | { 360 | if ($value < $this->threadsCount) { 361 | $value = $this->threadsCount; 362 | } else if (!Thread::$useForks || $value < 1) { 363 | $value = 1; 364 | } 365 | $this->maxThreads = (int)$value; 366 | } 367 | 368 | 369 | /** 370 | * Debug logging 371 | * 372 | * @param string $message 373 | */ 374 | protected function debug($message) 375 | { 376 | if (!$this->debug) { 377 | return; 378 | } 379 | 380 | $time = Daemon::getTimeForLog(); 381 | $poolId = $this->id; 382 | $poolName = $this->poolName; 383 | $pid = posix_getpid(); 384 | $message = "{$time} [debug] [P{$poolId}.{$poolName}] " 385 | ."#{$pid}: {$message}"; 386 | 387 | if (class_exists('Aza\Kernel\Core', false) && $app = Core::$app) { 388 | $app->msg($message, Logger::LVL_DEBUG); 389 | } else { 390 | echo strip_tags($message), PHP_EOL; 391 | @ob_flush(); 392 | @flush(); 393 | } 394 | } 395 | } 396 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aza/thread", 3 | "description": "AzaThread - Anizoptera CMF simple and powerful threads emulation component for PHP (based on forks).", 4 | "keywords": [ 5 | "fork", "thread", "async", "parallel", "serialization", "multi-thread", "daemon" 6 | ], 7 | "homepage": "https://github.com/Anizoptera/AzaThread", 8 | "license": "MIT", 9 | "support": { 10 | "issues": "https://github.com/Anizoptera/AzaThread/issues" 11 | }, 12 | "authors": [ 13 | {"name": "Amal Samally", "email": "amal.samally@gmail.com", "homepage": "https://github.com/amal"}, 14 | {"name": "AzaGroup Members"} 15 | ], 16 | "require": { 17 | "php": ">=5.3.3", 18 | "aza/clibase": "~1.0", 19 | "aza/libevent": "~1.0", 20 | "aza/socket": "~1.0" 21 | }, 22 | "suggest": { 23 | "ext-posix": "Only synchronous compatibility mode will be available without it", 24 | "ext-pcntl": "Only synchronous compatibility mode will be available without it", 25 | "ext-libevent": "Only synchronous compatibility mode will be available without it" 26 | }, 27 | "autoload": { 28 | "psr-0": { 29 | "Aza\\Components\\Thread": "" 30 | } 31 | }, 32 | "target-dir": "Aza/Components/Thread", 33 | "extra": { 34 | "branch-alias": { 35 | "dev-master": "1.x-dev" 36 | } 37 | }, 38 | "minimum-stability": "dev" 39 | } 40 | -------------------------------------------------------------------------------- /examples/example.php: -------------------------------------------------------------------------------- 1 | 17 | * @license MIT 18 | */ 19 | 20 | 21 | 22 | /** 23 | * Test thread 24 | */ 25 | class TestThreadReturnFirstArgument extends Thread 26 | { 27 | /** 28 | * Main processing. 29 | * 30 | * @return mixed 31 | */ 32 | protected function process() 33 | { 34 | return $this->getParam(0); 35 | } 36 | } 37 | 38 | /** 39 | * Test thread 40 | */ 41 | class TestThreadEvents extends Thread 42 | { 43 | const EV_PROCESS = 'process'; 44 | 45 | /** 46 | * Main processing. 47 | * 48 | * @return mixed 49 | */ 50 | protected function process() 51 | { 52 | $events = $this->getParam(0); 53 | for ($i = 0; $i < $events; $i++) { 54 | $this->trigger(self::EV_PROCESS, $i); 55 | } 56 | } 57 | } 58 | 59 | 60 | // Checks 61 | if (!Thread::$useForks) { 62 | echo PHP_EOL, "You do not have the minimum system requirements to work in async mode!!!"; 63 | if (!Base::$hasForkSupport) { 64 | echo PHP_EOL, "You don't have pcntl or posix extensions installed or either not CLI SAPI environment!"; 65 | } 66 | if (!Base::$hasLibevent) { 67 | echo PHP_EOL, "You don't have libevent extension installed!"; 68 | } 69 | echo PHP_EOL; 70 | } 71 | 72 | 73 | // ---------------------------------------------- 74 | echo PHP_EOL, 75 | 'Simple example with one thread', 76 | PHP_EOL; 77 | 78 | $num = 10; // Number of tasks 79 | $thread = new TestThreadReturnFirstArgument(); 80 | 81 | // You can override preforkWait property 82 | // to TRUE to not wait thread at first time manually 83 | $thread->wait(); 84 | 85 | for ($i = 0; $i < $num; $i++) { 86 | $value = $i; 87 | // Run task and wait for the result 88 | if ($thread->run($value)->wait()->getSuccess()) { 89 | // Success 90 | $result = $thread->getResult(); 91 | echo 'result: ' . $result . PHP_EOL; 92 | } else { 93 | // Error handling here 94 | // processing is not successful if thread dies 95 | // when worked or working timeout exceeded 96 | echo 'error' . PHP_EOL; 97 | } 98 | } 99 | $thread->cleanup(); 100 | 101 | 102 | 103 | // ---------------------------------------------- 104 | echo PHP_EOL, 105 | 'Simple example with thread events', 106 | PHP_EOL; 107 | 108 | $events = 10; // Number of events 109 | $num = 3; // Number of tasks 110 | 111 | $thread = new TestThreadEvents(); 112 | 113 | // You can override preforkWait property 114 | // to TRUE to not wait thread at first time manually 115 | $thread->wait(); 116 | 117 | $cb = function($event_name, $event_data) { 118 | echo "event: $event_name : ", $event_data, PHP_EOL; 119 | }; 120 | $thread->bind(TestThreadEvents::EV_PROCESS, $cb); 121 | 122 | for ($i = 0; $i < $num; $i++) { 123 | $thread->run($events)->wait(); 124 | echo 'task ended', PHP_EOL; 125 | } 126 | $thread->cleanup(); 127 | 128 | 129 | 130 | // ---------------------------------------------- 131 | $threads = 4; 132 | 133 | echo PHP_EOL, 134 | "Simple example with pool of threads ($threads)", 135 | PHP_EOL; 136 | 137 | $pool = new ThreadPool('TestThreadReturnFirstArgument', $threads); 138 | 139 | $num = 25; // Number of tasks 140 | $left = $num; // Number of remaining tasks 141 | do { 142 | while ($left > 0 && $pool->hasWaiting()) { 143 | if (!$threadId = $pool->run($left)) { 144 | throw new Exception('Pool slots error'); 145 | } 146 | $left--; 147 | } 148 | if ($results = $pool->wait($failed)) { 149 | foreach ($results as $threadId => $result) { 150 | $num--; 151 | echo "result: $result (thread $threadId)", PHP_EOL; 152 | } 153 | } 154 | if ($failed) { 155 | // Error handling here 156 | // processing is not successful if thread dies 157 | // when worked or working timeout exceeded 158 | foreach ($failed as $threadId) { 159 | echo "error (thread $threadId)", PHP_EOL; 160 | $left++; 161 | } 162 | } 163 | } while ($num > 0); 164 | $pool->cleanup(); 165 | 166 | 167 | 168 | // ---------------------------------------------- 169 | $threads = 8; 170 | $jobs = range(1, 30); 171 | $jobs_num = count($jobs); 172 | 173 | echo PHP_EOL, 174 | "Example with pool of threads ($threads) and pool of jobs ($jobs_num)", 175 | PHP_EOL; 176 | 177 | $pool = new ThreadPool('TestThreadReturnFirstArgument', $threads); 178 | 179 | $num = $jobs_num; // Number of tasks 180 | $left = $jobs_num; // Number of remaining tasks 181 | $started = array(); 182 | do { 183 | while ($left > 0 && $pool->hasWaiting()) { 184 | $task = array_shift($jobs); 185 | if (!$threadId = $pool->run($task)) { 186 | throw new Exception('Pool slots error'); 187 | } 188 | $started[$threadId] = $task; 189 | $left--; 190 | } 191 | if ($results = $pool->wait($failed)) { 192 | foreach ($results as $threadId => $result) { 193 | unset($started[$threadId]); 194 | $num--; 195 | echo "result: $result (thread $threadId)", PHP_EOL; 196 | } 197 | } 198 | if ($failed) { 199 | // Error handling here 200 | // processing is not successful if thread dies 201 | // when worked or working timeout exceeded 202 | foreach ($failed as $threadId) { 203 | $jobs[] = $started[$threadId]; 204 | echo "error: {$started[$threadId]} (thread $threadId)", PHP_EOL; 205 | unset($started[$threadId]); 206 | $left++; 207 | } 208 | } 209 | } while ($num > 0); 210 | $pool->cleanup(); 211 | -------------------------------------------------------------------------------- /examples/speed_test.php: -------------------------------------------------------------------------------- 1 | 15 | * @license MIT 16 | */ 17 | 18 | /** 19 | * Threads speed test results in jobs per second 20 | * 21 | * ================================================= 22 | * 23 | * Intel Core i3 540 3.07 Ghz (Ubuntu 11.04) results 24 | * 25 | * IPC (empty jobs): 26 | * 27 | * 20219 - 1 thread (Sync) 28 | * 19900 - 1 thread (Sync, Data transfer) 29 | * 30 | * 4460 - 1 thread (Async) 31 | * 3082 - 1 thread (Async, Data transfer) 32 | * 33 | * 5224 - 2 threads (Async) 34 | * 3894 - 2 threads (Async, Data transfer) 35 | * 36 | * 5718 - 4 threads (Async) 37 | * 4295 - 4 threads (Async, Data transfer) 38 | * 39 | * 5806 - 8 threads (Async) 40 | * 4371 - 8 threads (Async, Data transfer) 41 | * 42 | * 5842 - 10 threads (Async) 43 | * 4303 - 10 threads (Async, Data transfer) 44 | * 45 | * 5890 - 12 threads (Async) 46 | * 4333 - 12 threads (Async, Data transfer) 47 | * 48 | * 6018 - 16 threads (Async) 49 | * 4234 - 16 threads (Async, Data transfer) 50 | * 51 | * Working jobs: 52 | * 53 | * 553 - 1 thread (Sync) 54 | * 330 - 1 thread (Async) 55 | * 580 - 2 threads (Async) 56 | * 1015 - 4 threads (Async) 57 | * 1040 - 8 threads (Async) 58 | * 1027 - 10 threads (Async) 59 | * 970 - 12 threads (Async) 60 | * 958 - 16 threads (Async) 61 | * 62 | * ================================================= 63 | * 64 | * Intel Core i7 2600K 3.40 Ghz (Ubuntu 11.04 on VMware virtual machine) results 65 | * 66 | * IPC (empty jobs): 67 | * 68 | * 26394 - 1 thread (Sync) 69 | * 25032 - 1 thread (Sync, Data transfer) 70 | * 71 | * 4928 - 1 thread (Async) 72 | * 4164 - 1 thread (Async, Data transfer) 73 | * 74 | * 7210 - 2 threads (Async) 75 | * 5910 - 2 threads (Async, Data transfer) 76 | * 77 | * 7129 - 4 threads (Async) 78 | * 6261 - 4 threads (Async, Data transfer) 79 | * 80 | * 7633 - 8 threads (Async) 81 | * 6630 - 8 threads (Async, Data transfer) 82 | * 83 | * 7810 - 10 threads (Async) 84 | * 6715 - 10 threads (Async, Data transfer) 85 | * 86 | * 7641 - 12 threads (Async) 87 | * 6540 - 12 threads (Async, Data transfer) 88 | * 89 | * 7587 - 16 threads (Async) 90 | * 6514 - 16 threads (Async, Data transfer) 91 | * 92 | * Working jobs: 93 | * 94 | * 763 - 1 thread (Sync) 95 | * 669 - 1 thread (Async) 96 | * 1254 - 2 threads (Async) 97 | * 2188 - 4 threads (Async) 98 | * 2618 - 8 threads (Async) 99 | * 2719 - 10 threads (Async) 100 | * 2739 - 12 threads (Async) 101 | * 2904 - 16 threads (Async) 102 | * 2830 - 18 threads (Async) 103 | * 2730 - 20 threads (Async) 104 | * 105 | * ================================================= 106 | */ 107 | 108 | 109 | 110 | 111 | ############# 112 | # Settings 113 | ############# 114 | 115 | $data = true; // Transmit data 116 | $work = true; // Do some work 117 | $tests = 6; // Number of iterations in tests 118 | $jobsT = 10000; // Number of jobs to do in one thread 119 | $jobsP = 20000; // Number of jobs to do in pools 120 | $poolMin = 2; // Minimum threads number in pool to test 121 | 122 | // Disable to use sync mode 123 | // Thread::$useForks = false; 124 | 125 | // Manually specify the type of data transfer between threads 126 | // Thread::$ipcDataMode = Thread::IPC_IGBINARY; 127 | 128 | 129 | 130 | 131 | ############# 132 | # Test 133 | ############# 134 | 135 | 136 | /** 137 | * Test thread 138 | */ 139 | class TestThreadNothing extends Thread 140 | { 141 | /** 142 | * Main processing. 143 | * 144 | * @return mixed 145 | */ 146 | protected function process() 147 | { 148 | } 149 | } 150 | 151 | /** 152 | * Test thread 153 | */ 154 | class TestThreadReturn extends Thread 155 | { 156 | /** 157 | * Main processing. 158 | * 159 | * @return mixed 160 | */ 161 | protected function process() 162 | { 163 | return array(123456789, 'abcdefghigklmnopqrstuvwxyz', 123.7456328); 164 | } 165 | } 166 | 167 | /** 168 | * Test thread 169 | */ 170 | class TestThreadWork extends Thread 171 | { 172 | /** 173 | * Main processing. 174 | * 175 | * @return mixed 176 | */ 177 | protected function process() 178 | { 179 | $r = null; 180 | $i = 1000; 181 | while ($i--) { 182 | $r = mt_rand(0, PHP_INT_MAX) * mt_rand(0, PHP_INT_MAX); 183 | } 184 | return $r; 185 | } 186 | } 187 | 188 | 189 | 190 | /** 191 | * Prints message 192 | * 193 | * @parma string $msg Message 194 | * @parma int $newline Newlines count 195 | */ 196 | $print = function($msg = '', $newline = 1) { 197 | echo $msg . str_repeat(PHP_EOL, (int)$newline); 198 | @ob_flush(); @flush(); 199 | }; 200 | $line = str_repeat('-', 80); 201 | 202 | 203 | if (!Thread::$useForks) { 204 | $print( 205 | 'ERROR: You need Forks, LibEvent, PCNTL and POSIX' 206 | .' support with CLI sapi to fully test Threads' 207 | ); 208 | return; 209 | } 210 | 211 | 212 | /** @var $threadClass Thread */ 213 | $threadClass = $work ? 'TestThreadWork' : ($data ? 'TestThreadReturn' : 'TestThreadNothing'); 214 | $threadClass = __NAMESPACE__ . '\\' . $threadClass; 215 | 216 | $arg1 = (object)array('foobarbaz' => 1234567890, 12.9876543); 217 | $arg2 = 123/16; 218 | 219 | 220 | 221 | // Test one thread 222 | $print($line); 223 | $print("One thread test; Jobs: $jobsT; Iterations: $tests"); 224 | $print($line, 2); 225 | 226 | /** @var $thread Thread */ 227 | $thread = new $threadClass; 228 | $thread->wait(); 229 | $res = array(); 230 | 231 | for ($j = 0; $j < $tests; ++$j) { 232 | $start = microtime(true); 233 | for ($i = 0; $i < $jobsT; $i++) { 234 | $data ? $thread->run($arg1, $arg2) : $thread->run(); 235 | $thread->wait()->getResult(); 236 | } 237 | $end = bcsub(microtime(true), $start, 99); 238 | $oneJob = bcdiv($end, $jobsT, 99); 239 | $res[] = bcdiv(1, $oneJob, 99); 240 | $jps = bcdiv(1, $oneJob); 241 | $print("Iteration: ".($j+1)."; Jobs per second: $jps"); 242 | } 243 | $sum = 0; 244 | foreach ($res as $r) { 245 | $sum = bcadd($sum, $r, 99); 246 | } 247 | $averageOneThreadJps = bcdiv($sum, $tests); 248 | $print("Average jobs per second: $averageOneThreadJps", 3); 249 | 250 | $thread->cleanup(); 251 | 252 | 253 | 254 | // Test pools 255 | $bestJps = 0; 256 | $bestThreadsNum = 0; 257 | $regression = 0; 258 | $lastJps = 0; 259 | $print($line); 260 | $print("Pool test; Jobs: $jobsP; Iterations: $tests"); 261 | $print($line, 2); 262 | 263 | $threads = $poolMin; 264 | $pool = new ThreadPool($threadClass, $threads); 265 | do { 266 | $print("Threads: $threads"); 267 | $print($line); 268 | 269 | $res = array(); 270 | for ($j = 0; $j < $tests; ++$j) { 271 | $start = microtime(true); 272 | 273 | $num = $jobsP; 274 | $i = 0; 275 | $maxI = ceil($jobsP * 1.5); 276 | do { 277 | while ($pool->hasWaiting()) { 278 | $data ? $pool->run($arg1, $arg2) : $pool->run(); 279 | } 280 | if ($results = $pool->wait()) { 281 | $num -= count($results); 282 | } 283 | $i++; 284 | } while ($num > 0 && $i < $maxI); 285 | 286 | $end = bcsub(microtime(true), $start, 99); 287 | $oneJob = bcdiv($end, $jobsP, 99); 288 | $res[] = bcdiv(1, $oneJob, 99); 289 | $jps = bcdiv(1, $oneJob); 290 | $print("Iteration: ".($j+1)."; Jobs per second: $jps"); 291 | } 292 | $sum = 0; 293 | foreach ($res as $r) { 294 | $sum = bcadd($sum, $r, 99); 295 | } 296 | $avJps = bcdiv($sum, $tests); 297 | $print("Average jobs per second: $avJps", 2); 298 | 299 | if ($bestJps < $avJps) { 300 | $bestJps = $avJps; 301 | $bestThreadsNum = $threads; 302 | } 303 | if ($avJps < $bestJps || $avJps < $lastJps) { 304 | $regression++; 305 | } 306 | if ($regression >= 3) { 307 | break; 308 | } 309 | $lastJps = $avJps; 310 | 311 | // Increase number of threads 312 | $threads++; 313 | $pool->setMaxThreads($threads); 314 | } while (true); 315 | 316 | $pool->cleanup(); 317 | 318 | 319 | $print('', 3); 320 | $print("Best number of threads for your system: {$bestThreadsNum} ({$bestJps} jobs per second)"); 321 | 322 | $boost = bcdiv($bestJps, bcdiv($averageOneThreadJps, 100, 99), 2); 323 | $print("Performance boost in relation to a single thread: {$boost}%"); 324 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ./Tests/ 27 | 28 | 29 | 30 | 31 | 32 | benchmark 33 | performance 34 | 35 | 36 | 37 | 38 | 39 | ./ 40 | 41 | ./examples 42 | ./Tests 43 | ./vendor 44 | 45 | 46 | 47 | 48 | --------------------------------------------------------------------------------