|
線程安全
線程安全問題,一言以蔽之就是多線程環(huán)境下如何安全存取公共資源。我們知道,每個(gè)線程只擁有一個(gè)私有棧,共享所屬進(jìn)程的堆。在C中,當(dāng)一個(gè)變量被聲明在任何函數(shù)之外時(shí),就成為一個(gè)全局變量,這時(shí)這個(gè)變量會(huì)被分配到進(jìn)程的共享存儲(chǔ)空間,不同線程都引用同一個(gè)地址空間,因此一個(gè)線程如果修改了這個(gè)變量,就會(huì)影響到全部線程。這看似為線程共享數(shù)據(jù)提供了便利,但是php往往是每個(gè)線程處理一個(gè)請求,因此希望每個(gè)線程擁有一個(gè)全局變量的副本,而不希望請求間相互干擾。 早期的php往往用于單線程環(huán)境,每個(gè)進(jìn)程只啟動(dòng)一個(gè)線程,因此不存在線程安全問題。后來出現(xiàn)了多線程環(huán)境下使用php的場景,因此Zend引入了Zend線程安全機(jī)制(Zend Thread Safety,簡稱ZTS)用于保證線程的安全。
ZTS的基本原理及實(shí)現(xiàn)
基本思想
說起來ZTS的基本思想是很直觀的,不是就是需要每個(gè)全局變量在每個(gè)線程都擁有一個(gè)副本嗎?那我就提供這樣的機(jī)制: 在多線程環(huán)境下,申請全局變量不再是簡單聲明一個(gè)變量,而是整個(gè)進(jìn)程在堆上分配一塊內(nèi)存空間用作“線程全局變量池”,在進(jìn)程啟動(dòng)時(shí)初始化這個(gè)內(nèi)存池,每當(dāng)有線程需要申請全局變量時(shí),通過相應(yīng)方法調(diào)用TSRM(Thread Safe Resource Manager,ZTS的具體實(shí)現(xiàn))并傳遞必要的參數(shù)(如變量大小等等),TSRM負(fù)責(zé)在內(nèi)存池中分配相應(yīng)內(nèi)存區(qū)塊并將這塊內(nèi)存的引用標(biāo)識(shí)返回,這樣下次這個(gè)線程需要讀寫此變量時(shí),就可以通過將唯一的引用標(biāo)識(shí)傳遞給TSRM,TSRM將負(fù)責(zé)真正的讀寫操作。這樣就實(shí)現(xiàn)了線程安全的全局變量。下圖給出了ZTS原理的示意圖:

數(shù)據(jù)結(jié)構(gòu)
TSRM中比較重要的數(shù)據(jù)結(jié)構(gòu)有兩個(gè):tsrm_tls_entry和tsrm_resource_type。下面先看tsrm_tls_entry。 tsrm_tls_entry定義在TSRM/TSRM.c中:
復(fù)制代碼 代碼如下:
typedef struct _tsrm_tls_entry tsrm_tls_entry;
struct _tsrm_tls_entry {
void **storage;
int count;
THREAD_T thread_id;
tsrm_tls_entry *next;
}
每個(gè)tsrm_tls_entry結(jié)構(gòu)負(fù)責(zé)表示一個(gè)線程的所有全局變量資源,其中thread_id存儲(chǔ)線程ID,count記錄全局變量數(shù),next指向下一個(gè)節(jié)點(diǎn)。storage可以看做指針數(shù)組,其中每個(gè)元素是一個(gè)指向本節(jié)點(diǎn)代表線程的一個(gè)全局變量。最終各個(gè)線程的tsrm_tls_entry被組成一個(gè)鏈表結(jié)構(gòu),并將鏈表頭指針賦值給一個(gè)全局靜態(tài)變量tsrm_tls_table。注意,因?yàn)閠srm_tls_table是一個(gè)貨真價(jià)實(shí)的全局變量,所以所有線程會(huì)共享這個(gè)變量,這就實(shí)現(xiàn)了線程間的內(nèi)存管理一致性。tsrm_tls_entry和tsrm_tls_table結(jié)構(gòu)的示意圖如下:

復(fù)制代碼 代碼如下:
typedef struct {
size_t size;
ts_allocate_ctor ctor;
ts_allocate_dtor dtor;
int done;
}
tsrm_resource_type;上文說過tsrm_tls_entry是以線程為單位的(每個(gè)線程一個(gè)節(jié)點(diǎn)),而tsrm_resource_type以資源(或者說全局變量)為單位,每次一個(gè)新的資源被分配時(shí),就會(huì)創(chuàng)建一個(gè)tsrm_resource_type。所有tsrm_resource_type以數(shù)組(線性表)的方式組成tsrm_resource_table,其下標(biāo)就是這個(gè)資源的ID。每個(gè)tsrm_resource_type存儲(chǔ)了此資源的大小和構(gòu)造、析構(gòu)方法指針。某種程度上,tsrm_resource_table可以看做是一個(gè)哈希表,key是資源ID,value是tsrm_resource_type結(jié)構(gòu)。
實(shí)現(xiàn)細(xì)節(jié)
這一小節(jié)分析TSRM一些算法的實(shí)現(xiàn)細(xì)節(jié)。因?yàn)檎麄€(gè)TSRM涉及代碼比較多,這里揀其中具有代表性的兩個(gè)函數(shù)分析。 第一個(gè)值得注意的是tsrm_startup函數(shù),這個(gè)函數(shù)在進(jìn)程起始階段被sapi調(diào)用,用于初始化TSRM的環(huán)境。由于tsrm_startup略長,這里摘錄出我認(rèn)為應(yīng)該注意的地方:
復(fù)制代碼 代碼如下:
/* Startup TSRM (call once for the entire process) */
TSRM_API int tsrm_startup(int expected_threads, int expected_resources, int debug_level, char *debug_filename)
{
/* code... */
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;
}
/* code... */
return 1;
}
其實(shí)tsrm_startup的主要任務(wù)就是初始化上文提到的兩個(gè)數(shù)據(jù)結(jié)構(gòu)。第一個(gè)比較有意思的是它的前兩個(gè)參數(shù):expected_threads和expected_resources。這兩個(gè)參數(shù)由sapi傳入,表示預(yù)計(jì)的線程數(shù)和資源數(shù),可以看到tsrm_startup會(huì)按照這兩個(gè)參數(shù)預(yù)先分配空間(通過calloc)。因此TSRM會(huì)首先分配可容納expected_threads個(gè)線程和expected_resources個(gè)資源的。要看各個(gè)sapi默認(rèn)會(huì)傳入什么,可以看各個(gè)sapi的源碼(在sapi目錄下),我簡單看了一下:

php技術(shù):PHP及Zend Engine的線程安全模型分析,轉(zhuǎn)載需保留來源!
鄭重聲明:本文版權(quán)歸原作者所有,轉(zhuǎn)載文章僅為傳播更多信息之目的,如作者信息標(biāo)記有誤,請第一時(shí)間聯(lián)系我們修改或刪除,多謝。