《PHP中的線程安全》要點(diǎn):
本文介紹了PHP中的線程安全,希望對您有用。如果有疑問,可以聯(lián)系我們。
緣起TSRM
在多線程系統(tǒng)中,進(jìn)程保留著資源所有權(quán)的屬性,而多個(gè)并發(fā)執(zhí)行流是執(zhí)行在進(jìn)程中運(yùn)行的線程.如Apache2 中的woker,主控制進(jìn)程生成多個(gè)子進(jìn)程,每個(gè)子進(jìn)程中包含固定的線程數(shù),各個(gè)線程獨(dú)立地處理哀求.同樣,為了不在哀求到來時(shí)再生成線程,MinSpareThreads和MaxSpareThreads設(shè)置了最少和最多的空閑線程數(shù);而MaxClients設(shè)置了所有子進(jìn)程中的線程總數(shù).如果現(xiàn)有子進(jìn)程中的線程總數(shù)不能滿足負(fù)載,控制進(jìn)程將派生新的子進(jìn)程.
當(dāng)PHP運(yùn)行在如上類似的多線程服務(wù)器時(shí),此時(shí)的PHP處在多線程的生命周期中.在一定的時(shí)間內(nèi),一個(gè)進(jìn)程空間中會存在多個(gè)線程,同一進(jìn)程中的多個(gè)線程公用模塊初始化后的全局變量,如果和PHP在CLI模式下一樣運(yùn)行腳本,則多個(gè)線程會試圖讀寫一些存儲在進(jìn)程內(nèi)存空間的公共資源(如在多個(gè)線程公用的模塊初始化后的函數(shù)外會存在較多的全局變量),
此時(shí)這些線程訪問的內(nèi)存地址空間相同,當(dāng)一個(gè)線程修改時(shí),會影響其它線程,這種共享會提高一些操作的速度,但是多個(gè)線程間就產(chǎn)生了較大的耦合,并且當(dāng)多個(gè)線程并發(fā)時(shí),就會產(chǎn)生常見的數(shù)據(jù)一致性問題或資源競爭等并發(fā)常見問題,比如多次運(yùn)行結(jié)果和單線程運(yùn)行的結(jié)果不一樣.如果每個(gè)線程中對全局變量、靜態(tài)變量只有讀操作,而無寫操作,則這些個(gè)全局變量就是線程平安的,只是這種情況不太現(xiàn)實(shí).
為解決線程的并發(fā)問題,PHP引入了TSRM: 線程平安資源管理器(Thread Safe Resource Manager). TRSM 的實(shí)現(xiàn)代碼在 PHP 源碼的 /TSRM 目錄下,調(diào)用隨處可見,通常,我們稱之為 TSRM 層.一般來說,TSRM 層只會在被指明需要的時(shí)候才會在編譯時(shí)啟用(比如,Apache2+worker MPM,一個(gè)基于線程的MPM),因?yàn)閃in32下的Apache來說,是基于多線程的,所以這個(gè)層在Win32下總是被開啟的.
TSRM的實(shí)現(xiàn)
進(jìn)程保留著資源所有權(quán)的屬性,線程做并發(fā)訪問,PHP中引入的TSRM層關(guān)注的是對共享資源的訪問,這里的共享資源是線程之間共享的存在于進(jìn)程的內(nèi)存空間的全局變量.當(dāng)PHP在單進(jìn)程模式下時(shí),一個(gè)變量被聲明在任何函數(shù)之外時(shí),就成為一個(gè)全局變量.
PHP解決并發(fā)的思路非常簡單,既然存在資源競爭,那么直接規(guī)避掉此問題,將多個(gè)資源直接復(fù)制多份,多個(gè)線程競爭的全局變量在進(jìn)程空間中各自都有一份,各做各的,完全隔離.以標(biāo)準(zhǔn)的數(shù)組擴(kuò)展為例,首先會聲明當(dāng)前擴(kuò)展的全局變量,然后在模塊初始化時(shí)會調(diào)用全局變量初始化宏初始化array的,比如分配內(nèi)存空間操作.
這里的聲明和初始化操作都是區(qū)分ZTS和非ZTS,對于非ZTS的情況,直接就是聲明變量,初始化變量.對于ZTS情況,PHP內(nèi)核會添加TSRM,對應(yīng)到這里的代碼就是聲明時(shí)不再是聲明全局變量,而是用ts_rsrc_id代碼,初始化是不再是初始化變量,而是調(diào)用ts_allocate_id函數(shù)在多線程環(huán)境中給當(dāng)前這個(gè)模塊申請一個(gè)全局變量并返回資源ID.
資源ID變量名由模塊名和global_id組成.它是一個(gè)自增的整數(shù),整個(gè)進(jìn)程會共享這個(gè)變量,在進(jìn)程SAPI初始調(diào)用,初始化TSRM環(huán)境時(shí), id_count作為一個(gè)靜態(tài)變量將被初始化為0.這是一個(gè)非常簡單的實(shí)現(xiàn),自增.確保了資源不會沖突,每個(gè)線程的獨(dú)立.
資源id的分配
當(dāng)通過ts_allocate_id函數(shù)分配全局資源ID時(shí),PHP內(nèi)核會鎖一下,確保生成的資源ID的唯一,這里鎖的作用是在時(shí)間維度將并發(fā)的內(nèi)容變成串行,因?yàn)椴l(fā)的根本問題就是時(shí)間的問題.
當(dāng)加鎖以后,id_count自增,生成一個(gè)資源ID,生成資源ID后,就會給當(dāng)前資源ID分配存儲的位置,每一個(gè)資源都會存儲在 resource_types_table 中,當(dāng)一個(gè)新的資源被分配時(shí),就會創(chuàng)建一個(gè)tsrm_resource_type.每次所有tsrm_resource_type以數(shù)組的方式組成tsrm_resource_table,其下標(biāo)就是這個(gè)資源的ID.其實(shí)我們可以將tsrm_resource_table看做一個(gè)HASH表,key是資源ID,value是tsrm_resource_type結(jié)構(gòu).只是,任何一個(gè)數(shù)組都可以看作一個(gè)HASH表,如果數(shù)組的key值有意義的話. resource_types_table的定義如下:
typedef struct {
在分配了資源ID后,PHP內(nèi)核會接著遍歷所有線程為每一個(gè)線程的tsrm_tls_entry分配這個(gè)線程全局變量需要的內(nèi)存空間.這里每個(gè)線程全局變量的大小在各自的調(diào)用處指定.
每一次的ts_allocate_id調(diào)用,PHP內(nèi)核都會遍歷所有線程并為每一個(gè)線程分配相應(yīng)資源,如果這個(gè)操作是在PHP生命周期的哀求處理階段進(jìn)行,豈不是會重復(fù)調(diào)用?
PHP考慮了這種情況,ts_allocate_id的調(diào)用在模塊初始化時(shí)就調(diào)用了.
在模塊初始化階段,通過SAPI調(diào)用tsrm_startup啟動TSRM, tsrm_startup函數(shù)會傳入兩個(gè)非常重要的參數(shù),一個(gè)是expected_threads,表示預(yù)期的線程數(shù),一個(gè)是expected_resources,表示預(yù)期的資源數(shù).不同的SAPI有不同的初始化值,比如mod_php5,cgi這些都是一個(gè)線程一個(gè)資源.
TSRM啟動后,在模塊初始化過程中會遍歷每個(gè)擴(kuò)展的模塊初始化方法,擴(kuò)展的全局變量在擴(kuò)展的實(shí)現(xiàn)代碼開頭聲明,在MINIT方法中初始化.其在初始化時(shí)會知會TSRM申請的全局變量以及大小,這里所謂的知會操作其實(shí)就是前面所說的ts_allocate_id函數(shù). TSRM在內(nèi)存池中分配并注冊,然后將資源ID返回給擴(kuò)展.后續(xù)每個(gè)線程通過資源ID定位全局變量,比如我們前面提到的數(shù)組擴(kuò)展,如果要調(diào)用當(dāng)前擴(kuò)展的全局變量,則使用:ARRAYG(v),這個(gè)宏的定義:
#ifdef ZTS#define ARRAYG(v) TSRMG(array_globals_id, zend_array_globals *, v)#else#define ARRAYG(v) (array_globals.v)#endif
如果是非ZTS則直接調(diào)用全局變量的屬性字段,如果是ZTS,則需要通過TSRMG獲取變量.
TSRMG的定義:
#define TSRMG(id, type, element) (((type) (*((void ***) tsrm_ls))[TSRM_UNSHUFFLE_RSRC_ID(id)])->element)
去掉這一堆括號,TSRMG宏的意思就是從tsrm_ls中按資源ID獲取全局變量,并返回對應(yīng)變量的屬性字段.
那么現(xiàn)在的問題是這個(gè)tsrm_ls從哪里來的?
其實(shí)這在我們寫擴(kuò)展的時(shí)候會經(jīng)常用到:
#define TSRMLS_D void ***tsrm_ls#define TSRMLS_DC , TSRMLS_D#define TSRMLS_C tsrm_ls#define TSRMLS_CC , TSRMLS_C
以上為ZTS模式下的定義,非ZTS模式下其定義全部為空.
最后個(gè)問題,tsrm_ls是從什么時(shí)候開始出現(xiàn)的,從哪里來?要到哪里去?
答案就在php_module_startup函數(shù)中,在PHP內(nèi)核的模塊初始化時(shí),如果是ZTS模式,則會定義一個(gè)局部變量tsrm_ls,這就是我們線程平安開始的地方.從這里開始,在每個(gè)需要的地方通過在函數(shù)參數(shù)中以宏的形式帶上這個(gè)參數(shù),實(shí)現(xiàn)線程的平安.
維易PHP培訓(xùn)學(xué)院每天教你實(shí)戰(zhàn)技能,PHP、MYSQL、LINUX、APP、JS,CSS全面培養(yǎng)人才。
轉(zhuǎn)載請注明本頁網(wǎng)址:
http://www.snjht.com/jiaocheng/6512.html