## 緣起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 {
size_t size;//資源的大小
ts_allocate_ctor ctor;//構造方法指針
ts_allocate_dtor dtor;//析構方法指針
int done;
} tsrm_resource_type;
在分配了資源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,這就是我們線程安全開始的地方。從這里開始,在每個需要的地方通過在函數參數中以宏的形式帶上這個參數,實現線程的安全。
## 參考資料[]()
- [究竟什么是TSRMLS_CC?- 54chen](http://www.54chen.com/php-tech/what-is-tsrmls_cc.html)
- [深入研究PHP及Zend Engine的線程安全模型](http://blog.codinglabs.org/articles/zend-thread-safety.html)
- 第一章 準備工作和背景知識
- 第一節 環境搭建
- 第二節 源碼結構、閱讀代碼方法
- 第三節 常用代碼
- 第四節 小結
- 第二章 用戶代碼的執行
- 第一節 生命周期和Zend引擎
- 第二節 SAPI概述
- Apache模塊
- 嵌入式
- FastCGI
- 第三節 PHP腳本的執行
- 詞法分析和語法分析
- opcode
- opcode處理函數查找
- 第四節 小結
- 第三章 變量及數據類型
- 第一節 變量的結構和類型
- 哈希表(HashTable)
- PHP的哈希表實現
- 鏈表簡介
- 第二節 常量
- 第三節 預定義變量
- 第四節 靜態變量
- 第五節 類型提示的實現
- 第六節 變量的生命周期
- 變量的賦值和銷毀
- 變量的作用域
- global語句
- 第七節 數據類型轉換
- 第八節 小結
- 第四章 函數的實現
- 第一節 函數的內部結構
- 函數的內部結構
- 函數間的轉換
- 第二節 函數的定義,傳參及返回值
- 函數的定義
- 函數的參數
- 函數的返回值
- 第三節 函數的調用和執行
- 第四節 匿名函數及閉包
- 第五節 小結
- 第五章 類和面向對象
- 第一節 類的結構和實現
- 第二節 類的成員變量及方法
- 第三節 訪問控制的實現
- 第四節 類的繼承,多態及抽象類
- 第五節 魔術方法,延遲綁定及靜態成員
- 第六節 PHP保留類及特殊類
- 第七節 對象
- 第八節 命名空間
- 第九節 標準類
- 第十節 小結
- 第六章 內存管理
- 第一節 內存管理概述
- 第二節 PHP中的內存管理
- 第三節 內存使用:申請和銷毀
- 第四節 垃圾回收
- 新的垃圾回收
- 第五節 內存管理中的緩存
- 第六節 寫時復制(Copy On Write)
- 第七節 內存泄漏
- 第八節 小結
- 第七章 Zend虛擬機
- 第一節 Zend虛擬機概述
- 第二節 語法的實現
- 詞法解析
- 語法分析
- 實現自己的語法
- 第三節 中間代碼的執行
- 第四節 PHP代碼的加密解密
- 第五節 小結
- 第八章 線程安全
- 第二節 線程,進程和并發
- 第三節 PHP中的線程安全
- 第九章 錯誤和異常處理
- 第十章 輸出緩沖
- 第十六章 PHP語言特性的實現
- 第一節 循環語句
- foreach的實現
- 第二十章 怎么樣系列(how to)
- 附錄
- 附錄A PHP及Zend API
- 附錄B PHP的歷史
- 附錄C VLD擴展使用指南
- 附錄D 怎樣為PHP貢獻
- 附錄E phpt測試文件說明
- 附錄F PHP5.4新功能升級解析
- 附錄G:re2c中文手冊