EasySwoole的ContextManager的分析和使用

ContextManager主要用来实现协程上下文的隔离,框架中实现隔离的原理简单粗暴,easyswoole使用了进程粒度的单例ContextManager将不同协程下的变量,以各协程为粒度,存储在各自协程id下,最终形式就是二维数组,直观感觉就是将各协程的上下文隔离开来了。

easyswoole的上下文管理器还实现了类似懒加载和协程退出的hook机制,核心代码就是curd,大致如下。

简单讲解

简单应用

分析

<?php

namespace EasySwoole\Component\Context;

use EasySwoole\Component\Context\Exception\ModifyError;
use EasySwoole\Component\Singleton;
use Swoole\Coroutine;

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实现上下文管理。这里是我写的包,非常简单,胆子大的可生产使用。

发现错误,欢迎指正,感谢!!!

版权声明:本文为alwayslinger原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/alwayslinger/p/13969731.html