环境:
[root@localhost data]# php -v
PHP 7.2.12 (cli) (built: Apr 9 2019 02:42:57) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
[root@localhost data]#
[root@localhost data]# php --ri swoole
swoole
Swoole => enabled
Author => Swoole Team <[email protected]>
Version => 4.3.4
Built => May 22 2019 06:25:45
coroutine => enabled
epoll => enabled
eventfd => enabled
signalfd => enabled
cpu_affinity => enabled
spinlock => enabled
rwlock => enabled
openssl => OpenSSL 1.0.2k-fips 26 Jan 2017
http2 => enabled
pcre => enabled
zlib => enabled
mutex_timedlock => enabled
pthread_barrier => enabled
futex => enabled
async_redis => enabled
Directive => Local Value => Master Value
swoole.enable_coroutine => On => On
swoole.display_errors => On => On
swoole.use_shortname => On => On
swoole.unixsock_buffer_size => 8388608 => 8388608
代码:
<?php
class Test
{
public function __get($name)
{
// 协程切换
\Swoole\Coroutine::sleep(1);
\Swoole\Coroutine::sleep(1);
\Swoole\Coroutine::sleep(1);
// 其他hook的redis,pdo操作等会导致协程切换的都会导致
return $name;
}
}
$t = new Test;
for ($i = 0; $i < 3; $i++) {
go(function() use ($t, $i){
$name = "name_$i";
$t->$name; // 当每次属性名称不同时,__get中的协程切换不会有问题
});
}
for ($i = 0; $i < 3; $i++) {
go(function() use ($t, $i){
$t->aaa; // 当并行操作的属性名称相同时,__get中的协程切换会导致并行的下一个 _get 操作 PHP Notice: Undefined property
});
}
执行结果:
[root@localhost data]# php t.php
PHP Notice: Undefined property: Test::$aaa in /data/t.php on line 31
Notice: Undefined property: Test::$aaa in /data/t.php on line 31
PHP Notice: Undefined property: Test::$aaa in /data/t.php on line 31
Notice: Undefined property: Test::$aaa in /data/t.php on line 31
因为我采用的 Yii2 那种全局组件方案,大量使用了 __get 初始化组件,有些在内部也包含了协程切换,导致下一个并行同名组件的请求直接不初始化了,PHP Notice: Undefined property 错误,我之前预期的是不抛出错误继续执行的。
复现,但是直接显式调用_get()无此问题 可能是bug
<?php
class Test
{
public function _get($name)
{
// 协程切换
\Swoole\Coroutine::sleep(2);
// 其他hook的redis,pdo操作等会导致协程切换的都会导致
return $name;
}
}
$t = new Test;
for ($i = 0; $i < 3; $i++) {
go(function() use ($t, $i){
$t->_get('aaa');
});
}
__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打上这个掩码。
协程切换出去后,下次调用被判断为循环调用了
Most helpful comment
参考PHP7内核剖析
协程切换出去后,下次调用被判断为循环调用了