Swoole-src: 魔术方法 __get 内的协程切换,导致并行操作 PHP Notice: Undefined property

Created on 13 Jun 2019  ·  3Comments  ·  Source: swoole/swoole-src

环境:

[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
Incompatibility php-bug

Most helpful comment

参考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打上这个掩码。

协程切换出去后,下次调用被判断为循环调用了

All 3 comments

因为我采用的 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');
    });
}


参考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打上这个掩码。

协程切换出去后,下次调用被判断为循环调用了

Was this page helpful?
0 / 5 - 0 ratings

Related issues

morozovsk picture morozovsk  ·  3Comments

andreybolonin picture andreybolonin  ·  4Comments

lotarbo picture lotarbo  ·  4Comments

erDong01 picture erDong01  ·  3Comments

jobs-git picture jobs-git  ·  3Comments