Skip to content

Commit cc96914

Browse files
authored
Merge pull request #82 from botuniverse/feature/coroutine-support
协程支持(并发布 0.5.1)
2 parents 76f88a3 + a2f0b75 commit cc96914

10 files changed

+202
-30
lines changed

src/OneBot/Driver/Coroutine/Adaptive.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ class Adaptive
2727
public static function initWithDriver(Driver $driver)
2828
{
2929
if ($driver->getName() === 'swoole') {
30-
self::$coroutine = SwooleCoroutine::getInstance();
30+
self::$coroutine = SwooleCoroutine::getInstance($driver);
3131
} elseif ($driver->getName() === 'workerman' && PHP_VERSION_ID >= 80100) {
3232
// 只有 PHP >= 8.1 才能使用 Fiber 协程接口
33-
self::$coroutine = FiberCoroutine::getInstance();
33+
self::$coroutine = FiberCoroutine::getInstance($driver);
3434
}
3535
}
3636

src/OneBot/Driver/Coroutine/CoroutineInterface.php

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@
44

55
namespace OneBot\Driver\Coroutine;
66

7+
use OneBot\Driver\Process\ExecutionResult;
8+
79
interface CoroutineInterface
810
{
9-
public static function getInstance(...$arg);
11+
/**
12+
* 返回当前协程实现是否可以使用
13+
*/
14+
public static function isAvailable(): bool;
1015

1116
/**
1217
* 创建一个协程并运行
@@ -18,6 +23,8 @@ public static function getInstance(...$arg);
1823
*/
1924
public function create(callable $callback, ...$args): int;
2025

26+
public function exists(int $cid): bool;
27+
2128
/**
2229
* 挂起当前协程
2330
*
@@ -28,14 +35,30 @@ public function suspend();
2835
/**
2936
* 根据提供的协程 ID 恢复一个协程继续运行
3037
*
31-
* @param int $cid 协程 ID
32-
* @param mixed $value 要传给 suspend 返回值的内容
33-
* @return mixed
38+
* @param int $cid 协程 ID
39+
* @param mixed $value 要传给 suspend 返回值的内容
40+
* @return false|int
3441
*/
3542
public function resume(int $cid, $value = null);
3643

3744
/**
3845
* 获取当前协程 ID
3946
*/
4047
public function getCid(): int;
48+
49+
/**
50+
* @param float|int $time 协程 sleep
51+
*/
52+
public function sleep($time);
53+
54+
/**
55+
* 协程执行命令行
56+
* @param string $cmd 命令行
57+
*/
58+
public function exec(string $cmd): ExecutionResult;
59+
60+
/**
61+
* 获取正在运行的协程数量
62+
*/
63+
public function getCount(): int;
4164
}

src/OneBot/Driver/Coroutine/FiberCoroutine.php

Lines changed: 106 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,39 @@
44

55
namespace OneBot\Driver\Coroutine;
66

7+
use OneBot\Driver\Driver;
8+
use OneBot\Driver\Process\ExecutionResult;
79
use OneBot\Util\Singleton;
810

911
class FiberCoroutine implements CoroutineInterface
1012
{
1113
use Singleton;
1214

13-
/** @var \SplStack */
14-
private static $fiber_stacks;
15+
private static ?\SplStack $fiber_stacks = null;
1516

1617
/** @var array<int, \Fiber> */
17-
private static $suspended_fiber_map = [];
18+
private static array $suspended_fiber_map = [];
1819

20+
private Driver $driver;
21+
22+
public function __construct(Driver $driver)
23+
{
24+
$this->driver = $driver;
25+
}
26+
27+
public static function isAvailable(): bool
28+
{
29+
return PHP_VERSION_ID >= 80100;
30+
}
31+
32+
/**
33+
* @throws \Throwable
34+
* @throws \RuntimeException
35+
*/
1936
public function create(callable $callback, ...$args): int
2037
{
2138
if (PHP_VERSION_ID < 80100) {
22-
throw new \Exception('You need PHP >= 8.1 to enable Fiber feature!');
39+
throw new \RuntimeException('You need PHP >= 8.1 to enable Fiber feature!');
2340
}
2441
$fiber = new \Fiber($callback);
2542

@@ -37,38 +54,119 @@ public function create(callable $callback, ...$args): int
3754
return $id;
3855
}
3956

57+
public function exists(int $cid): bool
58+
{
59+
return isset(self::$suspended_fiber_map[$cid]);
60+
}
61+
62+
/**
63+
* @throws \Throwable
64+
* @throws \RuntimeException
65+
*/
4066
public function suspend()
4167
{
4268
if (PHP_VERSION_ID < 80100) {
43-
throw new \Exception('You need PHP >= 8.1 to enable Fiber feature!');
69+
throw new \RuntimeException('You need PHP >= 8.1 to enable Fiber feature!');
4470
}
4571
return \Fiber::suspend();
4672
}
4773

74+
/**
75+
* @param null|mixed $value
76+
* @throws \RuntimeException
77+
* @throws \Throwable
78+
* @return false|int
79+
*/
4880
public function resume(int $cid, $value = null)
4981
{
5082
if (PHP_VERSION_ID < 80100) {
51-
throw new \Exception('You need PHP >= 8.1 to enable Fiber feature!');
83+
throw new \RuntimeException('You need PHP >= 8.1 to enable Fiber feature!');
5284
}
5385
if (!isset(self::$suspended_fiber_map[$cid])) {
54-
ob_logger()->error('ID ' . $cid . ' Fiber not suspended!');
55-
return;
86+
return false;
5687
}
5788
self::$fiber_stacks->push(self::$suspended_fiber_map[$cid]);
89+
if (self::$suspended_fiber_map[$cid]->isSuspended()) {
90+
echo "(I have been suspended)\n";
91+
} else {
92+
echo "[I am not been suspended]\n";
93+
}
94+
debug_print_backtrace();
5895
self::$suspended_fiber_map[$cid]->resume($value);
5996
self::$fiber_stacks->pop();
6097
if (self::$suspended_fiber_map[$cid]->isTerminated()) {
6198
unset(self::$suspended_fiber_map[$cid]);
6299
}
100+
return $cid;
63101
}
64102

65103
public function getCid(): int
66104
{
67105
try {
68106
$v = self::$fiber_stacks->pop();
107+
self::$fiber_stacks->push($v);
69108
} catch (\RuntimeException $e) {
70109
return -1;
71110
}
72111
return spl_object_id($v);
73112
}
113+
114+
/**
115+
* @param mixed $time
116+
* @throws \Throwable
117+
* @throws \RuntimeException
118+
*/
119+
public function sleep($time)
120+
{
121+
if (($cid = $this->getCid()) !== -1) {
122+
$this->driver->getEventLoop()->addTimer($time * 1000, function () use ($cid) {
123+
$this->resume($cid);
124+
});
125+
$this->suspend();
126+
return;
127+
}
128+
129+
usleep($time * 1000 * 1000);
130+
}
131+
132+
/**
133+
* @throws \Throwable
134+
* @throws \RuntimeException
135+
*/
136+
public function exec(string $cmd): ExecutionResult
137+
{
138+
if (($cid = $this->getCid()) !== -1) {
139+
$descriptorspec = [
140+
0 => ['pipe', 'r'], // 标准输入,子进程从此管道中读取数据
141+
1 => ['pipe', 'w'], // 标准输出,子进程向此管道中写入数据
142+
2 => STDERR, // 标准错误
143+
];
144+
$res = proc_open($cmd, $descriptorspec, $pipes, getcwd());
145+
if (is_resource($res)) {
146+
$this->driver->getEventLoop()->addReadEvent($pipes[1], function ($x) use ($cid, $res, $pipes) {
147+
$stdout = stream_get_contents($x);
148+
$status = proc_get_status($res);
149+
$this->driver->getEventLoop()->delReadEvent($x);
150+
if ($status['exitcode'] !== -1) {
151+
fclose($x);
152+
fclose($pipes[0]);
153+
$out = new ExecutionResult($status['exitcode'], $stdout);
154+
} else {
155+
$out = new ExecutionResult(-1);
156+
}
157+
$this->resume($cid, $out);
158+
});
159+
return $this->suspend();
160+
}
161+
throw new \RuntimeException('Cannot open process with command ' . $cmd);
162+
}
163+
164+
exec($cmd, $output, $code);
165+
return new ExecutionResult($code, $output);
166+
}
167+
168+
public function getCount(): int
169+
{
170+
return self::$fiber_stacks->count();
171+
}
74172
}

src/OneBot/Driver/Coroutine/SwooleCoroutine.php

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,20 @@
44

55
namespace OneBot\Driver\Coroutine;
66

7+
use OneBot\Driver\Process\ExecutionResult;
78
use OneBot\Util\Singleton;
89
use Swoole\Coroutine;
910

1011
class SwooleCoroutine implements CoroutineInterface
1112
{
1213
use Singleton;
1314

14-
private static $resume_values = [];
15+
private static array $resume_values = [];
16+
17+
public static function isAvailable(): bool
18+
{
19+
return extension_loaded('swoole') || extension_loaded('openswoole');
20+
}
1521

1622
public function create(callable $callback, ...$args): int
1723
{
@@ -20,7 +26,7 @@ public function create(callable $callback, ...$args): int
2026

2127
public function suspend()
2228
{
23-
Coroutine::yield();
29+
Coroutine::suspend();
2430
if (isset(self::$resume_values[$this->getCid()])) {
2531
$value = self::$resume_values[$this->getCid()];
2632
unset(self::$resume_values[$this->getCid()]);
@@ -29,19 +35,39 @@ public function suspend()
2935
return null;
3036
}
3137

38+
public function exists(int $cid): bool
39+
{
40+
return Coroutine::exists($cid);
41+
}
42+
3243
public function resume(int $cid, $value = null)
3344
{
3445
if (Coroutine::exists($cid)) {
3546
self::$resume_values[$cid] = $value;
3647
Coroutine::resume($cid);
3748
return $cid;
3849
}
39-
ob_logger()->error('Swoole coroutine #' . $cid . ' not exists');
4050
return false;
4151
}
4252

4353
public function getCid(): int
4454
{
4555
return Coroutine::getCid();
4656
}
57+
58+
public function sleep($time)
59+
{
60+
Coroutine::sleep($time);
61+
}
62+
63+
public function exec(string $cmd): ExecutionResult
64+
{
65+
$result = Coroutine\System::exec($cmd);
66+
return new ExecutionResult($result['code'], $result['output']);
67+
}
68+
69+
public function getCount(): int
70+
{
71+
return Coroutine::stats()['coroutine_num'];
72+
}
4773
}

src/OneBot/Driver/Event/EventDispatcher.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace OneBot\Driver\Event;
66

7+
use OneBot\Driver\Coroutine\Adaptive;
78
use OneBot\Driver\Interfaces\HandledDispatcherInterface;
89
use OneBot\Exception\ExceptionHandler;
910

@@ -13,8 +14,13 @@ class EventDispatcher implements HandledDispatcherInterface
1314
/**
1415
* 分发事件
1516
*/
16-
public function dispatch(object $event): object
17+
public function dispatch(object $event, bool $inside = false): object
1718
{
19+
if (($co = Adaptive::getCoroutine()) !== null && !$inside) {
20+
$co->create([$this, 'dispatch'], $event, true);
21+
return $event;
22+
}
23+
ob_logger()->warning('Dispatching event in fiber: ' . $co->getCid());
1824
foreach (ob_event_provider()->getEventListeners($event->getName()) as $listener) {
1925
try {
2026
// TODO: 允许 Listener 修改 $event

src/OneBot/Driver/Swoole/TopEventListener.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Choir\Http\HttpFactory;
88
use Choir\WebSocket\FrameInterface;
9+
use OneBot\Driver\Coroutine\Adaptive;
910
use OneBot\Driver\Event\Http\HttpRequestEvent;
1011
use OneBot\Driver\Event\Process\ManagerStartEvent;
1112
use OneBot\Driver\Event\Process\ManagerStopEvent;
@@ -38,6 +39,7 @@ public function onWorkerStart(Server $server)
3839
} else {
3940
ProcessManager::initProcess(ONEBOT_PROCESS_WORKER, $server->worker_id);
4041
}
42+
Adaptive::initWithDriver(SwooleDriver::getInstance());
4143
ob_event_dispatcher()->dispatchWithHandler(new WorkerStartEvent());
4244
}
4345

src/OneBot/Driver/Workerman/TopEventListener.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Choir\Http\HttpFactory;
88
use Choir\WebSocket\FrameFactory;
99
use Choir\WebSocket\FrameInterface;
10+
use OneBot\Driver\Coroutine\Adaptive;
1011
use OneBot\Driver\Event\Http\HttpRequestEvent;
1112
use OneBot\Driver\Event\Process\WorkerStartEvent;
1213
use OneBot\Driver\Event\Process\WorkerStopEvent;
@@ -32,6 +33,7 @@ class TopEventListener
3233
public function onWorkerStart(Worker $worker)
3334
{
3435
ProcessManager::initProcess(ONEBOT_PROCESS_WORKER, $worker->id);
36+
Adaptive::initWithDriver(WorkermanDriver::getInstance());
3537
ob_event_dispatcher()->dispatchWithHandler(new WorkerStartEvent());
3638
}
3739

0 commit comments

Comments
 (0)