以前总觉得php是多进程单线程的,每个访问都是一个单独的进程互相不干扰各自的数据,数据是隔离的,最早的PHP都是来一个用户新建一个进程,访问结束就销毁这个进程,但是这样非常的浪费性能,后续PHP-fpm的出现将这个进程复用实现了,大大的优化速度,用户来了以后是分配一个线程的空闲进程来服务,用户离开就回到空闲里面给后面的用户使用。现在php早已实现了多线程的功能,今天我来学学这个多线程是怎么实现的。

目前PHP多数是单线程环境,比如cli、fpm、cgi,每个进程只启动一个主线程,这种情况下也就不存在线程安全的情况了,在多线程环境下就需要考虑线程安全的问题了,PHP中有很多的全局变量这个如果多线程公用的情况下就会造成线程安全你的问题,PHP专门做了一个安全机制:Zend线程安全(Zend Thread Safe, ZTS)

PHP为了解决这个问题做了一个程安全资源管理器(Thread Safe Resource Mananger, TSRM),其主要实现的原理就是单个进程有一个公用的全局变量,如果多线程互相贡献不安全,就把这些数据拷贝若干份,保证每个线程都有自己的全局变量,这样线程之间互不干扰完美的解决这个安全的问题.

PHP中定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct {
size_t size; //资源的大小
ts_allocate_ctor ctor; //初始化函数
ts_allocate_dtor dtor;
int done;
} tsrm_resource_type;

struct _tsrm_tls_entry {
void **storage; //资源数组
int count; //拥有的资源数:storage数组大小
THREAD_T thread_id; //所属线程id
tsrm_tls_entry *next;
};

一个资源如果想被多线程使用,就必须想TSRM注册资源,TSRM会给这个资源分配一个ID,并把资源相关数据初始化保存到tsrm_resource_type中去,所有的线程必须通过这个ID来访问这个资源,如果线程第一次访问这个资源,TSRM会初始化这个资源,也就是复制一份出来给这个线程使用包括后续访问。

PHP线程安全分配图表

tsrm_tls_table 保存着所有线程物理位置,这个位置通过根据线程id与预设置的线程数tsrm_tls_table_size取模得到的

每个线程拥有一个tsrm_tls_entry结构,当前线程的所有资源保存在storage数组中,它是一个链表结构,查找资源时首先根据:线程id % tsrm_tls_table_size得到一个tsrm_tls_entry,然后开始遍历链表比较thread_id确定是否是当前线程的。
线程本地存储(Thread Local Storage, TLS),在创建完当前线程的tsrm_tls_entry后会把这个值保存到当前线程的TLS中,这样在ts_resource()获取资源时中就可以通过tsrm_tls_get()直接取到了,节省加锁检索的时间。