使用 Swoole 协程 特性,请认真阅读本章节编程须知。
协程内部禁止使用全局变量
协程使用 use 关键字引入外部变量到当前作用域禁止使用引用
协程之间通讯必须使用 Channel
即协程之间通讯不要使用全局变量或者引用外部变量到当前作用域,而要使用 Channel
项目中如果有扩展 hook 了 zend_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 退出
Swoole v4.1.0 版本及以上直接支持了在协程、服务事件循环中使用 PHP 的 exit,此时底层会自动抛出一个可捕获的 Swoole\ExitException,开发者可以在需要的位置捕获并实现与原生 PHP 一样的退出逻辑。
Swoole\ExitException 继承于 Exception 且新增了两个方法 getStatus 和 getFlags:
namespace Swoole;
class ExitException extends \Exception
{
public function getStatus(): mixed
public function getFlags(): int
}获取 exit ($status) 退出时的传入的 status 参数,支持任意的变量类型。
public function getStatus(): mixed
获取 exit 退出时所处的环境信息掩码。
public function getFlags(): int
目前有以下掩码:
| 常量 | 说明 |
|---|---|
| SWOOLE_EXIT_IN_COROUTINE | 协程中退出 |
| SWOOLE_EXIT_IN_SERVER | Server 中退出 |
use Swoole\Coroutine;
use function Swoole\Coroutine\run;
function route()
{
controller();
}
function controller()
{
your_code();
}
function your_code()
{
Coroutine::sleep(.001);
exit(1);
}
run(function () {
try {
route();
} catch (\Swoole\ExitException $e) {
var_dump($e->getMessage());
var_dump($e->getStatus() === 1);
var_dump($e->getFlags() === SWOOLE_EXIT_IN_COROUTINE);
}
});use function Swoole\Coroutine\run;
$exit_status = 0;
run(function () {
try {
exit(123);
} catch (\Swoole\ExitException $e) {
global $exit_status;
$exit_status = $e->getStatus();
}
});
var_dump($exit_status);在协程编程中可直接使用 try/catch 处理异常。但必须在协程内捕获,不得跨协程捕获异常。
不仅是应用层 throw 的 Exception,底层的一些错误也是可以被捕获的,如 function、class、method 不存在
下面的代码中,try/catch 和 throw 在不同的协程中,协程内无法捕获到此异常。当协程退出时,发现有未捕获的异常,将引起致命错误。
PHP 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;
}
});原因:参考 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 类中,使用 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]);
}
}
}
}使用:
use Swoole\Coroutine\Context;
$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();