PHP7 扩展开发(三) 线程安全

编辑于 2016-09-30

* 移动设备下, 可左滑手指以查看较宽代码

随着多线程软件越来越多, PHP 亟需一种新的资源管理方式, 并最终在内核中形成了一个新的抽象层: TSRM (Thread Safe Resource Management).

TSRM 的引入

我们知道, 在 C 或者 PHP 等编程中, 要在多个过程中访问同一个变量有两种方式, 一种是通过参数传递, 比如下面的代码:

#include <stdio.h>

void output_func(char *message) {
    printf("%s\n", message);
}
 
int main(int argc, char *argv[]) {
    output_func(argv[0]);
 
    return 0;
}

另外一种方式是, 在高一级作用域中存储这个变量:

#include <stdio.h>
 
char *message;

void output_func(void) {
    printf("%s\n", message);
}
 
int main(int argv, char *argv[]) {
    message = argv[0];
    output_func();
 
    return 0;
}

在一个没有线程的程序中, 我们往往倾向于把全局变量声明在源文件的顶部.

对于 PHP 使用第二种方式: 一般的 单线程 模型比如 PHP CLI 方式,或者 Apache2 + prefork MPM (也是一种多进程模型), 可以放心的被使用, 也不会出错. 但是在多线程的模型下, 这种方式就不在安全了, 比如 Apache2 + worker MPM 和 IIS (对 Win32 下的 Apache 来说, 是基于多线程的, 所以这个层在 Win32 下总是被开启的). 在这种情况下, 所有的线程共享同一个进程的地址空间 (共享地址空间). 也就是说, 多个线程共用一个全局变量, 这个时候就会产生竞争. 这个时候的全局变量是 非线程安全 (NTS, None-Thread Safety) 的.

关于运行模式: 不了解的请看上一篇文章

每个线程的全局变量, 可以都会根据自己的 id 在共享地址空间内操作, 从而不影响其他线程. 因此, PHP 引入了 TSRM 层, 实现了 线程安全 (TS, Thread Safety), 而不需要开发者考虑过多细节.

声明全局变量

在 PHP 为我们生成的 php_myextention.h 中, 我们清楚的看到:

/* 
 	Declare any global variables you may need between the BEGIN
	and END macros here:
*/
ZEND_BEGIN_MODULE_GLOBALS(beast)
	long  global_value;
	char *global_string;
ZEND_END_MODULE_GLOBALS(beast)

是的, 这样就同时声明了线程安全与非线程安全的全局变量.

正常的 PHP 构建默认是关闭线程安全的, 只有在被构建的 sapi 明确需要线程安全或线程安全在 ./configure 阶段显式的打开时, 才会以线程安全 (TSRM) 方式构建.

在 PHP5 和以前的版本,你会看到很多以「TSRMLS」开头的宏, 它就与定位线程中的全局变量有关,但 PHP7 认为这种方式并不好,这些宏就仅用作兼容以前的扩展代码.

访问全局变量

看看扩展中的如下代码:

#define MYEXTENTION_G(v) ZEND_MODULE_GLOBALS_ACCESSOR(myextention, v)

在线程安全和非线程安全环境下, 可以使用一致的代码访问全局变量: MYEXTENTION_G(v).

关于「ZEND_MODULE_GLOBALS_ACCESSOR」这个宏,这里不细究,主要意思如下:

非线程安全环境下,上述操作等价于全局变量.

线程安全环境下,PHP7 最终通过操作系统提供的pthread_getspecific()功能来操作线程独享数据.

内核中的线程安全

同样, 在 PHP 内核代码中, 不同的范围有不同的宏:

// 编译时信息, 包括函数表等(zend_globals_macros.h:32)
// Zend/zend_globals_macros.h:
# define CG(v) ZEND_TSRMG(compiler_globals_id, zend_compiler_globals *, v)

// 执行时信息(zend_globals_macros.h:43)
// Zend/zend_globals_macros.h:
# define EG(v) ZEND_TSRMG(executor_globals_id, zend_executor_globals *, v)

// 主要存储 php.ini 中的信息
// main/php_globals.h:
# define PG(v) ZEND_TSRMG(core_globals_id, php_core_globals *, v)

// 主要用于获取 SAPI 层范围内的全局变量 
// main/SAPI.h:
# define SG(v) ZEND_TSRMG(sapi_globals_id, sapi_globals_struct *, v)