当前位置: 首页 > 文档资料 > Swoole 中文文档 >

协程编程须知

优质
小牛编辑
139浏览
2023-12-01

使用 Swoole 协程 特性,请认真阅读本章节编程须知。

编程范式

  • 协程内部禁止使用全局变量
  • 协程使用use关键字引入外部变量到当前作用域禁止使用引用
  • 协程之间通讯必须使用Channel

!> 即协程之间通讯不要使用全局变量或者引用外部变量到当前作用域,而要使用Channel

  • 项目中如果有扩展hookzend_execute_ex或者zend_execute_internal,需要特别注意一下c栈。可以使用co::set重新设置C栈大小

!> hook这两个入口函数后,大部分情况下会把平坦的php指令调用变为C函数调用,增加c栈的消耗。

退出协程

在Swoole低版本中, 协程中使用exit强行退出脚本会导致内存错误导致不可预期的结果或coredump, 在Swoole服务中使用exit会使整个服务进程退出且内部的协程全部异常终止导致严重问题,Swoole长期以来一直禁止开发者使用exit, 但开发者可以使用抛出异常这种非常规的方式, 在顶层catch来实现和exit相同的退出逻辑

!> v4.2.2版本及以上允许脚本(未创建http_server)在只有当前协程的情况下exit退出

Swoolev4.1.0版本及以上直接支持了在协程服务事件循环中使用PHP的exit,此时底层会自动抛出一个可捕获的Swoole\ExitException, 开发者可以在需要的位置捕获并实现与原生PHP一样的退出逻辑。

Swoole\ExitException

Swoole\ExitException继承于Exception且新增了两个方法getStatusgetFlags:

namespace Swoole;

class ExitException extends \Exception
{
    public function getStatus():mixed
    public function getFlags():int
}

getStatus()

获取exit($status)退出时的传入的status参数, 支持任意的变量类型。

public function getStatus():mixed

getFlags()

获取exit退出时所处的环境信息掩码。

public function getFlags():int

目前有以下掩码:

SWOOLE_EXIT_IN_COROUTINE //协程中退出
SWOOLE_EXIT_IN_SERVER //服务中退出

使用方法

基本使用

function route()
{
    controller();
}

function controller()
{
    your_code();
}

function your_code()
{
    co::sleep(.001);
    exit(1);
}

go(function () {
    try {
        route();
    } catch (\Swoole\ExitException $e) {
        var_dump($e->getMessage());
        var_dump($e->getStatus() === 1);
        var_dump($e->getFlags() === SWOOLE_EXIT_IN_COROUTINE);
    }
});

带状态码的退出

$exit_status = 0;
go(function () {
    try {
        exit(123);
    } catch (\Swoole\ExitException $e) {
        global $exit_status;
        $exit_status = $e->getStatus();
    }
});
swoole_event_wait();
var_dump($exit_status);

异常处理

在协程编程中可直接使用try/catch处理异常。但必须在协程内捕获,不得跨协程捕获异常

!> 不仅是应用层throwException,底层的一些错误也是可以被捕获的,如functionclassmethod不存在

错误示例

下面的代码中,try/catchthrow在不同的协程中,协程内无法捕获到此异常。当协程退出时,发现有未捕获的异常,将引起致命错误。

Fatal error: Uncaught RuntimeException
try {
    Swoole\Coroutine::create(function () {
        throw new \RuntimeException(__FILE__, __LINE__);
    });
}
catch (\Throwable $e) {
    echo $e;
}

正确示例

在协程内捕获异常。

function test() {
    throw new \RuntimeException(__FILE__, __LINE__);
}

Swoole\Coroutine::create(function () {
    try {
        test();
    }
    catch (\Throwable $e) {
        echo $e;
    }
});

get / set 魔术方法里不得产生协程切换

原因:参考PHP7内核剖析

Note: 如果类存在__get()方法,则在实例化对象分配属性内存(即:properties_table)时会多分配一个zval,类型为HashTable,每次调用__get($var)时会把输入的$var名称存入这个哈希表,这样做的目的是防止循环调用,举个例子:

public function __get($var) { return $this->$var; }

这种情况是调用get()时又访问了一个不存在的属性,也就是会在get()方法中递归调用,如果不对请求的$var作判断则将一直递归下去,所以在调用get()前首先会判断当前$var是不是已经在get()中了,如果是则不会再调用__get(),否则会把$var作为key插入那个HashTable,然后将哈希值设置为:guard |= IN_ISSET,调用完__get()再把哈希值设置为:guard &= ~IN_ISSET。

这个HashTable不仅仅是给__get()用的,其它魔术方法也会用到,所以其哈希值类型是zend_long,不同的魔术方法占不同的bit位;其次,并不是所有的对象都会额外分配这个HashTable,在对象创建时会根据 zend_class_entry.ce_flags 是否包含 ZEND_ACC_USE_GUARDS 确定是否分配,在类编译时如果发现定义了get()、set()、unset()、isset()方法则会将ce_flags打上这个掩码。

协程切换出去后,下次调用将会被判断为循环调用,此问题为PHP特性所致,与PHP开发组沟通后仍暂时无解。

建议:自己实现get/set方法显式调用

原始问题链接:#2625

严重错误

以下行为会导致出现严重错误。

在多个协程间共用一个连接

与同步阻塞程序不同,协程是并发处理请求的,因此同一时间可能会有很多个请求在并行处理,一旦共用客户端连接,就会导致不同协程之间发生数据错乱。参考: 多协程共享TCP连接

使用类静态变量/全局变量保存上下文

多个协程是并发执行的,因此不能使用类静态变量/全局变量保存协程上下文内容。使用局部变量是安全的,因为局部变量的值会自动保存在协程栈中,其他协程访问不到协程的局部变量。

错误示例

$server = new Swoole\Http\Server('127.0.0.1', 9501);

$_array = [];
$server->on('request', function ($request, $response) {
    global $_array;
    //请求 /a(协程 1 )
    if ($request->server['request_uri'] == '/a') {
        $_array['name'] = 'a';
        co::sleep(1.0);
        echo $_array['name'];
        $response->end($_array['name']);
    }
    //请求 /b(协程 2 )
    else {
        $_array['name'] = 'b';
        $response->end();
    }
});
$server->start();

发起2个并发请求。

curl http://127.0.0.1:9501/a
curl http://127.0.0.1:9501/b
  • 协程1中设置了全局变量$_array['name']的值为a
  • 协程1调用co::sleep挂起
  • 协程2执行,将$_array['name']的值为b,协程2结束
  • 这时定时器返回,底层恢复协程1的运行。而协程1的逻辑中有一个上下文的依赖关系。当再次打印$_array['name']的值时,程序预期是a,但这个值已经被协程2所修改,实际结果却是b,这样就造成了逻辑错误
  • 同理,使用类静态变量Class::$array、全局对象属性$object->array、其他超全局变量$GLOBALS等,进行上下文保存在协程程序中是非常危险的。可能会出现不符合预期的行为。

正确示例:使用Context管理上下文

可以使用一个Context类来管理协程上下文,在Context类中,使用Coroutine::getUid获取了协程ID,然后隔离不同协程之间的全局变量,协程退出时清理上下文数据

use Swoole\Coroutine;

class Context
{
    protected static $pool = [];

    static function get($key)
    {
        $cid = Coroutine::getuid();
        if ($cid < 0)
        {
            return null;
        }
        if(isset(self::$pool[$cid][$key])){
            return self::$pool[$cid][$key];
        }
        return null;
    }

    static function put($key, $item)
    {
        $cid = Coroutine::getuid();
        if ($cid > 0)
        {
            self::$pool[$cid][$key] = $item;
        }

    }

    static function delete($key = null)
    {
        $cid = Coroutine::getuid();
        if ($cid > 0)
        {
            if($key){
                unset(self::$pool[$cid][$key]);
            }else{
                unset(self::$pool[$cid]);
            }
        }
    }
}

使用:

$server = new Swoole\Http\Server('127.0.0.1', 9501);

$server->on('request', function ($request, $response) {
    if ($request->server['request_uri'] == '/a') {
        Context::put('name', 'a');
        co::sleep(1.0);
        echo Context::get('name');
        $response->end(Context::get('name'));
        //退出协程时清理
        Context::delete('name');
    } else {
        Context::put('name', 'b');
        $response->end();
        //退出协程时清理
        Context::delete();
    }
});
$server->start();