├── .gitignore ├── .scrutinizer.yml ├── .sensiolabs.yml ├── .travis.yml ├── Fork ├── Fork.php ├── ForkInterface.php ├── Process.php ├── ProcessInterface.php ├── Task.php └── TaskInterface.php ├── LICENSE ├── README.md ├── Tests ├── Fork │ ├── ForkTest.php │ └── ProcessTest.php └── Task │ └── DemoTask.php ├── composer.json └── phpunit.xml.dist /.gitignore: -------------------------------------------------------------------------------- 1 | /composer.lock 2 | /vendor/ 3 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | excluded_paths: 3 | - vendor/* 4 | - Tests/* 5 | 6 | checks: 7 | php: 8 | code_rating: true 9 | duplication: true 10 | 11 | build: 12 | tests: 13 | override: 14 | - 15 | command: ./vendor/bin/phpunit --coverage-clover ./clover.xml 16 | coverage: 17 | file: clover.xml 18 | format: clover 19 | -------------------------------------------------------------------------------- /.sensiolabs.yml: -------------------------------------------------------------------------------- 1 | ignore_branches: 2 | - gh-pages 3 | 4 | global_exclude_dirs: 5 | - vendor 6 | - Tests 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | sudo: false 3 | 4 | php: 5 | - 7.1 6 | - 7.2 7 | 8 | cache: 9 | directories: 10 | - $HOME/.composer/cache 11 | 12 | before_install: 13 | - phpenv config-rm xdebug.ini || true 14 | - phpenv rehash 15 | - composer self-update 16 | 17 | install: 18 | - composer install 19 | 20 | script: 21 | - ./vendor/bin/phpunit 22 | -------------------------------------------------------------------------------- /Fork/Fork.php: -------------------------------------------------------------------------------- 1 | tasks = []; 27 | $this->process = $process; 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function getProcess(): ProcessInterface 34 | { 35 | return $this->process; 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function exists(TaskInterface $task): bool 42 | { 43 | return false !== in_array($task, $this->tasks, true); 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function attach(TaskInterface $task): ForkInterface 50 | { 51 | $this->tasks[] = $task; 52 | 53 | return $this; 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public function detach(TaskInterface $task): ForkInterface 60 | { 61 | if ($this->exists($task)) { 62 | $key = array_search($task, $this->tasks, true); 63 | 64 | unset($this->tasks[$key]); 65 | } 66 | 67 | return $this; 68 | } 69 | 70 | /** 71 | * {@inheritdoc} 72 | */ 73 | public function each(): \Closure 74 | { 75 | return function () { 76 | foreach ($this->tasks as $task) { 77 | $task->execute(); 78 | } 79 | }; 80 | } 81 | 82 | /** 83 | * {@inheritdoc} 84 | */ 85 | public function run(int $processesCount = ProcessInterface::AUTO_DETECT_OF_PROCESSES_QUANTITY): ProcessInterface 86 | { 87 | return $this->process->setCountOfChildProcesses($processesCount)->create($this->each()); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Fork/ForkInterface.php: -------------------------------------------------------------------------------- 1 | setAllowedFork(function_exists('pcntl_fork')); 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function setAllowedFork(bool $allowedFork): void 29 | { 30 | $this->allowedFork = $allowedFork; 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public function create(\Closure $closure): ProcessInterface 37 | { 38 | for ($i = 0; $i < $this->processesCount; ++$i) { 39 | if ($this->fork()) { 40 | $closure(); 41 | 42 | $this->terminate(); 43 | } 44 | } 45 | 46 | return $this; 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | */ 52 | public function wait(): ProcessInterface 53 | { 54 | if ($this->allowedFork) { 55 | pcntl_wait($status); 56 | } 57 | 58 | return $this; 59 | } 60 | 61 | /** 62 | * {@inheritdoc} 63 | */ 64 | public function getPid(): int 65 | { 66 | return getmypid(); 67 | } 68 | 69 | /** 70 | * {@inheritdoc} 71 | * 72 | * @codeCoverageIgnore 73 | */ 74 | public function isAlive(int $pid): bool 75 | { 76 | if (0 === strncasecmp(PHP_OS, 'win', 3)) { 77 | exec(sprintf('TASKLIST /FO LIST /FI "PID eq %d"', $pid), $info); 78 | 79 | return count($info) > 1; 80 | } 81 | 82 | return posix_kill($pid, 0); 83 | } 84 | 85 | /** 86 | * {@inheritdoc} 87 | */ 88 | public function getMemoryUsage(): float 89 | { 90 | return round(memory_get_usage(true) / 1024 / 1024, 2); 91 | } 92 | 93 | /** 94 | * {@inheritdoc} 95 | */ 96 | public function setCountOfChildProcesses(int $processesCount): ProcessInterface 97 | { 98 | if ($processesCount < 1) { 99 | $processesCount = $this->getOptimalNumberOfChildProcesses(); 100 | } 101 | 102 | if ($processesCount > static::MAX_PROCESSES_QUANTITY) { 103 | $processesCount = static::MAX_PROCESSES_QUANTITY; 104 | } 105 | 106 | if ($this->allowedFork) { 107 | $this->processesCount = $processesCount; 108 | } else { 109 | $this->processesCount = 1; 110 | } 111 | 112 | return $this; 113 | } 114 | 115 | /** 116 | * @return bool Returns TRUE if this is a child process, else - FALSE 117 | */ 118 | protected function fork(): bool 119 | { 120 | if ($this->allowedFork) { 121 | return 0 === pcntl_fork(); 122 | } 123 | 124 | return true; 125 | } 126 | 127 | /** 128 | * @codeCoverageIgnore 129 | * 130 | * @return bool 131 | */ 132 | protected function terminate(): bool 133 | { 134 | if ($this->allowedFork) { 135 | return posix_kill(getmypid(), SIGKILL); 136 | } 137 | 138 | return true; 139 | } 140 | 141 | /** 142 | * Returns the optimal number of child processes. 143 | * 144 | * @return int 145 | */ 146 | protected function getOptimalNumberOfChildProcesses(): int 147 | { 148 | $coreNumber = 1; 149 | $detectCommand = [ 150 | 'linux' => 'cat /proc/cpuinfo | grep processor | wc -l', 151 | ]; 152 | 153 | $os = strtolower(trim(PHP_OS)); 154 | 155 | if (isset($detectCommand[$os])) { 156 | $coreNumber = intval($this->execute($detectCommand[$os])); 157 | } 158 | 159 | return $coreNumber; 160 | } 161 | 162 | /** 163 | * @param string $command 164 | * 165 | * @return string 166 | */ 167 | protected function execute(string $command): string 168 | { 169 | return trim(shell_exec($command)); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /Fork/ProcessInterface.php: -------------------------------------------------------------------------------- 1 | = static::GARBAGE_COLLECT_ITERATIONS) { 24 | $currentIteration = 0; 25 | 26 | gc_collect_cycles(); 27 | 28 | return true; 29 | } 30 | 31 | return false; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Fork/TaskInterface.php: -------------------------------------------------------------------------------- 1 | attach($task)->run(4)->wait(); // 4 - this is number of subprocesses 58 | ``` 59 | 60 | And another example: 61 | ``` php 62 | $task1 = new DemoTask(); 63 | $task2 = new DemoTask(); 64 | $task3 = new DemoTask(); 65 | 66 | $fork->attach($task1)->attach($task2)->attach($task3); 67 | $fork->run(); // by default, the optimal number of subprocesses will be determined 68 | $fork->wait(); 69 | ``` 70 | 71 | If you call method `wait`, the current process (main) will wait while all child processes will be finished. 72 | 73 | [package-link]: https://packagist.org/packages/symfony-bundles/fork 74 | [license-link]: https://github.com/symfony-bundles/fork/blob/master/LICENSE 75 | [license-image]: https://poser.pugx.org/symfony-bundles/fork/license 76 | [testing-link]: https://travis-ci.org/symfony-bundles/fork 77 | [testing-image]: https://travis-ci.org/symfony-bundles/fork.svg?branch=master 78 | [stable-image]: https://poser.pugx.org/symfony-bundles/fork/v/stable 79 | [downloads-image]: https://poser.pugx.org/symfony-bundles/fork/downloads 80 | [sensiolabs-insight-link]: https://insight.sensiolabs.com/projects/83639a9c-881b-4738-b3e9-ea304600c900 81 | [sensiolabs-insight-image]: https://insight.sensiolabs.com/projects/83639a9c-881b-4738-b3e9-ea304600c900/big.png 82 | [code-coverage-link]: https://scrutinizer-ci.com/g/symfony-bundles/fork/?branch=master 83 | [code-coverage-image]: https://scrutinizer-ci.com/g/symfony-bundles/fork/badges/coverage.png?b=master 84 | [scrutinizer-code-quality-link]: https://scrutinizer-ci.com/g/symfony-bundles/fork/?branch=master 85 | [scrutinizer-code-quality-image]: https://scrutinizer-ci.com/g/symfony-bundles/fork/badges/quality-score.png?b=master 86 | -------------------------------------------------------------------------------- /Tests/Fork/ForkTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Fork\ForkInterface::class, new Fork\Fork()); 14 | } 15 | 16 | public function testTasksPoll() 17 | { 18 | $task = new DemoTask(); 19 | $fork = new Fork\Fork(); 20 | 21 | $fork->attach(new DemoTask())->attach($task); 22 | 23 | $this->assertTrue($fork->exists($task)); 24 | $this->assertFalse($fork->exists(new DemoTask())); 25 | 26 | $this->assertFalse($fork->detach($task)->exists($task)); 27 | 28 | gc_disable(); 29 | 30 | $fork->run(); 31 | } 32 | 33 | public function testTasksExecuting() 34 | { 35 | $process = $this->getMockBuilder(Fork\Process::class) 36 | ->setMethods(['fork', 'terminate']) 37 | ->getMock(); 38 | 39 | $process->method('fork')->willReturn(true); 40 | 41 | $pid = $process->getPid(); 42 | $fork = new Fork\Fork($process); 43 | 44 | $fork 45 | ->attach($task1 = new DemoTask()) 46 | ->attach($task2 = new DemoTask()) 47 | ->attach($task3 = new DemoTask()); 48 | 49 | $fork->run(); 50 | 51 | $this->assertTrue($task1->isExecuted()); 52 | $this->assertTrue($task2->isExecuted()); 53 | $this->assertTrue($task3->isExecuted()); 54 | 55 | $this->assertSame($pid, $fork->getProcess()->getPid()); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Tests/Fork/ProcessTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Fork\ProcessInterface::class, new Fork\Process()); 13 | } 14 | 15 | public function testProcessesCount() 16 | { 17 | $process = new Fork\Process(); 18 | $reflection = new \ReflectionObject($process); 19 | 20 | $processesCount = $reflection->getProperty('processesCount'); 21 | $processesCount->setAccessible(true); 22 | 23 | $allowedFork = $reflection->getProperty('allowedFork'); 24 | $allowedFork->setAccessible(true); 25 | $allowedFork->getValue($process); 26 | 27 | $process->setCountOfChildProcesses(1); 28 | $this->assertSame(1, $processesCount->getValue($process)); 29 | 30 | $process->setCountOfChildProcesses(Fork\ProcessInterface::MAX_PROCESSES_QUANTITY + 1); 31 | 32 | if ($allowedFork->getValue($process)) { 33 | $this->assertSame(Fork\ProcessInterface::MAX_PROCESSES_QUANTITY, $processesCount->getValue($process)); 34 | } else { 35 | $this->assertSame(1, $processesCount->getValue($process)); 36 | } 37 | 38 | $process->setCountOfChildProcesses(8); 39 | $this->assertSame(function_exists('pcntl_fork') ? 8 : 1, $processesCount->getValue($process)); 40 | } 41 | 42 | public function testCreate() 43 | { 44 | $process = $this->getMockBuilder(Fork\Process::class) 45 | ->setMethods(['fork', 'terminate']) 46 | ->getMock(); 47 | 48 | $process->method('fork')->willReturn(true); 49 | 50 | $filename = '/tmp/symfony-cache-unit-test.create'; 51 | 52 | file_put_contents($filename, 0); 53 | 54 | $processesCount = mt_rand(1, Fork\ProcessInterface::MAX_PROCESSES_QUANTITY); 55 | 56 | $process->setCountOfChildProcesses($processesCount)->create(function () use ($filename) { 57 | file_put_contents($filename, file_get_contents($filename) + 1); 58 | })->wait(); 59 | 60 | $this->assertEquals(function_exists('pcntl_fork') ? $processesCount : 1, file_get_contents($filename)); 61 | } 62 | 63 | public function testForkDisabled() 64 | { 65 | $process = new Fork\Process(); 66 | $reflection = new \ReflectionObject($process); 67 | 68 | $property = $reflection->getProperty('allowedFork'); 69 | $property->setAccessible(true); 70 | $property->setValue($process, false); 71 | 72 | $method = $reflection->getMethod('fork'); 73 | $method->setAccessible(true); 74 | 75 | $process->setCountOfChildProcesses(2); 76 | 77 | $this->assertTrue($method->invoke($process)); 78 | } 79 | 80 | public function testIsAliveProcess() 81 | { 82 | $process = new Fork\Process(); 83 | 84 | $this->assertTrue($process->isAlive($process->getPid())); 85 | } 86 | 87 | public function testMemoryUsage() 88 | { 89 | $process = new Fork\Process(); 90 | 91 | $startMemoryUsage = $process->getMemoryUsage(); 92 | 93 | $list = range(1, 16400); 94 | 95 | $finishMemoryUsage = $process->getMemoryUsage(); 96 | 97 | $this->assertCount(16400, $list); 98 | 99 | $this->assertTrue($finishMemoryUsage > $startMemoryUsage + 1); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Tests/Task/DemoTask.php: -------------------------------------------------------------------------------- 1 | isExecuted; 16 | } 17 | 18 | public function execute(): void 19 | { 20 | $this->isExecuted = true; 21 | 22 | for ($i = 0; $i < 20; ++$i) { 23 | $object = new \stdClass(); 24 | $object->reference = $object; 25 | 26 | $this->foo($object); 27 | 28 | $this->garbageCollect(); 29 | } 30 | } 31 | 32 | protected function foo(\stdClass &$baz) 33 | { 34 | $baz->bar = range(0, 100000); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symfony-bundles/fork", 3 | "type": "library", 4 | "description": "SymfonyBundles Fork Library", 5 | "keywords": ["fork", "library"," flow", "process", "multiprocesses", "thread", "symfony"], 6 | "homepage": "https://github.com/symfony-bundles/fork", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Dmitry Khaperets", 11 | "email": "khaperets@gmail.com" 12 | } 13 | ], 14 | "autoload": { 15 | "psr-4": { 16 | "SymfonyBundles\\": "" 17 | }, 18 | "exclude-from-classmap": [ 19 | "/Tests/" 20 | ] 21 | }, 22 | "require": { 23 | "php": "^7.1.3" 24 | }, 25 | "require-dev": { 26 | "phpunit/phpunit": "^6.4", 27 | "phpunit/php-code-coverage": "^5.0" 28 | }, 29 | "extra": { 30 | "branch-alias": { 31 | "dev-master": "2.x-dev" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ./Tests 16 | 17 | 18 | 19 | 20 | 21 | ./ 22 | 23 | ./Tests 24 | ./vendor 25 | 26 | 27 | 28 | 29 | --------------------------------------------------------------------------------