PHP 源码学习之线程安全

全局变量的应用

以正规化的数组扩充为例,首先会声明当前扩张的全局变量。

ZEND_DECLARE_MODULE_GLOBALS(array)

下一场在模块起先化时会调用全局变量开首化宏初步化
array,比如分配内存空间操作。

static void php_array_init_globals(zend_array_globals *array_globals)
{
    memset(array_globals, 0, sizeof(zend_array_globals));
}

/* code... */

PHP_MINIT_FUNCTION(array) /* {{{ */
{
    ZEND_INIT_MODULE_GLOBALS(array, php_array_init_globals, NULL);
    /* code... */
}

此间的宣示和先导化操作都是分别ZTS和非ZTS。

#ifdef ZTS

#define ZEND_DECLARE_MODULE_GLOBALS(module_name)                            \
    ts_rsrc_id module_name##_globals_id;

#define ZEND_INIT_MODULE_GLOBALS(module_name, globals_ctor, globals_dtor)   \
    ts_allocate_id(&module_name##_globals_id, sizeof(zend_##module_name##_globals), (ts_allocate_ctor) globals_ctor, (ts_allocate_dtor) globals_dtor);

#else

#define ZEND_DECLARE_MODULE_GLOBALS(module_name)                            \
    zend_##module_name##_globals module_name##_globals;

#define ZEND_INIT_MODULE_GLOBALS(module_name, globals_ctor, globals_dtor)   \
    globals_ctor(&module_name##_globals);

#endif

对此非ZTS的气象,直接表明变量,开首化变量;对于ZTS情形,PHP内核会添加TSRM,不再是宣称全局变量,而是用ts_rsrc_id代替,先河化时也不再是开端化变量,而是调用ts_allocate_id函数在多线程环境中给当下以此模块申请一个全局变量并赶回资源ID。其中,资源ID变量名由模块名加global_id组成。

若果要调用当前扩展的全局变量,则应用:ARRAYG(v),这么些宏的定义:

#ifdef ZTS
#define ARRAYG(v) TSRMG(array_globals_id, zend_array_globals *, v)
#else
#define ARRAYG(v) (array_globals.v)
#endif

假假若非ZTS则直接调用全局变量的性质字段,假如是ZTS,则需要通过TSRMG获取变量。

TSRMG的定义:

#define TSRMG(id, type, element) (((type) (*((void ***) tsrm_ls))[TSRM_UNSHUFFLE_RSRC_ID(id)])->element)

去掉这一堆括号,TSRMG宏的趣味就是从tsrm_ls中按资源ID获取全局变量,并赶回对应变量的习性字段。

那么现在的题材是这些 tsrm_ls 从哪个地方来的?

资源 ID 的分配

咱俩领悟开始化一个全局变量时索要动用 ZEND_INIT_MODULE_GLOBALS
宏(下边的数组扩大的事例中会有证实),而其实际则是调用的
ts_allocate_id 函数在多线程环境下报名一个全局变量,然后回来分配的资源
ID。代码虽然相比较多,实际仍旧比较清楚,下边附带申明举行验证:

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_ERROR((TSRM_ERROR_LEVEL_CORE, "Obtaining a new resource id, %d bytes", size));

    // 加上多线程互斥锁
    tsrm_mutex_lock(tsmm_mutex);

    /* obtain a resource id */
    *rsrc_id = TSRM_SHUFFLE_RSRC_ID(id_count++); // 全局静态变量 id_count 加 1
    TSRM_ERROR((TSRM_ERROR_LEVEL_CORE, "Obtained resource id %d", *rsrc_id));

    /* store the new resource type in the resource sizes table */
    // 因为 resource_types_table_size 是有初始值的(expected_resources),所以不一定每次都要扩充内存
    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;
    }

    // 将全局变量结构体的大小、构造函数和析构函数都存入 tsrm_resource_type 的数组 resource_types_table 中
    resource_types_table[TSRM_UNSHUFFLE_RSRC_ID(*rsrc_id)].size = size;
    resource_types_table[TSRM_UNSHUFFLE_RSRC_ID(*rsrc_id)].ctor = ctor;
    resource_types_table[TSRM_UNSHUFFLE_RSRC_ID(*rsrc_id)].dtor = dtor;
    resource_types_table[TSRM_UNSHUFFLE_RSRC_ID(*rsrc_id)].done = 0;

    /* enlarge the arrays for the already active threads */
    // PHP内核会接着遍历所有线程为每一个线程的 tsrm_tls_entry
    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) {
                        // 最后对 p->storage[j] 地址存放的全局变量进行初始化,
                        // 这里 ts_allocate_ctor 函数的第二个参数不知道为什么预留,整个项目中实际都未用到过,对比PHP7发现第二个参数也的确已经移除了
                        resource_types_table[j].ctor(p->storage[j], &p->storage);
                    }
                }
                p->count = id_count;
            }
            p = p->next;
        }
    }

    // 取消线程互斥锁
    tsrm_mutex_unlock(tsmm_mutex);

    TSRM_ERROR((TSRM_ERROR_LEVEL_CORE, "Successfully allocated new resource id %d", *rsrc_id));
    return *rsrc_id;
}

当通过 ts_allocate_id 函数分配全局资源 ID 时,PHP
内核会先加上互斥锁,确保生成的资源 ID
的唯一,这里锁的功能是在时刻维度将现出的始末变成串行,因为并发的有史以来问题就是光阴的题目。当加锁将来,id_count
自增,生成一个资源 ID,生成资源 ID 后,就会给当下资源 ID
分配存储的职位, 每一个资源都会蕴藏在 resource_types_table
中,当一个新的资源被分配时,就会创制一个 tsrm_resource_type。 所有
tsrm_resource_type 以数组的方法结合
tsrm_resource_table,其下标就是以此资源的 ID。 其实我们可以将
tsrm_resource_table 看做一个 HASH 表,key 是资源 ID,value 是
tsrm_resource_type 结构(任何一个数组都得以看做一个 HASH
表,假如数组的key 值有意义的话)。

ca88官网,在分配了资源 ID 后,PHP 内核会接着遍历所有线程为每一个线程的
tsrm_tls_entry 分配那个线程全局变量需要的内存空间。
这里每个线程全局变量的大小在各自的调用处指定(也就是全局变量结构体的轻重缓急)。最终对地点存放的全局变量举办起头化。为此我画了一张图予以注脚

ca88官网 1

上图中还有一个疑惑的地点,tsrm_tls_table 的要素是什么添加的,链表是什么实现的。大家把这么些题材先留着,后边会商量。

每五次的 ts_allocate_id 调用,PHP
内核都会遍历所无线程并为每一个线程分配相应资源,
如若这多少个操作是在PHP生命周期的乞求处理阶段展开,岂不是会再一次调用?

PHP 考虑了这种景象,ts_allocate_id 的调用在模块起首化时就调用了。

TSRM 启动后,在模块最先化过程中会遍历每个扩大的模块开端化方法,
扩张的全局变量在壮大的兑现代码起初阐明,在 MINIT 方法中初叶化。
其在起始化时会知会 TSRM
申请的全局变量以及大小,那里所谓的打招呼操作实际就是前边所说的
ts_allocate_id 函数。 TSRM
在内存池中分配并注册,然后将资源ID重返给增加。

二、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 这一个都是一个线程一个资源。

TSRM_API int tsrm_startup(int expected_threads, int expected_resources, int debug_level, char *debug_filename)
{
    /* code... */

    tsrm_tls_table_size = expected_threads; // SAPI 初始化时预计分配的线程数,一般都为1

    tsrm_tls_table = (tsrm_tls_entry **) calloc(tsrm_tls_table_size, sizeof(tsrm_tls_entry *));

    /* code... */

    id_count=0;

    resource_types_table_size = expected_resources; // SAPI 初始化时预先分配的资源表大小,一般也为1

    resource_types_table = (tsrm_resource_type *) calloc(resource_types_table_size, sizeof(tsrm_resource_type));

    /* code... */

    return 1;
}

简洁出里面完成的五个首要的行事,起头化了 tsrm_tls_table
链表、resource_types_table 数组,以及
id_count。而这两个全局变量是具无线程共享的,实现了线程间的内存管理的一致性。

从效用域上来说,C语言可以定义4种不同的变量:全局变量,静态全局变量,局部变量,静态局部变量。

tsrm_ls 的初步化

tsrm_ls 通过 ts_resource(0) 先导化。展开实际最后调用的是 ts_resource_ex(0,NULL) 。下面将 ts_resource_ex 一些宏展开,线程以 pthread 为例。

#define THREAD_HASH_OF(thr,ts)  (unsigned long)thr%(unsigned long)ts

static MUTEX_T tsmm_mutex;

void *ts_resource_ex(ts_rsrc_id id, THREAD_T *th_id)
{
    THREAD_T thread_id;
    int hash_value;
    tsrm_tls_entry *thread_resources;

    // tsrm_tls_table 在 tsrm_startup 已初始化完毕
    if(tsrm_tls_table) {
        // 初始化时 th_id = NULL;
        if (!th_id) {

            //第一次为空 还未执行过 pthread_setspecific 所以 thread_resources 指针为空
            thread_resources = pthread_getspecific(tls_key);

            if(thread_resources){
                TSRM_SAFE_RETURN_RSRC(thread_resources->storage, id, thread_resources->count);
            }

            thread_id = pthread_self();
        } else {
            thread_id = *th_id;
        }
    }
    // 上锁
    pthread_mutex_lock(tsmm_mutex);

    // 直接取余,将其值作为数组下标,将不同的线程散列分布在 tsrm_tls_table 中
    hash_value = THREAD_HASH_OF(thread_id, tsrm_tls_table_size);
    // 在 SAPI 调用 tsrm_startup 之后,tsrm_tls_table_size = expected_threads
    thread_resources = tsrm_tls_table[hash_value];

    if (!thread_resources) {
        // 如果还没,则新分配。
        allocate_new_resource(&tsrm_tls_table[hash_value], thread_id);
        // 分配完毕之后再执行到下面的 else 区间
        return ts_resource_ex(id, &thread_id);
    } else {
         do {
            // 沿着链表逐个匹配
            if (thread_resources->thread_id == thread_id) {
                break;
            }
            if (thread_resources->next) {
                thread_resources = thread_resources->next;
            } else {
                // 链表的尽头仍然没有找到,则新分配,接到链表的末尾
                allocate_new_resource(&thread_resources->next, thread_id);
                return ts_resource_ex(id, &thread_id);
            }
         } while (thread_resources);
    }

    TSRM_SAFE_RETURN_RSRC(thread_resources->storage, id, thread_resources->count);

    // 解锁
    pthread_mutex_unlock(tsmm_mutex);

}

而 allocate_new_resource 则是为新的线程在相应的链表中分配内存,并且将具有的全局变量都投入到其 storage 指针数组中。

static void allocate_new_resource(tsrm_tls_entry **thread_resources_ptr, THREAD_T thread_id)
{
    int i;

    (*thread_resources_ptr) = (tsrm_tls_entry *) malloc(sizeof(tsrm_tls_entry));
    (*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;

    // 设置线程本地存储变量。在这里设置之后,再到 ts_resource_ex 里取
    pthread_setspecific(*thread_resources_ptr);

    if (tsrm_new_thread_begin_handler) {
        tsrm_new_thread_begin_handler(thread_id, &((*thread_resources_ptr)->storage));
    }

    for (i=0; i<id_count; i++) {
        if (resource_types_table[i].done) {
            (*thread_resources_ptr)->storage[i] = NULL;
        } else {
            // 为新增的 tsrm_tls_entry 节点添加 resource_types_table 的资源
            (*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], &(*thread_resources_ptr)->storage);
            }
        }
    }

    if (tsrm_new_thread_end_handler) {
        tsrm_new_thread_end_handler(thread_id, &((*thread_resources_ptr)->storage));
    }

    pthread_mutex_unlock(tsmm_mutex);
}

地点有一个知识点,Thread Local Storage
,现在有一全局变量 tls_key,所无线程都能够动用它,改变它的值。
表面上看起来这是一个全局变量,所无线程都足以使用它,而它的值在每一个线程中又是单独存储的。那就是线程本地存储的意思。
那么哪些贯彻线程本地存储吗?

亟待联合 tsrm_startupts_resource_exallocate_new_resource 函数并配以注释一起举例表达:

// 以 pthread 为例
// 1. 首先定义了 tls_key 全局变量
static pthread_key_t tls_key;

// 2. 然后在 tsrm_startup 调用 pthread_key_create() 来创建该变量
pthread_key_create( &tls_key, 0 ); 

// 3. 在 allocate_new_resource 中通过 tsrm_tls_set 将 *thread_resources_ptr 指针变量存入了全局变量 tls_key 中
tsrm_tls_set(*thread_resources_ptr);// 展开之后为 pthread_setspecific(*thread_resources_ptr);

// 4. 在 ts_resource_ex 中通过 tsrm_tls_get() 获取在该线程中设置的 *thread_resources_ptr 
//    多线程并发操作时,相互不会影响。
thread_resources = tsrm_tls_get();

在知晓了 tsrm_tls_table 数组和其中链表的创办之后,再看 ts_resource_ex 函数中调用的这些重返宏

#define TSRM_SAFE_RETURN_RSRC(array, offset, range)     \
    if (offset==0) {                                    \
        return &array;                                  \
    } else {                                            \
        return array[TSRM_UNSHUFFLE_RSRC_ID(offset)];   \
    }

就是基于传入 tsrm_tls_entry 和 storage 的数组下标 offset ,然后回来该全局变量在该线程的 storage数组中的地址。到这边就了然了在多线程中收获全局变量宏 TSRMG 宏定义了。

实际上这在大家写扩充的时候会时不时应用:

#define TSRMLS_D void ***tsrm_ls   /* 不带逗号,一般是唯一参数的时候,定义时用 */
#define TSRMLS_DC , TSRMLS_D       /* 也是定义时用,不过参数前面有其他参数,所以需要个逗号 */
#define TSRMLS_C tsrm_ls
#define TSRMLS_CC , TSRMLS_C

NOTICE 写扩充的时候也许过多同桌都分不清楚到底用哪一个,通过宏展开我们得以见到,他们各自是带逗号和不带逗号,以及表达及调用,那么瑞典语中“D”就是代表:Define,而
后边的”C”是 Comma,逗号,前边的”C”就是Call。

上述为ZTS模式下的概念,非ZTS情势下其定义全体为空。

一、缘起TSRM

在多线程系统中,进程保留着资源所有权的性能,而五个冒出执行流是执行在经过中运作的线程。
如 Apache2 中的
worker,主控制进程生成三个子进程,每个子进程中蕴藏固定的线程数,各类线程独立地拍卖请求。
同样,为了不在请求到来时再生成线程,MinSpareThreads 和 马克斯SpareThreads
设置了最少和最多的空闲线程数; 而 马克斯Clients
设置了所有子进程中的线程总数。假若现有子进程中的线程总数无法满意负荷,控制过程将派生新的子进程。

当 PHP 运行在如上好像的多线程服务器时,此时的 PHP
处在多线程的生命周期中。
在早晚的时日内,一个过程空间中会存在四个线程,同一进程中的多个线程公用模块着手化后的全局变量,
若是和 PHP 在 CLI
情势下一样运行脚本,则六个线程会猜度读写一些储存在过程内存空间的公家资源(如在五个线程公用的模块起头化后的函数外会存在较多的全局变量),

那会儿那些线程访问的内存地址空间相同,当一个线程修改时,会影响其他线程,那种共享会提高部分操作的快慢,
不过三个线程间就发生了较大的耦合,并且当五个线程并发时,就会发出广泛的多寡一致性问题或资源竞争等并发常见问题,
比如多次周转结果和单线程运行的结果不等同。假若每个线程中对全局变量、静态变量唯有读操作,而无写操作,则这几个个全局变量就是线程安全的,只是这种景色不太现实。

为釜底抽薪线程的出现问题,PHP 引入了 TSRM: 线程安全资源管理器(Thread Safe
Resource Manager)。 TRSM 的落实代码在 PHP 源码的 /TSRM
目录下,调用随处可见,通常,我们誉为 TSRM 层。 一般的话,TSRM
层只会在被指明需要的时候才会在编译时启用(比如,Apache2+worker
MPM,一个基于线程的MPM), 因为 Win32 下的 Apache
来说,是基于多线程的,所以这一个层在 Win32 下连续被翻开的。

参考资料

 

本文来源:https://github.com/zhoumengkang/tipi/blob/master/book/chapt08/08-03-zend-thread-safe-in-php.markdown?spm=5176.100239.blogcont60787.4.Mvv5xg&file=08-03-zend-thread-safe-in-php.markdown

上面仅从函数功用域的角度解析一下两样的变量,假若所有变量注解不重名。

  • 全局变量,在函数外申明,例如,int gVar;。全局变量,所有函数共享,在此外地点出现这么些变量名都是指这个变量

  • 静态全局变量(static sgVar),其实也是怀有函数共享,不过那一个会有编译器的范围,算是编译器提供的一种效应

  • 一些变量(函数/块内的int var;),不共享,函数的往往推行中提到的这么些变量都是并行独立的,他们只是重名的不同变量而已

  • 一对静态变量(函数中的static int sVar;),本函数间共享,函数的每五回实践中关系的这些变量都是以此同一个变量

上边三种成效域都是从函数的角度来定义效率域的,可以满足所有咱们对单线程编程中变量的共享境况。
现在大家来分析一下多线程的场地。在多线程中,两个线程共享除函数调用栈之外的任何资源。
由此下边两种功能域从概念来看就成为了。

  • 全局变量,所有函数共享,由此有着的线程共享,不同线程中出现的例外变量都是这同一个变量

  • 静态全局变量,所有函数共享,也是持无线程共享

  • 部分变量,此函数的各次执行中涉嫌的这一个变量没有关联,由此,也是各样线程间也是不共享的

  • 静态局部变量,本函数间共享,函数的历次执行涉及的这一个变量都是同一个变量,由此,各种线程是共享的

相关文章