摘要: 注:本篇非我一己之力所完成,最后发布在了《TIPI》这本电子书上。 了解线程安全之前,我们先回顾几点基础知识点,是我们后面分析学习的基础。 变量的作用域 从作用域上来说,C语言可以定义4种不同的变量:全局变量,静态全局变量,局部变量,静态局部变量。 下面仅从函数作用域的角度分析一下不同的变
注:本篇非我一己之力所完成,最后发布在了《TIPI》这本电子书上。
了解线程安全之前,我们先回顾几点基础知识点,是我们后面分析学习的基础。
变量的作用域
从作用域上来说,C语言可以定义4种不同的变量:全局变量,静态全局变量,局部变量,静态局部变量。
下面仅从函数作用域的角度分析一下不同的变量,假设所有变量声明不重名。
- 全局变量(
int gVar;
),在函数外声明。全局变量,所有函数共享,在任何地方出现这个变量名都是指这个变量。 - 静态全局变量(
static sgVar
),其实也是所有函数共享,但是这个会有编译器的限制,算是编译器提供的一种功能。 - 局部变量(函数/块内的
int var;
),不共享,函数的多次执行中涉及的这个变量都是相互独立的,他们只是重名的不同变量而已。 - 局部静态变量(函数中的
static int sVar;
),本函数间共享,函数的每一次执行中涉及的这个变量都是这个同一个变量。
上面几种作用域都是从函数的角度来定义作用域的,可以满足所有我们对单线程编程中变量的共享情况。 现在我们来分析一下多线程的情况。
在多线程中,多个线程共享除函数调用栈之外的其他资源。 因此上面几种作用域从定义来看就变成了。
- 全局变量,所有函数共享,因此所有的线程共享,不同线程中出现的不同变量都是这同一个变量。
- 静态全局变量,所有函数共享,也是所有线程共享。
- 局部变量,此函数的各次执行中涉及的这个变量没有联系,因此,也是各个线程间也是不共享的。
- 静态局部变量,本函数间共享,函数的每次执行涉及的这个变量都是同一个变量,因此,各个线程是共享的。
线程安全资源管理器的由来
在多线程系统中,进程保留着资源所有权的属性,而多个并发执行流是执行在进程中运行的线程。 如 Apache2 中的 worker,主控制进程生成多个子进程,每个子进程中包含固定的线程数,各个线程独立地处理请求。 同样,为了不在请求到来时再生成线程,MinSpareThreads 和 MaxSpareThreads 设置了最少和最多的空闲线程数; 而 MaxClients 设置了所有子进程中的线程总数。如果现有子进程中的线程总数不能满足负载,控制进程将派生新的子进程。
当 PHP 运行在如上类似的多线程服务器时,此时的 PHP 处在多线程的生命周期中。 在一定的时间内,一个进程空间中会存在多个线程,同一进程中的多个线程公用模块初始化后的全局变量, 如果和 PHP 在 CLI 模式下一样运行脚本,则多个线程会试图读写一些存储在进程内存空间的公共资源(如在多个线程公用的模块初始化后的函数外会存在较多的全局变量)。
此时这些线程访问的内存地址空间相同,当一个线程修改时,会影响其它线程,这种共享会提高一些操作的速度, 但是多个线程间就产生了较大的耦合,并且当多个线程并发时,就会产生常见的数据一致性问题或资源竞争等并发常见问题, 比如多次运行结果和单线程运行的结果不一样。如果每个线程中对全局变量、静态变量只有读操作,而无写操作,则这些个全局变量就是线程安全的,只是这种情况不太现实。
为解决线程的并发问题,PHP 引入了 TSRM: 线程安全资源管理器(Thread Safe Resource Manager)。 TRSM 的实现代码在 PHP 源码的 /TSRM 目录下,调用随处可见,通常,我们称之为 TSRM 层。 一般来说,TSRM 层只会在被指明需要的时候才会在编译时启用(比如,Apache2+worker MPM,一个基于线程的MPM), 因为 Win32 下的 Apache 来说,是基于多线程的,所以这个层在 Win32 下总是被开启的。
TSRM的实现
进程保留着资源所有权的属性,线程做并发访问,PHP 中引入的 TSRM 层关注的是对共享资源的访问, 这里的共享资源是线程之间共享的存在于进程的内存空间的全局变量。 当 PHP 在单进程模式下时,一个变量被声明在任何函数之外时,就成为一个全局变量。
首先定义了如下几个非常重要的全局变量(这里的全局变量是多线程共享的)。
/* The memory manager table */
static tsrm_tls_entry **tsrm_tls_table=NULL;
static int tsrm_tls_table_size;
static ts_rsrc_id id_count;
/* The resource sizes table */
static tsrm_resource_type *resource_types_table=NULL;
static int resource_types_table_size;
**tsrm_tls_table
的全拼 thread safe resource manager thread local storage table,用来存放各个线程的tsrm_tls_entry
链表。tsrm_tls_table_size
用来表示**tsrm_tls_table
的大小。id_count
作为全局变量资源的 id 生成器,是全局唯一且递增的。*resource_types_table
用来存放全局变量对应的资源。resource_types_table_size
表示*resource_types_table
的大小。
其中涉及到两个关键的数据结构 tsrm_tls_entry
和 tsrm_resource_type
。
typedef struct _tsrm_tls_entry tsrm_tls_entry;
struct _tsrm_tls_entry {
void **storage;// 本节点的全局变量数组
int count;// 本节点全局变量数
THREAD_T thread_id;// 本节点对应的线程 ID
tsrm_tls_entry *next;// 下一个节点的指针
};
typedef struct {
size_t size;// 被定义的全局变量结构体的大小
ts_allocate_ctor ctor;// 被定义的全局变量的构造方法指针
ts_allocate_dtor dtor;// 被定义的全局变量的析构方法指针
int done;
} tsrm_resource_type;
当新增一个全局变量时,id_count
会自增1(加上线程互斥锁)。然后根据全局变量需要的内存、构造函数、析构函数生成对应的资源tsrm_resource_type
,存入 *resource_types_table
,再根据该资源,为每个线程的所有tsrm_tls_entry
节点添加其对应的全局变量。
有了这个大致的了解,下面通过仔细分析 TSRM 环境的初始化和资源 ID 的分配来理解这一完整的过程。
TSRM 环境的初始化
模块初始化阶段,在各个 SAPI main 函数中通过调用 tsrm_startup
来初始化 TSRM 环境。tsrm_startup
函数会传入两个非常重要的参数,一个是 expected_threads
,表示预期的线程数, 一个是 expected_resources
,表示预期的资源数。不同的 SAPI 有不同的初始化值,比如mod_php5,cgi 这些都是一个线程一个资源。