《PHP中的線程安全》要點:
本文介紹了PHP中的線程安全,希望對您有用。如果有疑問,可以聯系我們。
緣起TSRM
在多線程系統中,進程保留著資源所有權的屬性,而多個并發執行流是執行在進程中運行的線程.如Apache2 中的woker,主控制進程生成多個子進程,每個子進程中包含固定的線程數,各個線程獨立地處理哀求.同樣,為了不在哀求到來時再生成線程,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在單進程模式下時,一個變量被聲明在任何函數之外時,就成為一個全局變量.
PHP解決并發的思路非常簡單,既然存在資源競爭,那么直接規避掉此問題,將多個資源直接復制多份,多個線程競爭的全局變量在進程空間中各自都有一份,各做各的,完全隔離.以標準的數組擴展為例,首先會聲明當前擴展的全局變量,然后在模塊初始化時會調用全局變量初始化宏初始化array的,比如分配內存空間操作.
這里的聲明和初始化操作都是區分ZTS和非ZTS,對于非ZTS的情況,直接就是聲明變量,初始化變量.對于ZTS情況,PHP內核會添加TSRM,對應到這里的代碼就是聲明時不再是聲明全局變量,而是用ts_rsrc_id代碼,初始化是不再是初始化變量,而是調用ts_allocate_id函數在多線程環境中給當前這個模塊申請一個全局變量并返回資源ID.
資源ID變量名由模塊名和global_id組成.它是一個自增的整數,整個進程會共享這個變量,在進程SAPI初始調用,初始化TSRM環境時, id_count作為一個靜態變量將被初始化為0.這是一個非常簡單的實現,自增.確保了資源不會沖突,每個線程的獨立.
資源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值有意義的話. resource_types_table的定義如下:
typedef struct {
在分配了資源ID后,PHP內核會接著遍歷所有線程為每一個線程的tsrm_tls_entry分配這個線程全局變量需要的內存空間.這里每個線程全局變量的大小在各自的調用處指定.
每一次的ts_allocate_id調用,PHP內核都會遍歷所有線程并為每一個線程分配相應資源,如果這個操作是在PHP生命周期的哀求處理階段進行,豈不是會重復調用?
PHP考慮了這種情況,ts_allocate_id的調用在模塊初始化時就調用了.
在模塊初始化階段,通過SAPI調用tsrm_startup啟動TSRM, tsrm_startup函數會傳入兩個非常重要的參數,一個是expected_threads,表示預期的線程數,一個是expected_resources,表示預期的資源數.不同的SAPI有不同的初始化值,比如mod_php5,cgi這些都是一個線程一個資源.
TSRM啟動后,在模塊初始化過程中會遍歷每個擴展的模塊初始化方法,擴展的全局變量在擴展的實現代碼開頭聲明,在MINIT方法中初始化.其在初始化時會知會TSRM申請的全局變量以及大小,這里所謂的知會操作其實就是前面所說的ts_allocate_id函數. TSRM在內存池中分配并注冊,然后將資源ID返回給擴展.后續每個線程通過資源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從哪里來的?
其實這在我們寫擴展的時候會經常用到:
#define TSRMLS_D void ***tsrm_ls#define TSRMLS_DC , TSRMLS_D#define TSRMLS_C tsrm_ls#define TSRMLS_CC , TSRMLS_C
以上為ZTS模式下的定義,非ZTS模式下其定義全部為空.
最后個問題,tsrm_ls是從什么時候開始出現的,從哪里來?要到哪里去?
答案就在php_module_startup函數中,在PHP內核的模塊初始化時,如果是ZTS模式,則會定義一個局部變量tsrm_ls,這就是我們線程平安開始的地方.從這里開始,在每個需要的地方通過在函數參數中以宏的形式帶上這個參數,實現線程的平安.
維易PHP培訓學院每天教你實戰技能,PHP、MYSQL、LINUX、APP、JS,CSS全面培養人才。