├── 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 |

3 |
4 | **workbunny/process
**
5 |
6 | **🐇 A lightweight multi-process helper base on PHP. 🐇
**
7 |
8 |
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 | }
--------------------------------------------------------------------------------