EasySwoole的ContextManager的分析和使用
ContextManager主要用来实现协程上下文的隔离,框架中实现隔离的原理简单粗暴,easyswoole使用了进程粒度的单例ContextManager将不同协程下的变量,以各协程为粒度,存储在各自协程id下,最终形式就是二维数组,直观感觉就是将各协程的上下文隔离开来了。
easyswoole的上下文管理器还实现了类似懒加载和协程退出的hook机制,核心代码就是curd,大致如下。
<?php
namespace EasySwooleComponentContext;
use EasySwooleComponentContextExceptionModifyError;
use EasySwooleComponentSingleton;
use SwooleCoroutine;
class ContextManager
{
use Singleton;
private $contextHandler = [];
private $context = [];
private $deferList = [];
// 为指定key注册handler,在对应时机hook
public function registerItemHandler($key, ContextItemHandlerInterface $handler): ContextManager
{
$this->contextHandler[$key] = $handler;
return $this;
}
// 向manager中注册
public function set($key, $value, $cid = null): ContextManager
{
if (isset($this->contextHandler[$key])) {
throw new ModifyError('key is already been register for context item handler');
}
// 获取协程id
$cid = $this->getCid($cid);
// es的contextmanager 在写方法中也提供了cid参数,也就说我们可以跨协程修改其他协程的上下文了
// 这种方式灵活性可能更高 相对的提高了开发负担
$this->context[$cid][$key] = $value;
return $this;
}
public function get($key, $cid = null)
{
$cid = $this->getCid($cid);
if (isset($this->context[$cid][$key])) {
return $this->context[$cid][$key];
}
if (isset($this->contextHandler[$key])) {
/** @var ContextItemHandlerInterface $handler */
// 调用注册的handeler的onContextCreate方法
// 实现类似懒加载机制
$handler = $this->contextHandler[$key];
$this->context[$cid][$key] = $handler->onContextCreate();
return $this->context[$cid][$key];
}
return null;
}
public function unset($key, $cid = null)
{
// 删除写操作同样提供了cid参数
$cid = $this->getCid($cid);
if (isset($this->context[$cid][$key])) {
if (isset($this->contextHandler[$key])) {
/** @var ContextItemHandlerInterface $handler */
// 执行注册的onDestroy方法
$handler = $this->contextHandler[$key];
$item = $this->context[$cid][$key];
unset($this->context[$cid][$key]);
return $handler->onDestroy($item);
}
unset($this->context[$cid][$key]);
return true;
} else {
return false;
}
}
public function destroy($cid = null)
{
$cid = $this->getCid($cid);
if (isset($this->context[$cid])) {
$data = $this->context[$cid];
foreach ($data as $key => $val) {
$this->unset($key, $cid);
}
}
unset($this->context[$cid]);
}
// 值得注意的是每个方法都调用了getCid方法 也就是说执行了manager的任何方法都会注册defer进行,
// 从而执行清理工作
public function getCid($cid = null): int
{
if ($cid === null) {
// 如果没指定cid 那么获取当前协程id
$cid = Coroutine::getUid();
// 如果deferList中不存在对应的cid 并且cid合法
// 那么就注册defer 在协程退出时清除指定的协程上下文
if (!isset($this->deferList[$cid]) && $cid > 0) {
$this->deferList[$cid] = true;
Coroutine::defer(function () use ($cid) {
unset($this->deferList[$cid]);
$this->destroy($cid);
});
}
return $cid;
}
return $cid;
}
public function destroyAll($force = false)
{
if ($force) {
$this->context = [];
} else {
foreach ($this->context as $cid => $data) {
$this->destroy($cid);
}
}
}
public function getContextArray($cid = null): ?array
{
$cid = $this->getCid($cid);
if (isset($this->context[$cid])) {
return $this->context[$cid];
} else {
return null;
}
}
}
# 不知道作者出于什么考量,所有写操作同样允许传入cid,有的竞品框架此部分功能并不支持类似功能
# 至于为什么提供了onDestroy功能,es官方给的说法是可以执行类似回收资源的操作。
# 但这种操作这无非是将回收的执行逻辑放到了不同位置的defer中,类似回收资源的逻辑完全可以在取出连接处处理,或者连接池直接支持回收功能。
public function ctxSetGet()
{
$pcid = Coroutine::getCid();
ContextManager::getInstance()->set('parent', 'parent');
go(function () use ($pcid) {
$parentValue = ContextManager::getInstance()->get('parent', $pcid);
var_dump($parentValue); // parent
ContextManager::getInstance()->set('parent', 'modified by sub', $pcid);
});
Coroutine::sleep(0.1);
# 可以看到跨协程修改了数据
$v = ContextManager::getInstance()->get('parent');
echo $v, PHP_EOL; // modified by sub
}
# 此案例抄自es官网
class Handler implements ContextItemHandlerInterface
{
function onContextCreate()
{
$class = new stdClass();
$class->time = time();
return $class;
}
function onDestroy($context)
{
var_dump($context);
}
}
ContextManager::getInstance()->registerItemHandler('key',new Handler());
go(function (){
go(function (){
ContextManager::getInstance()->get('key');
});
co::sleep(1);
ContextManager::getInstance()->get('key');
});
实际上,swoole在4.3后提供了原生context api 返回值为ArrayObject类型,方便各位一顿操作,协程退出后上下文自动清理,具体细节可自行查看文档。easyswoole历史原因并没有使用swoole原生context实现上下文管理。这里是我写的包,非常简单,胆子大的可生产使用。
发现错误,欢迎指正,感谢!!!