原文地址http://blog.jpauli.tech/2017/01/12/threads-and-php.html
前言
PHP 和线程,单凭这简短的几个字,就足以写一本书。像往常一样,我们不会这么做,但是会给出一定程度上跟这个话题相关的信息与细节。让我们从一些人在谈论这个话题时通常感到的困惑开始,PHP 不是一种线程语言, PHP 的内核没有使用线程,而且 PHP 天生也不允许用户层代码通过任何方式使用多线程作为并发机制。
因此 PHP 跟其他一些技术有很大的区别,例如 Java。Java 不仅自身使用了大量的线程,它还允许用户通过编程来是用线程。然而,PHP 不适用线程是有它的原因的。
PHP 内核没有使用线程,主要是为了简化开发。当你读到下一节的时候,你就会了解到线程并不是一个能使任何程序都能更快运行的魔法技术。是不是听起来很像是在推销不是吗?但是我们不是推销,而是谈论技术,而且 我们很清楚我们在说什么。因此 PHP 引擎目前没有使用多线程,也许将来会使用。但是使用多线程在编程上会引发很多问题,例如程序运行结果不是你所期待的等等。主要的困难是跨平坦的多线程编程,其次就是资源共享和 锁的管理,再次就是并不是所有的程序都能够被转化成多线程程序。PHP 的设计主要在 2000 前后,在那个时候,多线程编程并不是很广泛和成熟,PHP 引擎开发工程师决定创造一个完全没有线程的单片机引擎(当然他们也没有 足够的能力去驾驭一个稳定的跨平台的多线程引擎)。
PHP 用户层代码也不允许使用线程,因为那不是 PHP 期待你的代码运行的方式。PHP 是一个"发送并忘记(fire-and-forget)“型的语言,你应该尽可能快的处理完请求,然后释放,然后接着处理下一个请求。PHP 被设计作为一种 胶水语言:你不用处理可能使用到线程的复杂任务,而是访问快速而且已经准备好的资源,将它们粘合到一起,然后再返回给用户。通过 PHP,无论什么可能花费比通常时间多的时间的任务,都不能用 PHP 来处理。这就是为什么 在 PHP 中我们通常使用基于消息队列的系统(Gearman, AMQP, ActiveMQ 等等)来异步处理一些耗时任务。正如 Unix 看待事物的方式:“开发小而完备的工具,然后将他们连接在一起”。因此 PHP 的设计不是允许大规模的并行,而是 其他专门的技术–是用正确的工具来解决特定的问题。
线程的简介
让我们来快速的介绍下线程。注意,我们不会阐述太多细节的东西,对于你想深入了解关于线程的任何细节,都可以在相关书籍和站点上找到。
线程是进程中的轻量的事务处理单元,注意,一个进程可以产生多个线程,一个线程必须有且只能属于一个进程。进程是操作系统中的基本工作处理单元。在多 CPU 的机器上,不同的 CPU 将会平行工作,这样对于计算能力的提升会 有很大的好处。如果进程 A 和 B 都准备被执行,而且两个 CPU(或者两个 CPU 核心)也都有空闲的负载,那么进程 A 和 B 将会同时被执行。因此,计算机将能高效的在一个单位时间内同时进行多个运算,我们称之为“并行”(parallelism)。
进程
线程
进程和线程的关系
线程不是进程,线程是进程中的执行单元。也就是说,一个进程可以将它的工作划分成多个小的任务,使他们同时执行。例如:进程 A 和进程 B 都能够创造线程,分别为 A1,A2,B1,B2,如果计算机有多个 CPU(例如 8 个),那么 A1, A2, B1, B2 将会在同一个时帧运行。
使用线程,程序员可以决定将进程任务划分成多个小的任务,使得他们能同时执行
线程的执行跟进程几乎完全一样:他们都拥有一个状态,内核线程调度程序通过这个状态来管理它们。
线程比进程更加轻量级,线程只需要一个栈和一些寄存器,而进程则需要更多的条件(内核虚拟机,堆,一些信号信息,一些文件描述符信息,一些锁信息等等)。
进程的内存是由内核和内存管理单元管理,而线程的内存是由程序员自己和一些线程库来管理。
线程的内存布局
正如我们了解的,线程拥有独立的栈,也就是说,线程访问有个函数中声明的变量时,他们访问的是他们持有的该变量的拷贝。但是我们不能用同样描述来说明进程的堆:堆在线程间是共享的,通常存放全局变量和文件描述符。 这样做有利也有弊。如果你只是读取一个全局的内存,你只需要在一个恰当的时机读取(例如在线程 X 之后,线程 Y 之前),如果你想要去写,那么你必须保证多个线程不能在同一时刻去写同一个内存空间:这样会破坏那个内存区域 让记忆体处于不可预知的状态;这种情况我们就称之为“竞争条件”,同时这也是线程编程背后所面临的主要挑战。
对于并发访问情况的发生,你需要在你的代码中加入一些诸如可重入性和同步机制的编程技术,可重入性用来防止并发,而同步则主要是保证并发按照可预测的方式进行。
拥有了一个很大的共享内存,就有必要去同步公共空间的访问,常用的技术有信号(semaphores)灯和互斥器(mutexes)。它们都是基于锁的概念,如果一个资源被锁定,同时有一个线程尝试访问,那么这个线程就会被阻塞,直到共享资源可以被访问。 这就是为什么使用线程并不一定就意味着你的程序能跑的更快。如果你不能有效的划分任务,或者不能有效的管理锁,程序将会比不用线程的单进程执行任务耗费更多的时间:因为线程总是在相互等待。
如果你没有熟练使用过线程的话,使用起来确实很复杂。你需要花很多时间去练习,而且会面临很多问题,如果你漏掉了一点点细节,那么你的整个程序可能在你面前崩溃掉。调试线程程序比调试非线程程序要困难的多,假如我们正在讨论的是成百上千个线程运行到进程中的真实用例,那么你将会很快迷失自己,陷入困难之中。
由于前面所述的那种共享内存的方式不是我们想要的,于是出现了 Thread Local Storage(TLS)。TLS 的主要原理是全局数据被线程持有,而且不能共享给其他线程,它们是一个代表全局状态的内存区域,但是对于线程而言是私有的。 要实现 LTS,在线程被创建的时候,就要申请一些进程堆内存,线程库提供一个 key,将该 key 关联到这块存储区域。每次访问这块属于特殊线程的区域,都需要使用这个特定的 key 来解锁才行。线程被销毁的时候,需要同时释放这块堆内存。
Thread libraries
正如你的猜想,操作线程需要操作系统内核的帮助。在 90 年代中期,线程出现在操作系统中,又过了很长的时间,才逐渐成熟。但是依然存在跨平台的问题,尤其是 windows 和 unix 这两大对立阵营,他们采用了不同的线程模型和不同的线程库。如今,类 unix 系统中使用的是pthread(也同时存在一些其他的 thread libraries)。Pthread 代表的是"Posix threads”,这是一个可以追溯到 1995 年的 POSIX 规范的实现。因此,如果你想在你的程序中使用线程,你需要通过 gcc 的-lpthread
开关开连接 libpthread 到你的程序。同时 libpthread 是一个用 c 语言编写的开源程式库,它有自己独立的版本控制和管理。
所以,通常情况下,在类 unix 系统中,我们使用pthread来进行多线程编程。需要注意的是,pthread 允许并发,但是是否平行,这个取决于操作系统和计算机本身。并发是多个线程运行在同一个 CPU 执行序,平行是多个线程在同一时刻运行在不同的 CPU 上。
并发:
平行:
PHP 和多线程
让我们先回顾一下:
- PHP 不是一个多线程的语言:PHP 引擎不是通过管理线程来实现其并发机制。
- PHP 不提供用户端操作线程的的方法:你不能通过原生 PHP 语言来直接操作线程。有一个由 PHP 核心开发人员 Joe Watkins 开发的 PHP 扩展:ext/pthread 提供了操作线程的方法,虽然这是一个非常棒的扩展库,但是我个人还是不推荐如此使用 PHP,毕竟对于多线程编程,PHP 并不是合适的语言,比如我就会选择 C 或者 Java。
那么,谈论 PHP 和多线程有什么意义呢?
PHP 是如何处理请求的
这里说的其实是 PHP 是如何处理 HTTP 请求的。为了在同一时间内服务多个客户端,一个 web 伺服器程式需要一些并发(或者平行)机制。你不能因为响应一个客户端而阻塞其他所有的请求不是吗?如此以来,伺服器程式通常的做法是使用多进程,或多线程去响应客户端。从历史的角度来看,在 unix 上,使用的是多进程模型。因为进程是 unix 的基础,从 unix 诞生的时候起,进程就诞生了,而且拥有创建、销毁、和同步的能力。在 unix 环境中,多个 PHP 服务多个客户端,但是每一个 PHP 在一个独立的进程中运行。
如果你还记得前言中介绍的,在这种情况下,PHP 代码中不需要做任何额外的事情:进程间是彼此隔离的,进程 A 处理请求 A 中的数据,不会影响到进程 B 处理请求 B 中的数据,而这正是我们想要的。
使用这种模型的包括php-fpm
和 Apache 的mpm_prefork
,通常,在 98%的情形下你是用的是二者中的其中一种架构。但是,到了 windows 环境下或者在那些使用线程的 unix 系统中,事情将会变得复杂。windows 毫无质疑地是一个很优秀的操作系统,但是它有一个弊端就是它的代码不是公开的。不过幸运的是关于它内部引擎是如何工作的原理能够在互联网和一些书籍上找到,而且微软工程师也分享了很多关于 windows 核心的相关知识。在处理并发和平行的问题上,windows
选择了不同于 unix 的道路。windows 高度依赖线程,事实上,在 windows 上创建一个进程的代价是很大的以至于你通常不会这么做。在 windows 系统中,你每时每刻都在使用线程。windows 中的线程也比 unix 中强大很多。因此当你在 windows 上运行 PHP 的时候,伺服器程序(例如 IIS,Apache,FooBarBaz)会使用多线程处理不同的客户端,而不是进程。也就是说,在这样的环境下,PHP 将会运行在线程中,而且 PHP 要额外的小心线程的规则:它必须是线程安全的。
PHP 必须是线程安全的,也就是说它必须能够控制不是由它自身创建的并发性,而且必须能够。聪明的你也许已经想到了,要解决这个问题,PHP 就要寻找一种方法,能够防止其自身访问自己的全局变量。
于是就有了一个叫做Zend Thread Safety(ZTS)的模块,用以实现线程安全性。
Zend Thread Safety 的内部细节
开启 ZTS 可以通过使用--enable-maintainer-zts
编译开关。通常情况下,你不需要打开此开关,除非是运行在 windows 系统中,或者是你需要使用一些扩展需要引擎是线程安全的时候。检查是否开启 ZTS 可以有很多方式,例如使用命令行php -v
liubang@venux:~$ /opt/app/php-7.1/bin/php -v
PHP 7.1.7 (cli) (built: Jul 11 2017 10:00:35) ( NTS )
Copyright (c) 1997-2017 The PHP Group
你也可以使用phpinfo()
来查看。在 PHP 中也可以使用检查PHP_ZTS
常量来判断是否启用
if (PHP_ZTS) {
echo "OK";
}
在 ZTS 模式下,PHP 内核都是线程安全的,除非你使用了非线程安全的扩展。官方 PHP 扩展都是线程安全的,但是对于一些第三方的扩展,谁能保证呢?
使用和设计可重入函数
当设计一个 PHP 扩展的时候,使用可重入函数。可重入函数是指函数不依赖任何全局状态来工作。简单来说,可重入函数的正确定义是一个函数可以在这个函数执行的任何时刻中断它。如果一个函数被平行调用于多个线程中,如果他们
使用了全局的变量或状态,那么显然它不是可重入函数。一些传统的 libc 函数不是可重入函数,因为他们诞生于一个没有线程的年代。因此一些 libc 发布了可重入版本,通常是在函数加上_r
后缀。同时,最新的 C11 标准也给线程提供
了很大的空间,C11 libc 将修改函数后缀为_s
.
处于跨平台的考虑,PHP 自身也提供了这些可重入函数,可以访问源码来查看 PHP 提供的可重入函数列表。
不要连接非线程安全的程式库
线程编程是关于整个进程镜像共享的,而进程镜像中包括一些连接的程式库。如果你的扩展连接了非线程安全的库,那么你将要采取一些措施来避免这些库访问全局资源。有些事在 C 语言和多线程语言中很常见,但是却很容易被人忘记。
使用 ZTS
当我们开发 PHP 内核或者编写 PHP 扩展的时候,我们一定要区分两种全局变量。一种是普通的 C 语言全局变量, 叫做"true globals",对于这种变量我们不必在多线程中做一些额外的工作,只需要正常的读取就好了,因为这种全局变量在线程创建之前就已经被 创建和初始化了。而执行这些操作的方法在内核中叫做module init,在很多 PHP 扩展里,我们都能看到形如以下的代码:
static int val; /* true global */
PHP_MINIT(wow_ext)
{
if (somthong()) {
val = 3;
}
}
php 扩展有很多 hook,通过 PHP 文件来触发。这个叫做MINIT()
的 hook 是用来初始化 PHP 的,执行到这一步时,PHP 开始启动,我们可以在这里安全的读写 true globals,就像例子中那样。此外还有一个非常重要的 hook,叫做RINIT()
,
即请求初始化,每一个 PHP 扩展的RINIT()
hook 在每一个新的请求处理时都会被触发,也就是说RINIT()
在一个扩展中能够被调用很多次。在RINIT()
中,PHP 已经处于线程当中,所以此时的代码必须是线程安全的。无论是 C 语言的全局变量,还是线程全局变量,他们都是全局变量,都需要通过 ZTS 层来防止线程不安全的发生。
PHP_RINIT(wow_ext)
{
if (something()) {
WOW_G(val) = 3; /* writing to a thread global */
}
}
我们通过宏WOW_G()
来访问线程全局变量,下面我们来探讨下这个宏背后到底发生了什么。
宏的必要
记住,当 PHP 在多线程的环境中式运行时,所有面向请求的全局资源都必须对其访问做限制。但是当 PHP 在非线程环境下运行时,这种限制就是没有必要的,因为每个进程都有它自己的存储空间,没有共享的部分。因此访问面向请求的
全局变量的操作是区分环境的,也就是说我们需要寻找一种不区分环境的统一的表现形式来访问这些全局变量。我们使用宏就是为了解决这个问题。上面的WOW_G()
宏会区分不同的多任务引擎,而且如果你改变了条件,需要重新编译
你的扩展,这就是为什么 PHP 扩展不兼容 ZTS mode 和 non-ZTS mode:因为它不是二进制兼容的。
WOW_G()
宏在多进程模式下
#ifdef ZTS
#define WOW_G(v) wow_globals.v
#endif
而在多线程环境中
#ifdef ZTS
#define WOW_G(v) wow_globals.v
#else
#define WOW_G(v) (((wow_globals *) (*((void ***) tsrm_get_ls_cache())))[((wow_globals_id)-1)]->v)
#endif
ZTS 模式是不是看上去很复杂。在多进程环境中,使用的是 NZTS(Non Zend Thread Safe),使用全局变量会被命名为wow_globals
。这是一个存放全局变量的结构体,你可以通过使用WOW_G
宏来访问其中的成员。WOW_G(foo)
代表wow_globals.foo
。很显然,你需要去声明这样的变量,然后在启动的时候将其初始化。而这一切也可以通过宏来操作:
ZEND_BEGIN_MODULE_GLOBALS(wow)
int foo;
ZEND_END_MODULE_GLOBALS(wow)
ZEND_DECLARE_MODULE_GLOBALS(wow)
这些宏将被展开为:
#define ZEND_BEGIN_MODULE_GLOBALS(module_name) typedef struct _zend_##module_name##_globals {
#define ZEND_END_MODULE_GLOBALS(module_name) } zend_##module_name##_globals;
#define ZEND_DECLARE_MODULE_GLOBALS(module_name) zend_##module_name##_globals module_name##_globals;
上面就是在多进程模式下的实现,是不是很简单。
但是在多线程模式下,也就是使用了 ZTS,将不会再有 C 语言中的全局变量声明,但是宏的表现形式确是一致的:
#define ZEND_BEGIN_MODULE_GLOBALS(module_name) typedef struct _zend_##module_name##_globals {
#define ZEND_END_MODULE_GLOBALS(module_name) } zend_##module_name##_globals;
#define ZEND_DECLARE_MODULE_GLOBALS(module_name) ts_rsrc_id module_name##_globals_id;
在 ZTS 和 NZTS 模式下申明全局变量看上去差异不大,但是访问的时候却有很大的差别,在 ZTS 模式下,通过调用tsrm_get_ls_cache()
函数。该函数调用Thread Local Storage(TLS),然后返回一个跟当前线程绑定的内存区域。正如
你所看到的那样,这个内存区域是非常复杂的,就凭最开始的一个(void ***)case
类型装换,就能让我们嗅到它背后复杂的气息。
TSRM layer
ZTS 是通过一个叫做 TSRM 的层实现的。Thread Safe Resource Manager layer 仅仅是一些普通的 C 代码而已!它主要位于 PHP 源码中的 TSRM 目录中。即使我们将会描述它的细节,但是阅读一下源码也是一件很有意义的事情。
TSRM 并不完美,它从 PHP5(2004)开始才大体设计完成。它可以操作一些底层的线程库:Gnu Portable Thread, Posix Threads, State Threads, Win32 Threads or BeThreads。如果你想使用 TSRM,需要在编译的时候加上--with-tsrm-xxx
参数。在深入分析 TSRM 的时候我们只讲解 pthreads 的实现。
TSRM boot
在 PHP 启动的时候,执行 module initialization 时,PHP 会迅速调用tsrm_start()
。由于 PHP 现在还不知道有多少个线程需要建立线程安全保护,因此它初始化线程表的时候只存入 1 个元素。这个表随后会通过使用malloc
增加元素。
这个启动操作中同样很重要的一步是同时创建 TLS 键和需要被同步的 TLS 互斥锁。
static pthread_key_t tls_key;
TSRM_API int tsrm_startup(int expected_threads, int expected_resources, int debug_level, char *debug_filename)
{
pthread_key_create( &tls_key, 0 ); /* Create the key */
tsrm_error_file = stderr;
tsrm_error_set(debug_level, debug_filename);
tsrm_tls_table_size = expected_threads;
tsrm_tls_table = (tsrm_tls_entry **) calloc(tsrm_tls_table_size, sizeof(tsrm_tls_entry *));
if (!tsrm_tls_table) {
TSRM_ERROR((TSRM_ERROR_LEVEL_ERROR, "Unable to allocate TLS table"));
return 0;
}
id_count=0;
resource_types_table_size = expected_resources;
resource_types_table = (tsrm_resource_type *) calloc(resource_types_table_size, sizeof(tsrm_resource_type));
if (!resource_types_table) {
TSRM_ERROR((TSRM_ERROR_LEVEL_ERROR, "Unable to allocate resource types table"));
free(tsrm_tls_table);
tsrm_tls_table = NULL;
return 0;
}
tsmm_mutex = tsrm_mutex_alloc(); /* Allocate a mutex */
}
#define MUTEX_T pthread_mutex_t *
TSRM_API MUTEX_T tsrm_mutex_alloc(void)
{
MUTEX_T mutexp;
mutexp = (pthread_mutex_t *)malloc(sizeof(pthread_mutex_t));
pthread_mutex_init(mutexp,NULL);
return mutexp;
}
TSRM Resources
至此,TSRM 已经启动了,是时候向其中添加新资源了。一个 TSRM 资源,其实就是一个存放许多全局变量集合的内存区域,通常是给 PHP 扩展专用的,而且必须被当前特定的线程所持有,或者被限制访问。接着,这个内存区域有一个大小,而且需要有初始化(constructor)和销毁(destructor)操作。通常初始化就是用 0 将其填充,而销毁则不需要做任何事情。这样被称作 TSRM Resource 的内存区域,会被 TSRM layer 赋予一个唯一的 resource ID,调用者需要保存这样的一个 ID,以便在后续的调用中返还给 TSRM。
下面是 TSRM 创建一个新的 resource 的实现:
typedef struct {
size_t size;
ts_allocate_ctor ctor;
ts_allocate_dtor dtor;
int done;
} tsrm_resource_type;
TSRM_API ts_rsrc_id ts_allocate_id(ts_rsrc_id *rsrc_id, size_t size, ts_allocate_ctor ctor, ts_allocate_dtor dtor)
{
int i;
tsrm_mutex_lock(tsmm_mutex);
/* obtain a resource id */
*rsrc_id = id_count++;
/* store the new resource type in the resource sizes table */
if (resource_types_table_size < id_count) {
resource_types_table = (tsrm_resource_type *) realloc(resource_types_table, sizeof(tsrm_resource_type)*id_count);
if (!resource_types_table) {
tsrm_mutex_unlock(tsmm_mutex);
TSRM_ERROR((TSRM_ERROR_LEVEL_ERROR, "Unable to allocate storage for resource"));
*rsrc_id = 0;
return 0;
}
resource_types_table_size = id_count;
}
resource_types_table[(*rsrc_id)-1].size = size;
resource_types_table[(*rsrc_id)-1].ctor = ctor;
resource_types_table[(*rsrc_id)-1].dtor = dtor;
resource_types_table[(*rsrc_id)-1].done = 0;
/* enlarge the arrays for the already active threads */
for (i=0; i < tsrm_tls_table_size; i++) {
tsrm_tls_entry *p = tsrm_tls_table[i];
while (p) {
if (p->count < id_count) {
int j;
p->storage = (void *) realloc(p->storage, sizeof(void *)*id_count);
for (j=p->count; j<id_count; j++) {
p->storage[j] = (void *) malloc(resource_types_table[j].size);
if (resource_types_table[j].ctor) {
resource_types_table[j].ctor(p->storage[j]);
}
}
p->count = id_count;
}
p = p->next;
}
}
tsrm_mutex_unlock(tsmm_mutex);
return *rsrc_id;
}
从上述代码中可以看到,这个函数需要一个互斥锁。如果它被一个子线程调用,那么它将会持有锁,其他线程将不能在同一时刻操作 global thread storage。新的 resource 被添加到了一个动态的resource_types_table[]
数组里,然
后会生成一个唯一的标识rsrc_id
,随着资源的不断增加,这个标识的值也会增长。
在请求开始的时候
现在我们已经准备好开始处理请求了。切记,每个请求都是在特定的线程中被处理的。那么当一个请求到来的时候会发生什么呢?在每个请求最最最开始的时候,ts_resource_ex()
函数会被调用。这个函数会读取当前的线程 id,接着
尝试去获取由当前线程创建的资源,也就是专属于当前线程的用来存放全局变量的内存区域。如果没有获取到(说明是一个新的线程)那么它将会像 PHP 启动的时候那样,调用allocate_new_resource()
函数来为当前线程创建一个新的资源。
static void allocate_new_resource(tsrm_tls_entry **thread_resources_ptr, THREAD_T thread_id)
{
int i;
TSRM_ERROR((TSRM_ERROR_LEVEL_CORE, "Creating data structures for thread %x", thread_id));
(*thread_resources_ptr) = (tsrm_tls_entry *) malloc(sizeof(tsrm_tls_entry));
(*thread_resources_ptr)->storage = NULL;
if (id_count > 0) {
(*thread_resources_ptr)->storage = (void **) malloc(sizeof(void *)*id_count);
}
(*thread_resources_ptr)->count = id_count;
(*thread_resources_ptr)->thread_id = thread_id;
(*thread_resources_ptr)->next = NULL;
/* Set thread local storage to this new thread resources structure */
tsrm_tls_set(*thread_resources_ptr);
if (tsrm_new_thread_begin_handler) {
tsrm_new_thread_begin_handler(thread_id);
}
for (i=0; i<id_count; i++) {
if (resource_types_table[i].done) {
(*thread_resources_ptr)->storage[i] = NULL;
} else
{
(*thread_resources_ptr)->storage[i] = (void *) malloc(resource_types_table[i].size);
if (resource_types_table[i].ctor) {
resource_types_table[i].ctor((*thread_resources_ptr)->storage[i]);
}
}
}
if (tsrm_new_thread_end_handler) {
tsrm_new_thread_end_handler(thread_id);
}
tsrm_mutex_unlock(tsmm_mutex);
}
扩展中的 Local Storage 缓存
在 PHP7 中,每一个扩展都会声明一个 local storage 缓存。也就是说每一个扩展需要在每一个新的线程启动的时候读取该线程的 local storage,而不是在每次访问全局变量的时候迭代 storage 列表。要完成这样的魔法,还需要额外的操作。首先你需要编译 PHP 的时候加上DZEND_ENABLE_STATIC_TSRMLS_CACHE=1
参数,然后你应该用ZEND_TSRMLS_CACHE_DEFINE()
宏来声明你的全局变量:
#define ZEND_TSRMLS_CACHE_DEFINE(); __thread void *_tsrm_ls_cache = ((void *)0);
如你所见,这里声明了一个 C 语言的全局变量,但是使用了**__thread**这个特殊的声明。这是用来告知编译器,该变量是线程特有的变量。接着你需要使用ZEND_TSRMLS_CACHE_UPDATE()
宏来将 TSRM layer 中存放的全局变量填充到这个void *
storage 当中。
PHP_GINIT_FUNCTION(my_ext)
{
#ifdef ZTS
ZEND_TSRMLS_CACHE_UPDATE();
#endif
}
下面是这个宏展开的样子:
#define ZEND_TSRMLS_CACHE_UPDATE() _tsrm_ls_cache = tsrm_get_ls_cache();
针对 pthread 的实现:
#define tsrm_get_ls_cache() pthread_getspecific(tls_key)
至此,你应该能理解全局变量是如何通过下面的宏来访问的了:
#ifdef ZTS
#define MY_G(v) (((my_globals *) (*((void ***) _tsrm_ls_cache))[((my_globals_id)-1)])->(v))
使用MY_G()
宏来访问全局变量,当在多线程环境中的时候,它将会被展开未通过扩展的 id 查找_tsrm_ls_cache
区域:
my_globals_id:
每一个扩展都有存放它全局变量的空间,id 用于返回此扩展的存储空间。TSRM 会在一个新的请求/线程诞生的时候为当前线程创建这个存储空间。
总结
多线程编程不是一个简单的事情。在此,我只是描述了 PHP 是如何处理全局变量的管理的:它通过引擎中特定的 TSRM layer,使用 TLS 在每个新的线程和请求启动的时候分离每一个全局存储。它持有一个互斥锁,然后为当前线程创建 存储全局变量的存储空间,然后在释放互斥锁。通过这种方式,我们可以在 PHP 扩展的任何地方访问它自己的全局变量,而不需要使用互斥锁。
TSRMLS layer 背后的一切都是那么的抽象:这是一个用来简化全局变量管理的 C 代码层,尤其对于 PHP 扩展开发者而言,你通过一个宏来访问你的全局空间,如果你在 ZTS 环境下运行,这个宏会展开成特定的代码来访问每一个扩展中属于 你自己的一小部分,通过 TSRM 缓存,你不必在每次访问全局变量的时候去做查找操作,而是给你一个指向特定的存储空间的指针,你缓存起来,并在需要访问全局变量的时候使用它。
当然,这些说的都是基于请求的全局变量。你可能任然在使用 C 语言的全局变量,但是不要尝试着在处理一个请求的时候去写他们:这么做即使你没有使得整个服务器崩溃,也会给企业造成巨大的损失,而且会出现很难 debug 的奇怪行为!
评论