程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 網頁編程 >> PHP編程 >> 關於PHP編程 >> PHP 源碼學習之線程安全,php源碼線程

PHP 源碼學習之線程安全,php源碼線程

編輯:關於PHP編程

PHP 源碼學習之線程安全,php源碼線程


從作用域上來說,C語言可以定義4種不同的變量:全局變量,靜態全局變量,局部變量,靜態局部變量。

下面僅從函數作用域的角度分析一下不同的變量,假設所有變量聲明不重名。

  • 全局變量,在函數外聲明,例如,int gVar;。全局變量,所有函數共享,在任何地方出現這個變量名都是指這個變量

  • 靜態全局變量(static sgVar),其實也是所有函數共享,但是這個會有編譯器的限制,算是編譯器提供的一種功能

  • 局部變量(函數/塊內的int var;),不共享,函數的多次執行中涉及的這個變量都是相互獨立的,他們只是重名的不同變量而已

  • 局部靜態變量(函數中的static int sVar;),本函數間共享,函數的每一次執行中涉及的這個變量都是這個同一個變量

上面幾種作用域都是從函數的角度來定義作用域的,可以滿足所有我們對單線程編程中變量的共享情況。 現在我們來分析一下多線程的情況。在多線程中,多個線程共享除函數調用棧之外的其他資源。 因此上面幾種作用域從定義來看就變成了。

  • 全局變量,所有函數共享,因此所有的線程共享,不同線程中出現的不同變量都是這同一個變量

  • 靜態全局變量,所有函數共享,也是所有線程共享

  • 局部變量,此函數的各次執行中涉及的這個變量沒有聯系,因此,也是各個線程間也是不共享的

  • 靜態局部變量,本函數間共享,函數的每次執行涉及的這個變量都是同一個變量,因此,各個線程是共享的

一、緣起TSRM

在多線程系統中,進程保留著資源所有權的屬性,而多個並發執行流是執行在進程中運行的線程。 如 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 這些都是一個線程一個資源。

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。而這三個全局變量是所有線程共享的,實現了線程間的內存管理的一致性。

資源 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 值有意義的話)。

在分配了資源 ID 後,PHP 內核會接著遍歷所有線程為每一個線程的 tsrm_tls_entry 分配這個線程全局變量需要的內存空間。 這裡每個線程全局變量的大小在各自的調用處指定(也就是全局變量結構體的大小)。最後對地址存放的全局變量進行初始化。為此我畫了一張圖予以說明

圖8.2 PHP 線程安全示意圖

上圖中還有一個困惑的地方,tsrm_tls_table 的元素是如何添加的,鏈表是如何實現的。我們把這個問題先留著,後面會討論。

每一次的 ts_allocate_id 調用,PHP 內核都會遍歷所有線程並為每一個線程分配相應資源, 如果這個操作是在PHP生命周期的請求處理階段進行,豈不是會重復調用?

PHP 考慮了這種情況,ts_allocate_id 的調用在模塊初始化時就調用了。

TSRM 啟動後,在模塊初始化過程中會遍歷每個擴展的模塊初始化方法, 擴展的全局變量在擴展的實現代碼開頭聲明,在 MINIT 方法中初始化。 其在初始化時會知會 TSRM 申請的全局變量以及大小,這裡所謂的知會操作其實就是前面所說的 ts_allocate_id 函數。 TSRM 在內存池中分配並注冊,然後將資源ID返回給擴展。

全局變量的使用

以標准的數組擴展為例,首先會聲明當前擴展的全局變量。

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 從哪裡來的?

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模式下其定義全部為空。

參考資料

  • 究竟什麼是TSRMLS_CC?- 54chen
  • 深入研究PHP及Zend Engine的線程安全模型

 

本文來源於: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

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved