├── tests ├── .gitignore ├── BaseTestCase.php ├── ParentTest.php ├── RunTest.php └── ChildTest.php ├── .gitignore ├── phpunit.xml ├── .github └── workflows │ └── CI.yml ├── composer.json ├── LICENSE ├── README.md └── src └── Runtime.php /tests/.gitignore: -------------------------------------------------------------------------------- 1 | *.cache 2 | !.gitignore -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /.vscode 3 | /vendor 4 | composer.lock 5 | identifier.sqlite 6 | .phpunit.result.cache -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | tests 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [ "master" ] 5 | pull_request: 6 | branches: [ "master" ] 7 | 8 | jobs: 9 | PHPUnit: 10 | name: PHPUnit (PHP ${{ matrix.php }}) 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | php: 15 | - 8.3 16 | - 8.2 17 | - 8.1 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: shivammathur/setup-php@v2 21 | with: 22 | php-version: ${{ matrix.php }} 23 | extensions: pcntl, posix 24 | tools: phpunit:9, composer:v2 25 | coverage: none 26 | - run: composer install 27 | - run: vendor/bin/phpunit 28 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workbunny/process", 3 | "type": "library", 4 | "keywords": [ 5 | "multi-process", 6 | "process", 7 | "pcntl_fork" 8 | ], 9 | "homepage": "https://github.com/process", 10 | "license": "MIT", 11 | "description": "A lightweight multi-process helper base on PHP. ", 12 | "authors": [ 13 | { 14 | "name": "chaz6chez", 15 | "email": "chaz6chez1993@outlook.com", 16 | "homepage": "https://chaz6chez.cn" 17 | } 18 | ], 19 | "support": { 20 | "email": "chaz6chez1993@outlook.com", 21 | "issues": "https://github.com/workbuunny/process/issues", 22 | "source": "https://github.com/workbuunny/process" 23 | }, 24 | "require": { 25 | "php": "^7.4 | ^8.0", 26 | "ext-pcntl": "*", 27 | "ext-posix": "*" 28 | }, 29 | "require-dev": { 30 | "symfony/var-dumper": "^5.0 | ^6.0", 31 | "phpunit/phpunit": "^9.0" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "WorkBunny\\Process\\": "src" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "Tests\\": "tests" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 workbunny 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/BaseTestCase.php: -------------------------------------------------------------------------------- 1 | removeAllCaches(); 19 | $this->_runtime = new Runtime(); 20 | parent::setUp(); 21 | } 22 | 23 | /** 24 | * @param bool $reset 25 | * @return Runtime|null 26 | */ 27 | public function runtime(bool $reset = false): ?Runtime 28 | { 29 | if($reset){ 30 | $this->_runtime = new Runtime(); 31 | } 32 | return $this->_runtime; 33 | } 34 | 35 | /** 36 | * @param string $file 37 | * @param string $content 38 | * @return void 39 | */ 40 | public function write(string $file, string $content): void 41 | { 42 | file_put_contents(__DIR__ . "/$file.cache", $content, FILE_APPEND|LOCK_EX); 43 | } 44 | 45 | /** 46 | * @param string $file 47 | * @return string 48 | */ 49 | public function read(string $file): string 50 | { 51 | if(file_exists($file = __DIR__ . "/$file.cache")){ 52 | return trim(file_get_contents($file)); 53 | } 54 | throw new \RuntimeException('Cache Not Found : ' . $file); 55 | } 56 | 57 | /** 58 | * @return void 59 | */ 60 | public function removeAllCaches() 61 | { 62 | array_map('unlink', glob( __DIR__ . '/*.cache')); 63 | } 64 | 65 | /** 66 | * @return void 67 | */ 68 | public function removeCache(string $file) 69 | { 70 | if(file_exists($file = __DIR__ . "/$file.cache")){ 71 | @unlink($file); 72 | } 73 | } 74 | 75 | /** 76 | * @param $expected 77 | * @param $actual 78 | * @param string $file 79 | * @return void 80 | */ 81 | public function assertEqualsAndRmCache($expected, $actual, string $file = ''): void 82 | { 83 | $this->removeCache($file); 84 | $this->assertEquals($expected, $actual); 85 | } 86 | 87 | /** 88 | * @param array $expected 89 | * @param array $actual 90 | * @param string $file 91 | * @return void 92 | */ 93 | public function assertContainsHasAndRmCache(array $expected, array $actual, string $file = ''){ 94 | $this->removeCache($file); 95 | $this->assertCount(count($expected), $actual); 96 | foreach ($expected as $value){ 97 | $this->assertContains($value, $actual); 98 | } 99 | } 100 | 101 | } -------------------------------------------------------------------------------- /tests/ParentTest.php: -------------------------------------------------------------------------------- 1 | runtime()->child(); 25 | $this->write($file, $this->runtime()->getId() . PHP_EOL); 26 | $this->runtime()->wait(null, null, true); 27 | $this->assertContainsHasAndRmCache(['0', '1'], explode(PHP_EOL, $this->read($file)), $file); 28 | 29 | // 测试 30 | $this->runtime(true)->child(); 31 | $this->runtime()->exitChildren(); 32 | $this->write($file, (string)$this->runtime()->getId()); 33 | $this->assertEqualsAndRmCache('0', $this->read($file), $file); 34 | 35 | 36 | } 37 | 38 | /** 39 | * 测试exit方法 40 | * @covers \WorkBunny\Process\Runtime::exit 41 | * @return void 42 | */ 43 | public function testExit() 44 | { 45 | $file = __FUNCTION__; 46 | // 对照组 47 | $this->runtime()->child(); 48 | $this->write($file, $this->runtime()->getId() . PHP_EOL); 49 | $this->runtime()->wait(null, null, true); 50 | $this->assertContainsHasAndRmCache(['0', '1'], explode(PHP_EOL, $this->read($file)), $file); 51 | 52 | // 测试组-子上下文退出 53 | $this->runtime(true)->child(function(){ 54 | $this->runtime()->exit(); 55 | }); 56 | $this->runtime()->wait(null, null, true); 57 | $this->write($file, (string)$this->runtime()->getId()); 58 | $this->assertEqualsAndRmCache('0', $this->read($file), $file); 59 | 60 | // 测试组-子上下文退出后写入无效 61 | $this->runtime(true)->child(function() use ($file){ 62 | $this->runtime()->exit(); 63 | $this->write($file, $this->runtime()->getId() . PHP_EOL); 64 | }); 65 | $this->runtime()->wait(null, null, true); 66 | $this->write($file, (string)$this->runtime()->getId()); 67 | $this->assertEqualsAndRmCache('0', $this->read($file), $file); 68 | } 69 | 70 | /** 71 | * 测试使用isChild 72 | * @covers \WorkBunny\Process\Runtime::isChild 73 | * @covers \WorkBunny\Process\Runtime::getId 74 | * @return void 75 | */ 76 | public function testIsChild() 77 | { 78 | $file = __FUNCTION__; 79 | // 对照组 80 | $this->runtime()->child(); 81 | $this->write($file, $this->runtime()->getId() . PHP_EOL); 82 | $this->runtime()->wait(null, null, true); 83 | $this->assertContainsHasAndRmCache(['0', '1'], explode(PHP_EOL, $this->read($file)), $file); 84 | 85 | // 测试组 86 | $this->runtime(true)->child(); 87 | if($this->runtime()->isChild()){ 88 | $this->write($file, $this->runtime()->getId() . PHP_EOL); 89 | } 90 | $this->runtime()->wait(null, null, true); 91 | $this->assertEqualsAndRmCache('1', $this->read($file), $file); 92 | } 93 | 94 | /** 95 | * 测试获取父Runtime ID 96 | * @covers \WorkBunny\Process\Runtime::getId 97 | * @return void 98 | */ 99 | public function testGetParentRuntimeID() 100 | { 101 | $file = __FUNCTION__; 102 | 103 | $this->runtime()->child(); 104 | $this->runtime()->parent(function(Runtime $runtime) use ($file){ 105 | $this->write($file, (string)$runtime->getId()); 106 | }); 107 | 108 | $this->runtime()->wait(null, null, true); 109 | $this->assertEqualsAndRmCache('0', $this->read($file), $file); 110 | } 111 | 112 | /** 113 | * 同父异母的子 114 | * @covers \WorkBunny\Process\Runtime::parent 115 | * @return void 116 | */ 117 | public function testHalfBrother() 118 | { 119 | $file = __FUNCTION__; 120 | 121 | $this->runtime()->child(function (){ 122 | sleep(2); 123 | }); 124 | $this->runtime()->parent(function () use ($file){ 125 | $r = new Runtime(); 126 | $r->child(function () use ($file){ 127 | $this->write($file, 'child-2' . PHP_EOL); 128 | }); 129 | // 子监听孙 130 | $r->wait(null, null, true); 131 | }); 132 | 133 | if($this->runtime()->isChild()){ 134 | $this->write($file,'child-1' . PHP_EOL); 135 | } 136 | $this->runtime()->wait(null, null, true); 137 | $this->write($file,'parent' . PHP_EOL); 138 | 139 | $this->assertEqualsAndRmCache(['child-2', 'child-1', 'parent'], explode(PHP_EOL, $this->read($file)), $file); 140 | } 141 | } -------------------------------------------------------------------------------- /tests/RunTest.php: -------------------------------------------------------------------------------- 1 | runtime()->run(function(Runtime $runtime) use ($file){ 23 | $this->write($file, $runtime->getId() . PHP_EOL); 24 | exit; 25 | },function (Runtime $runtime) use ($file){ 26 | $this->write($file, $runtime->getId() . PHP_EOL); 27 | }, 3); 28 | 29 | $this->runtime()->wait(null, null, true); 30 | 31 | $this->assertContainsHasAndRmCache( 32 | ['0', '1', '2', '3'], 33 | explode(PHP_EOL, trim($this->read($file))), 34 | $file 35 | ); 36 | } 37 | 38 | /** 39 | * 测试run 40 | * @covers \WorkBunny\Process\Runtime::run 41 | * @return void 42 | */ 43 | public function testRunUseGC() 44 | { 45 | $file = __FUNCTION__; 46 | 47 | $this->runtime()->setConfig([ 48 | 'pre_gc' => true 49 | ]); 50 | 51 | $this->runtime()->run(function(Runtime $runtime) use ($file){ 52 | $this->write($file, $runtime->getId() . PHP_EOL); 53 | exit; 54 | },function (Runtime $runtime) use ($file){ 55 | $this->write($file, $runtime->getId() . PHP_EOL); 56 | }, 3); 57 | 58 | $this->runtime()->wait(null, null, true); 59 | 60 | $this->assertContainsHasAndRmCache( 61 | ['0', '1', '2', '3'], 62 | explode(PHP_EOL, trim($this->read($file))), 63 | $file 64 | ); 65 | } 66 | 67 | /** 68 | * 测试获取优先级 69 | * @covers \WorkBunny\Process\Runtime::run 70 | * @return void 71 | */ 72 | public function testRunGetPriority() 73 | { 74 | $file = __FUNCTION__; 75 | 76 | $this->runtime()->run(function(Runtime $runtime) use ($file){ 77 | $this->write($file, $runtime->getPriority($runtime->getId()) . PHP_EOL); 78 | },function (Runtime $runtime) use ($file){ 79 | $this->write($file, $runtime->getPriority($runtime->getId()) . PHP_EOL); 80 | }, 2); 81 | 82 | $this->runtime()->wait(null, null, true); 83 | 84 | $this->assertEqualsAndRmCache( 85 | ['0', '0', '0'], 86 | explode(PHP_EOL, trim($this->read($file))), 87 | $file 88 | ); 89 | } 90 | 91 | /** 92 | * 测试run 93 | * @covers \WorkBunny\Process\Runtime::run 94 | * @return void 95 | */ 96 | public function testRunToFork() 97 | { 98 | $file = __FUNCTION__; 99 | 100 | $this->runtime()->run(function(Runtime $runtime) use ($file){ 101 | $this->write($file, 'child' . $runtime->getId() . PHP_EOL); 102 | 103 | $runtime->child(function(Runtime $r) use ($file, $runtime){ 104 | $this->write($file, 'child-child' . $runtime->getId() . $r->getId() . PHP_EOL); 105 | }); 106 | exit; 107 | 108 | },function (Runtime $runtime) use ($file){ 109 | $this->write($file, 'parent' . ($id = $runtime->getId()) . PHP_EOL); 110 | 111 | $runtime->child(function(Runtime $r) use ($file, $id){ 112 | $this->write($file, 'parent-child' . $id . $r->getId() . PHP_EOL); 113 | exit; 114 | }); 115 | 116 | }, 3); 117 | 118 | $this->runtime()->wait(null, null, true); 119 | 120 | $this->assertContainsHasAndRmCache( 121 | ['parent0','parent-child04', 'child1', 'child2', 'child3'], 122 | explode(PHP_EOL, trim($this->read($file))), 123 | $file 124 | ); 125 | } 126 | 127 | /** 128 | * 测试run的Runtime嵌套 129 | * @covers \WorkBunny\Process\Runtime::run 130 | * @return void 131 | */ 132 | public function testRunToRuntimeNesting() 133 | { 134 | $file = __FUNCTION__; 135 | 136 | $this->runtime()->run(function(Runtime $runtime) use ($file){ 137 | $this->write($file, 'child' . ($id = $runtime->getId()) . PHP_EOL); 138 | 139 | $r = new Runtime(); 140 | $r->run(function(Runtime $runtime) use ($file, $id){ 141 | $this->write($file, 'child-child' . $id . $runtime->getId() . PHP_EOL); 142 | exit; 143 | },function(Runtime $runtime) use ($file, $id){ 144 | $this->write($file, 'child-parent' . $id . $runtime->getId() . PHP_EOL); 145 | },1); 146 | exit; 147 | 148 | },function (Runtime $runtime) use ($file){ 149 | $this->write($file, 'parent' . ($id = $runtime->getId()) . PHP_EOL); 150 | 151 | $r = new Runtime(); 152 | $r->run(function(Runtime $runtime) use ($file, $id){ 153 | $this->write($file, 'parent-child' . $id . $runtime->getId() . PHP_EOL); 154 | exit; 155 | },function(Runtime $runtime) use ($file, $id){ 156 | $this->write($file, 'parent-parent' . $id . $runtime->getId() . PHP_EOL); 157 | },1); 158 | 159 | }, 1); 160 | 161 | $this->runtime()->wait(null, null, true); 162 | 163 | $this->assertContainsHasAndRmCache( 164 | ['parent0','parent-parent00', 'parent-child01', 'child1', 'child-child11', 'child-parent10'], 165 | explode(PHP_EOL, trim($this->read($file))), 166 | $file 167 | ); 168 | } 169 | } -------------------------------------------------------------------------------- /tests/ChildTest.php: -------------------------------------------------------------------------------- 1 | runtime()->child(function (Runtime $runtime) use($file){ 25 | $this->write($file, (string)$runtime->getId()); 26 | }); 27 | $this->runtime()->wait(null, null, true); 28 | $this->assertEqualsAndRmCache('1', $this->read($file), $file); 29 | 30 | $this->runtime(true)->child(function (Runtime $runtime) use($file){ 31 | $this->write($file, (string)$runtime->getId()); 32 | }); 33 | $this->runtime()->exitChildren(); 34 | $this->runtime()->wait(); 35 | $this->assertEqualsAndRmCache('1', $this->read($file), $file); 36 | } 37 | 38 | /** 39 | * 测试获取子Runtime ID【在私有上下文中Exit】 40 | * @covers \WorkBunny\Process\Runtime::exit 41 | * @covers \WorkBunny\Process\Runtime::wait 42 | * @return void 43 | */ 44 | public function testGetChildRuntimeIDPrivateContextExit() 45 | { 46 | $file = __FUNCTION__; 47 | 48 | $this->runtime()->child(function (Runtime $runtime) use($file){ 49 | $this->write($file, 'child' . PHP_EOL); 50 | $runtime->exit(); 51 | }); 52 | 53 | $this->runtime()->wait(); 54 | 55 | $this->assertEqualsAndRmCache( 56 | 'child', 57 | $this->read($file), 58 | $file 59 | ); 60 | } 61 | 62 | /** 63 | * 测试子Runtime使用入参Runtime进行fork不生效 64 | * @covers \WorkBunny\Process\Runtime::child 65 | * @covers \WorkBunny\Process\Runtime::wait 66 | * @return void 67 | */ 68 | public function testChildUseSameRuntimeForkNotEffectByCallback() 69 | { 70 | $file = __FUNCTION__; 71 | 72 | $this->runtime()->child(function(Runtime $runtime) use ($file){ 73 | $this->write($file, 'child' . PHP_EOL); 74 | 75 | $runtime->child(function () use ($file){ 76 | $this->write($file, 'child-child' . PHP_EOL); 77 | }); 78 | }); 79 | 80 | $this->runtime()->wait(null, null, true); 81 | 82 | $this->assertEqualsAndRmCache('child', $this->read($file), $file); 83 | } 84 | 85 | /** 86 | * 测试使用isChild获取子Runtime进行Fork 87 | * @covers \WorkBunny\Process\Runtime::isChild 88 | * @covers \WorkBunny\Process\Runtime::wait 89 | * @return void 90 | */ 91 | public function testChildUseSameRuntimeForkNotEffectByIsChild() 92 | { 93 | $file = __FUNCTION__; 94 | 95 | $this->runtime()->child(); 96 | 97 | if($this->runtime()->isChild()){ 98 | $this->write($file, 'child' . PHP_EOL); 99 | 100 | $this->runtime()->child(function () use ($file){ 101 | $this->write($file, 'child-child' . PHP_EOL); 102 | }); 103 | } 104 | 105 | $this->runtime()->wait(null, null, true); 106 | 107 | $this->assertEqualsAndRmCache('child', $this->read($file), $file); 108 | } 109 | 110 | /** 111 | * 测试child 112 | * @covers \WorkBunny\Process\Runtime::child 113 | * @covers \WorkBunny\Process\Runtime::exit 114 | * @return void 115 | */ 116 | public function testChild() 117 | { 118 | $file = __FUNCTION__; 119 | 120 | $this->runtime()->child(function (Runtime $runtime) use($file){ 121 | $this->write($file, 'test' . PHP_EOL); 122 | }); 123 | 124 | $this->runtime()->wait(null, null, true); 125 | 126 | $this->assertEqualsAndRmCache('test',$this->read($file), $file); 127 | } 128 | 129 | /** 130 | * 测试child的替换 131 | * @covers \WorkBunny\Process\Runtime::child 132 | * @covers \WorkBunny\Process\Runtime::exit 133 | * @return void 134 | */ 135 | public function testChildReplace() 136 | { 137 | $oldId = $this->runtime()->child(function (Runtime $runtime){}); 138 | 139 | $oldPid = null; 140 | $this->runtime()->parent(function(Runtime $runtime) use ($oldId, &$oldPid){ 141 | $oldPid = $runtime->getPidMap()[$oldId]; 142 | }); 143 | 144 | $newId = $this->runtime()->child( 145 | function (Runtime $runtime){}, 146 | 0, 147 | $oldId 148 | ); 149 | 150 | $newPid = null; 151 | $this->runtime()->parent(function(Runtime $runtime) use ($newId, &$newPid){ 152 | $newPid = $runtime->getPidMap()[$newId]; 153 | }); 154 | 155 | $this->runtime()->wait(null, null, true); 156 | 157 | $this->assertEquals(true, $oldId === $newId); 158 | $this->assertEquals(true, $oldPid !== $newPid); 159 | } 160 | 161 | /** 162 | * 测试child的替换 163 | * @covers \WorkBunny\Process\Runtime::child 164 | * @covers \WorkBunny\Process\Runtime::exit 165 | * @return void 166 | */ 167 | public function testChildReplaceUseExit() 168 | { 169 | $oldId = $this->runtime()->child(function (Runtime $runtime) { 170 | $runtime->exit(); 171 | }); 172 | 173 | $oldPid = null; 174 | $this->runtime()->parent(function(Runtime $runtime) use ($oldId, &$oldPid){ 175 | $oldPid = $runtime->getPidMap()[$oldId]; 176 | }); 177 | 178 | $newId = $this->runtime()->child( 179 | function (Runtime $runtime){ 180 | $runtime->exit(); 181 | }, 182 | 0, 183 | $oldId 184 | ); 185 | 186 | $newPid = null; 187 | $this->runtime()->parent(function(Runtime $runtime) use ($newId, &$newPid){ 188 | $newPid = $runtime->getPidMap()[$newId]; 189 | }); 190 | 191 | $this->runtime()->wait(null, null, true); 192 | 193 | $this->assertEquals(true, $oldId === $newId); 194 | $this->assertEquals(true, $oldPid !== $newPid); 195 | } 196 | 197 | /** 198 | * 每个child只输出一次 199 | * @return void 200 | */ 201 | public function testMultiChildOutputOnceEach() 202 | { 203 | $file = __FUNCTION__; 204 | 205 | $this->runtime()->child(function (Runtime $runtime) use($file){ 206 | $this->write($file, $runtime->getId() . PHP_EOL); 207 | }); 208 | 209 | $this->runtime()->child(function (Runtime $runtime) use($file){ 210 | $this->write($file, $runtime->getId() . PHP_EOL); 211 | }); 212 | 213 | $this->runtime()->wait(null, null, true); 214 | 215 | $this->assertContainsHasAndRmCache( 216 | ['1', '2'], 217 | explode(PHP_EOL, $this->read($file)), 218 | $file 219 | ); 220 | } 221 | 222 | /** 223 | * 通过子上下文内创建新runtime,父子孙三代嵌套获取runtime id 224 | * @covers \WorkBunny\Process\Runtime::run 225 | * @return void 226 | */ 227 | public function testParentChildGrandChildByChildCreateNewRuntime() 228 | { 229 | $file = __FUNCTION__; 230 | 231 | $this->runtime()->child(function(Runtime $runtime) use ($file){ 232 | // 获取子runtime id 233 | $id = $runtime->getId(); 234 | // 创建孙 235 | $r = new Runtime(); 236 | $r->child(function (Runtime $r) use ($id, $file){ 237 | // 孙写入孙runtime id 238 | $this->write($file, $id . $r->getId() . PHP_EOL); 239 | }); 240 | // 子监听孙 241 | $r->wait(null, null, true); 242 | // 子写入子runtime id 243 | $this->write($file, $runtime->getId() . PHP_EOL); 244 | }); 245 | // 父监听子 246 | $this->runtime()->wait(null,null,true); 247 | // 父写入父runtime id 248 | $this->write($file, $this->runtime()->getId() . PHP_EOL); 249 | 250 | $this->assertContainsHasAndRmCache( 251 | ['11', '1', '0'], 252 | explode(PHP_EOL, $this->read($file)), 253 | $file 254 | ); 255 | } 256 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

workbunny

3 | 4 | **

workbunny/process

** 5 | 6 | **

🐇 A lightweight multi-process helper base on PHP. 🐇

** 7 | 8 |
9 | 10 | Build Status 11 | 12 | 13 | PHP Version Require 14 | 15 | 16 | GitHub license 17 | 18 | 19 |
20 | 21 | # 简介 22 | 23 | 这是一个基于ext-pcntl和ext-posix拓展的PHP多进程助手,用于更方便的调用使用。 24 | 25 | # 快速开始 26 | 27 | ``` 28 | composer require workbunny/process 29 | ``` 30 | 31 | - 创建一个子Runtime 32 | 33 | ```php 34 | // 使用对象方式 35 | $p = new \WorkBunny\Process\Runtime(); 36 | $p->child(function(){ 37 | var_dump('child'); 38 | }); 39 | ``` 40 | 41 | - 父Runtime执行 42 | 43 | ```php 44 | $p = new \WorkBunny\Process\Runtime(); 45 | 46 | $p->parent(function(){ 47 | var_dump('parent'); # 仅输出一次 48 | }); 49 | 50 | ``` 51 | 52 | - 快速创建运行多个子Runtime 53 | 54 | ```php 55 | $p = new \WorkBunny\Process\Runtime(); 56 | 57 | $p->run(function(){ 58 | var_dump('child'); 59 | },function(){ 60 | var_dump('parent'); 61 | }, 4); # 1 + 4 进程 62 | 63 | ``` 64 | 65 | - 监听子Runtime 66 | 67 | ```php 68 | $p = new \WorkBunny\Process\Runtime(); 69 | 70 | $p->wait(function(\WorkBunny\Process\Runtime $parent, int $status){ 71 | # 子进程正常退出则会调用该方法,被调用次数是正常退出的子进程数量 72 | },function(\WorkBunny\Process\Runtime $parent, $status){ 73 | # 子进程异常退出则会调用该方法,被调用次数是异常的子进程数量 74 | }); 75 | ``` 76 | 77 | # 方法 78 | 79 | **注:作用范围为父Runtime的方法仅在父Runtime内有有效响应** 80 | 81 | | 方法名 | 作用范围 | 是否产生分叉 | 描述 | 82 | |:-------------:|:-------------:|:------:|:---------------------------------------:| 83 | | child() | parentContext | √ | 分叉一个子Runtime / 替换一个子Runtime | 84 | | run() | parentContext | √ | 快速分叉N个子Runtime | 85 | | wait() | parentContext | × | 监听所有子Runtime状态 | 86 | | parent() | parentContext | × | 为父Runtime增加回调响应 | 87 | | isChild() | public | × | 判断是否是子Runtime | 88 | | getId() | public | × | 获取当前Runtime序号 | 89 | | getPid() | public | × | 获取当前RuntimePID | 90 | | getPidMap() | parentContext | × | 获取所有子RuntimePID | 91 | | number() | parentContext | × | 获取Runtime数量 or 产生子Runtime自增序号 | 92 | | setConfig() | public | × | 设置config | 93 | | getConfig() | public | × | 获取config | 94 | | getPidMap() | parentContext | × | 获取所有子RuntimePID | 95 | | setPriority() | public | × | 为当前Runtime设置优先级 **需要当前执行用户为super user** | 96 | | getPriority() | public | × | 获取当前Runtime优先级 | 97 | | exit() | public | × | 进程退出 | 98 | 99 | # 说明 100 | 101 | ## 1. 初始化 102 | 103 | - Runtime对象初始化支持配置 104 | - pre_gc :接受bool值,控制Runtime在fork行为发生前是否执行PHP GC;**注:Runtime默认不进行gc** 105 | - priority:接受索引数组,为所有Runtime设置优先级,索引下标对应Runtime序号; 106 | 如实际产生的Runtime数量大于该索引数组数量,则默认为0; 107 | 108 | **注:child()的priority参数会改变该默认值** 109 | 110 | **注:priority需要当前用户为super user** 111 | 112 | ```php 113 | $p = new \WorkBunny\Process\Runtime([ 114 | 'pre_gc' => true, 115 | 'priority' => [ 116 | 0, // 主Runtime优先级为0 117 | -1, // id=1的子Runtime优先级为-1 118 | -2, // id=2的子Runtime优先级为-2 119 | -3 // id=3的子Runtime优先级为-3 120 | ] 121 | ]); 122 | ``` 123 | 124 | ## 2. fork行为 125 | 126 | - 在 **fork** 行为发生后,Runtime对象会产生两个分支 127 | - id=0 的父Runtime 128 | - id=N 的子Runtime 129 | 130 | - **child()** 和 **run()** 之后的代码域会被父子进程同时执行,但相互隔离: 131 | 132 | ```php 133 | $p = new \WorkBunny\Process\Runtime(); 134 | $p->child(function(\WorkBunny\Process\Runtime $runtime){ 135 | var_dump($runtime->getId()); # id !== 0 136 | }); 137 | 138 | var_dump('parent'); # 打印两次 139 | ``` 140 | 141 | ```php 142 | $p = new \WorkBunny\Process\Runtime(); 143 | $p->run(function (\WorkBunny\Process\Runtime $runtime){ 144 | 145 | },function(\WorkBunny\Process\Runtime $runtime){ 146 | 147 | }, 4); 148 | 149 | var_dump('parent'); # 打印5次 150 | ``` 151 | 152 | - **child()** 函数可以进行替换子Runtime行为 153 | 154 | ```php 155 | $p = new \WorkBunny\Process\Runtime(); 156 | 157 | // 创建一个子Runtime 158 | // 假设父RuntimeID === 0,子RuntimeID === 1 159 | // 假设父RuntimePID === 99,子RuntimePID === 100 160 | $id = $p->child(function(\WorkBunny\Process\Runtime $runtime){ 161 | $runtime->getId(); // 假设 id === 1 162 | $runtime->getPid(); // 假设 pid === 100 163 | }); 164 | 165 | if($p->isChild()){ 166 | $id === 0; // $id 在子Runtime的上下文中始终为0 167 | posix_getpid() === 100; 168 | }else{ 169 | $id === 1;// $id 在当前父Runtime的上下文中为1 170 | posix_getpid() === 99; 171 | } 172 | 173 | // 对id === 1的子Runtime进行替换 174 | // 该用法会杀死原id下的子Runtime并新建Runtime替换它 175 | // 该方法并不会改变子Runtime的id,仅改变id对应的pid 176 | $newId = $p->child(function(\WorkBunny\Process\Runtime $runtime){ 177 | $runtime->getId(); # id === 1 178 | }, 0, $id); 179 | 180 | if($p->isChild()){ 181 | $id === $newId === 0; 182 | posix_getpid() !== 100; // 子Runtime PID发生变化,不再是100 183 | // 原PID === 100的子Runtime被kill 184 | }else{ 185 | $id === $newId === 1; // $id 没有发生变化 186 | posix_getpid() === 99; 187 | } 188 | ``` 189 | 190 | - 如需在子Runtime中进行 **fork** 操作,请创建新的Runtime;**不建议过多调用,因为进程的开销远比线程大** 191 | 192 | ```php 193 | $p = new \WorkBunny\Process\Runtime(); 194 | $id = $p->child(function(\WorkBunny\Process\Runtime $runtime){ 195 | var_dump($runtime->getId()); # id !== 0 196 | var_dump('old-child'); 197 | 198 | $newP = new \WorkBunny\Process\Runtime(); 199 | $newP->child(function(\WorkBunny\Process\Runtime $newP){ 200 | var_dump($newP->getId()); # id === 0 201 | var_dump('new-parent'); 202 | }); 203 | }); 204 | # run 方法同理 205 | ``` 206 | 207 | ## 3. 指定执行 208 | 209 | - 指定某个id的Runtime执行 210 | 211 | ```php 212 | $p = new \WorkBunny\Process\Runtime(); 213 | $p->run(function (){},function(){}, 4); 214 | 215 | if($p->getId() === 3){ 216 | var_dump('im No. 3'); # 仅id为3的Runtime会生效 217 | } 218 | 219 | # fork同理 220 | ``` 221 | 222 | - 指定所有子Runtime执行 223 | 224 | ```php 225 | $p = new \WorkBunny\Process\Runtime(); 226 | $p->run(function (){},function(){}, 4); 227 | 228 | if($p->isChild()){ 229 | var_dump('im child'); # 所有子Runtime都生效 230 | } 231 | 232 | # fork同理 233 | ``` 234 | 235 | - 指定父Runtime执行 236 | 237 | ```php 238 | $p = new \WorkBunny\Process\Runtime(); 239 | $p->run(function (){},function(){}, 4); 240 | 241 | if(!$p->isChild()){ 242 | var_dump('im parent'); # 父Runtime都生效 243 | } 244 | 245 | # 或以注册回调函数来执行 246 | $p->parent(function(\WorkBunny\Process\Runtime $parent){ 247 | var_dump('im parent'); 248 | }); 249 | 250 | # fork同理 251 | ``` 252 | 253 | ## 4. 回调函数相关 254 | 255 | - 所有注册的回调函数都可以接收当前的Runtime分支对象: 256 | 257 | ```php 258 | $p = new \WorkBunny\Process\Runtime(); 259 | $p->child(function(\WorkBunny\Process\Runtime $runtime){ 260 | var_dump($runtime->getId()); # id !== 0 261 | }); 262 | $p->parent(function (\WorkBunny\Process\Runtime $runtime){ 263 | var_dump($runtime->getId()); # id === 0 264 | }); 265 | 266 | $p->run(function (\WorkBunny\Process\Runtime $runtime){ 267 | var_dump($runtime->getId()); # id !== 0 268 | },function(\WorkBunny\Process\Runtime $runtime){ 269 | var_dump($runtime->getId()); # id === 0 270 | }, 4); 271 | ``` 272 | 273 | - **注:注册的父Runtime回调函数内传入的是父Runtime对象,注册的子Runtime回调函数内传入的参数是子Runtime对象** 274 | 275 | ```php 276 | $p = new \WorkBunny\Process\Runtime(); 277 | $p->child(function(\WorkBunny\Process\Runtime $runtime){ 278 | var_dump('child'); # 生效 279 | 280 | $runtime->child(function(){ 281 | var_dump('child-child'); # 由于fork作用范围为父Runtime,所以不生效 282 | }); 283 | }); 284 | 285 | $p->parent(function (\WorkBunny\Process\Runtime $runtime){ 286 | var_dump('parent'); # 生效 287 | 288 | $runtime->child(function(){ 289 | var_dump('parent-child'); # 生效 290 | }); 291 | }); 292 | 293 | # run 方法同理 294 | ``` 295 | 296 | ## 5. 其他 297 | 298 | - 获取当前Runtime数量 299 | 300 | **注:该方法仅父Runtime生效** 301 | 302 | ```php 303 | $p = new \WorkBunny\Process\Runtime(); 304 | var_dump($p->number(false)); # 仅父Runtime会输出 305 | ``` 306 | 307 | - 获取当前RuntimePID 308 | 309 | **注:该方法可结合指定执行区别获取** 310 | 311 | ```php 312 | $p = new \WorkBunny\Process\Runtime(); 313 | var_dump($p->getPid()); # 所有Runtime会输出 314 | ``` 315 | 316 | - 阻塞监听 317 | 318 | **注:该方法仅父Runtime生效** 319 | 320 | **注:该方法在会阻塞至所有子Runtime退出** 321 | 322 | ```php 323 | $p = new \WorkBunny\Process\Runtime(); 324 | 325 | // $id RuntimeID 326 | // $pid 进程PID 327 | // $status 进程退出状态 328 | $p->wait(function($id, $pid, $status){ 329 | # 子Runtime正常退出时 330 | }, function($id, $pid, $status){ 331 | # 子Runtime异常退出时 332 | }); 333 | ``` 334 | 335 | - 非阻塞监听 336 | 337 | **注:该方法仅父Runtime生效** 338 | 339 | **注:该方法应配合event-loop的timer或者future进行监听** 340 | 341 | ```php 342 | $p = new \WorkBunny\Process\Runtime(); 343 | 344 | // $id RuntimeID 345 | // $pid 进程PID 346 | // $status 进程退出状态 347 | $p->listen(function($id, $pid, $status){ 348 | # 子Runtime正常退出时 349 | }, function($id, $pid, $status){ 350 | # 子Runtime异常退出时 351 | }); 352 | ``` 353 | 354 | - 进程退出 355 | 356 | **注:该方法可结合指定执行区别获取** 357 | 358 | ```php 359 | $p = new \WorkBunny\Process\Runtime(); 360 | 361 | $p->exit(0, 'success'); 362 | ``` 363 | -------------------------------------------------------------------------------- /src/Runtime.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright chaz6chez 9 | * @link https://github.com/workbunny/process 10 | * @license https://github.com/workbunny/process/blob/main/LICENSE 11 | */ 12 | namespace WorkBunny\Process; 13 | 14 | use Closure; 15 | use InvalidArgumentException; 16 | use RuntimeException; 17 | 18 | class Runtime 19 | { 20 | /** @var int 序号 */ 21 | protected int $_id = 0; 22 | 23 | /** @var int PID */ 24 | protected int $_pid = 0; 25 | 26 | /** @var int 数目 */ 27 | protected int $_number = 1; 28 | 29 | /** @var array 子Runtime PID字典 在子Runtime内始终为[] */ 30 | protected array $_pidMap = []; 31 | 32 | /** 33 | * @see Runtime::__construct 34 | * @var array 配置 35 | */ 36 | protected array $_config = []; 37 | 38 | /** 39 | * @param array $config = [ 40 | * 'pre_gc' => true, // 是否在fork前执行php garbage cycle,默认为false 41 | * 42 | * 'priority' => [0,1,1,2,3], // 设置默认的Runtime优先级,如果创建Runtime数量大于该配置数量,默认为0 43 | * ] 44 | */ 45 | public function __construct(array $config = []) 46 | { 47 | $this->_config = $config; 48 | } 49 | 50 | /** 51 | * @see Runtime::__construct 52 | * @param array $config 53 | * @return void 54 | */ 55 | public function setConfig(array $config): void 56 | { 57 | $this->_config = $config; 58 | } 59 | 60 | /** 61 | * @see Runtime::__construct 62 | * @return array 63 | */ 64 | public function getConfig(): array 65 | { 66 | return $this->_config; 67 | } 68 | 69 | /** 70 | * 获取当前Runtime序号 71 | * @return int 72 | */ 73 | public function getId(): int 74 | { 75 | return $this->_id; 76 | } 77 | 78 | /** 79 | * 获取当前Runtime PID 80 | * @return int 81 | */ 82 | public function getPid(): int 83 | { 84 | return $this->_pid; 85 | } 86 | 87 | /** 88 | * @param array $pidMap 89 | * @return void 90 | */ 91 | public function setPidMap(array $pidMap): void 92 | { 93 | $this->_pidMap = $pidMap; 94 | } 95 | 96 | /** 97 | * 获取所有子Runtime的PID 98 | * @return array 99 | */ 100 | public function getPidMap(): array 101 | { 102 | return $this->_pidMap; 103 | } 104 | 105 | /** 106 | * 是否是子Runtime 107 | * @return bool 108 | */ 109 | public function isChild(): bool 110 | { 111 | return $this->getId() !== 0; 112 | } 113 | 114 | /** 115 | * 返回编号/数量 116 | * @param bool $increment 自增 117 | * @return int 子Runtime会固定返回0 118 | */ 119 | public function number(bool $increment = true): int 120 | { 121 | if(!$this->isChild()){ 122 | return $increment ? $this->_number ++ : $this->_number; 123 | } 124 | return 0; 125 | } 126 | 127 | /** 128 | * @param int $status 129 | * @param string|null $msg 130 | * @return void 131 | */ 132 | public function exit(int $status = 0, ?string $msg = null){ 133 | if($msg){ 134 | $this->echo($msg . PHP_EOL); 135 | } 136 | exit($status); 137 | } 138 | 139 | /** 140 | * @return void 141 | */ 142 | public function exitChildren(): void 143 | { 144 | if($this->isChild()){ 145 | $this->exit(); 146 | } 147 | } 148 | 149 | /** 150 | * @param string $msg 151 | * @return void 152 | */ 153 | public function echo(string $msg){ 154 | echo $msg; 155 | } 156 | 157 | /** 158 | * 快速执行 159 | * 160 | * 该方法下父子Runtime没有优先级区分,除非自行设置,否则始终为0 161 | * @param Closure $childContext = function(Runtime){} 162 | * @param Closure|null $parentContext = function(Runtime){} 163 | * @param int $forkCount 164 | * @return void 165 | */ 166 | public function run(Closure $childContext, ?Closure $parentContext = null, int $forkCount = 1): void 167 | { 168 | if($forkCount < 1){ 169 | throw new InvalidArgumentException('Fork count cannot be less than 1. '); 170 | } 171 | 172 | for($i = 1; $i <= $forkCount; $i ++){ 173 | $this->child($childContext); 174 | } 175 | 176 | $this->parent($parentContext); 177 | } 178 | 179 | /** 180 | * 父Runtime 阻塞监听 181 | * @param Closure|null $success = function(id, pid, status){} 182 | * @param Closure|null $error = function(id, pid, status){} 183 | * @param bool $exitChild 是否结束子runtime 184 | * @return void 185 | */ 186 | public function wait(?Closure $success = null, ?Closure $error = null, bool $exitChild = false): void 187 | { 188 | if(!$this->isChild()){ 189 | foreach ($this->_pidMap as $id => $pid){ 190 | $pid = pcntl_waitpid($pid, $status, WUNTRACED); 191 | if($pid > 0){ 192 | if ($status !== 0) { 193 | if ($error){ 194 | $error($id, $pid, $status); 195 | } 196 | }else{ 197 | if($success){ 198 | $success($id, $pid, $status); 199 | } 200 | } 201 | } 202 | } 203 | } 204 | if($exitChild){ 205 | $this->exitChildren(); 206 | } 207 | } 208 | 209 | /** 210 | * 父Runtime 非阻塞监听 211 | * @param Closure|null $success = function(id, pid, status){} 212 | * @param Closure|null $error = function(id, pid, status){} 213 | * @param bool $exitChild 是否结束子runtime 214 | * @return void 215 | */ 216 | public function listen(?Closure $success = null, ?Closure $error = null, bool $exitChild = false): void 217 | { 218 | if(!$this->isChild()){ 219 | foreach ($this->_pidMap as $id => $pid){ 220 | $pid = pcntl_waitpid($pid, $status, WNOHANG); 221 | if($pid > 0){ 222 | if ($status !== 0) { 223 | if ($error){ 224 | $error($id, $pid, $status); 225 | } 226 | }else{ 227 | if($success){ 228 | $success($id, $pid, $status); 229 | } 230 | } 231 | } 232 | } 233 | } 234 | if($exitChild){ 235 | $this->exitChildren(); 236 | } 237 | } 238 | 239 | /** 240 | * 父Runtime执行 241 | * @param Closure|null $context = function(Runtime){} 242 | * @return void 243 | */ 244 | public function parent(?Closure $context = null): void 245 | { 246 | if(!$this->isChild() and $context){ 247 | $context($this); 248 | } 249 | } 250 | 251 | /** 252 | * 创建一个子Runtime 253 | * @param Closure|null $context = function(Runtime){} 254 | * @param int $priority 默认 父子Runtime同为0,但父Runtime始终为0 255 | * @param int|null $id 该id的子Runtime如果存在则会创建新Runtime替换旧Runtime 256 | * @return int 257 | */ 258 | public function child(?Closure $context = null, int $priority = 0, ?int $id = null): int 259 | { 260 | // 创建子Runtime 261 | if($id = ($id !== null) ? $id : $this->number()){ 262 | // gc 263 | if(isset($this->getConfig()['pre_gc']) and boolval($this->getConfig()['pre_gc'])){ 264 | gc_collect_cycles(); 265 | } 266 | // fork 267 | $pid = pcntl_fork(); # 此代码往后就是父子进程公用代码块 268 | try { 269 | switch (true){ 270 | // 父Runtime 271 | case $pid > 0: 272 | $this->_id = 0; 273 | $this->_pid = posix_getpid(); 274 | $this->setPriority( 275 | 0, 276 | isset($this->getConfig()['priority'][0]) 277 | ? (int)($this->getConfig()['priority'][0]) 278 | : 0 279 | ); 280 | // 杀死旧Runtime 281 | if($oldPid = ($this->_pidMap[$id] ?? null)){ 282 | posix_kill($oldPid, SIGKILL); 283 | } 284 | $this->_pidMap[$id] = $pid; 285 | break; 286 | // 子Runtime 287 | case $pid === 0: 288 | $this->_id = $id; 289 | $this->_pid = posix_getpid(); 290 | $this->setPriority( 291 | $id, 292 | isset($this->getConfig()['priority'][$id]) 293 | ? (int)($this->getConfig()['priority'][$id]) 294 | : $priority 295 | ); 296 | $this->_pidMap = []; 297 | if($context){ 298 | $context($this); 299 | } 300 | break; 301 | // 异常 302 | default: 303 | throw new RuntimeException('Fork process fail. '); 304 | } 305 | }catch (\Throwable $throwable){ 306 | $this->exit(250, $throwable->getMessage()); 307 | } 308 | } 309 | return $id; 310 | } 311 | 312 | /** 313 | * 为Runtime设置优先级 314 | * @param int $id Runtime序号 0为主进程 315 | * @param int $priority 优先级 -20至20 越小优先级越高 316 | * @return void 317 | */ 318 | public function setPriority(int $id, int $priority): void 319 | { 320 | if($this->getId() === $id){ 321 | @pcntl_setpriority($priority); 322 | } 323 | } 324 | 325 | /** 326 | * 获取Runtime优先级 327 | * @param int $id Runtime序号 0为主进程 328 | * @return int|null -20至20 越小优先级越高 329 | */ 330 | public function getPriority(int $id): ?int 331 | { 332 | if($this->getId() === $id){ 333 | return ($priority = pcntl_getpriority()) === false ? null : $priority; 334 | } 335 | return null; 336 | } 337 | } --------------------------------------------------------------------------------