# 1.4 線程安全
在PHP初期,是作為單進程的CGI來運行的,所以并沒有考慮線程安全問題。
我們可以隨意的在全局作用域中設置變量并在程序中對他進行修改、訪問,內核申請的資源如果沒有正確的釋放,
也會在CGI進程結束后自動地被清理干凈。
后來,php被作為apache多進程模式下的一個模塊運行,但是這仍然把php局限在一個進程里,
我們設置的全局變量,只要在每個請求之前將其正確的初始化,并在每個請求之后正確的清理干凈,
便不會帶來什么麻煩。由于對于一個進程來說,同一個時間只能處理一個請求,
所以這是內核中加入了針對每個請求的內存管理功能,來防止服務器資源利用出現錯誤。
隨著使用在多線程模式的軟件系統越來越多,php內核中亟需一種新的資源管理方式,
并最終在php內核中形成了一個新的抽象層:TSRM(Thread Safe Resource Management)。
## 線程安全與非線程安全
在一個沒有線程的程序中,我們往往傾向于把全局變量聲明在源文件的頂部,
編譯器會自動的為它分配資源供我們在聲明語句之下的程序邏輯中使用。
**(即使通過fork()出一個子進程,它也會重新申請一段內存,父子進程中的變量從此沒有了任何聯系)**
但是在一個多線程的程序中,如果我們需要每個線程都擁有自己獨立的資源的話,
便需要為每個線程獨立開辟出一個區域來存放它們各自的資源,
在使用資源的時候,每個線程便會只在自己的那一畝三分地里找,而不會拔了別人的莊稼。
## Thread-Safe Data Pools(線程安全的資源池?)
在擴展的Module Init里,擴展可以調用ts_allocate_id()來告訴TRSM自己需要多少資源。
TRSM接收后更新系統使用的資源,并得到一個指向剛分配的那份資源的id。
````c
typedef struct {
int sampleint;
char *samplestring;
} php_sample_globals;
int sample_globals_id;
PHP_MINIT_FUNCTION(sample)
{
ts_allocate_id(&sample_globals_id,
sizeof(php_sample_globals),
(ts_allocate_ctor) php_sample_globals_ctor,
(ts_allocate_dtor) php_sample_globals_dtor);
return SUCCESS;
}
````
當一個請求需要訪問數據段的時候,擴展從TSRM層請求當前線程的資源池,
以ts_allocate_id()返回的資源ID來獲取偏移量。
換句話說,在代碼流中,你可能會在前面所說的MINIT語句中碰到SAMPLE_G(sampleint) = 5;
這樣的語句。在線程安全的構建下,這個語句通過一些宏擴展如下:
````c
(((php_sample_globals*)(*((void ***)tsrm_ls))[sample_globals_id-1])->sampleint = 5;
````
如果你看不懂上面的轉換也不用沮喪,它已經很好的封裝在PHPAPI中了,以至于許多開發者都不需要知道它怎樣工作的。
## 當不在線程環境時
因為在PHP的線程安全構建中訪問全局資源涉及到在線程數據池查找對應的偏移量,這是一些額外的負載,結果就是它比對應的非線程方式(直接從編譯期已經計算好的真實的全局變量地址中取出數據)慢一些。
考慮上面的例子,這一次在非線程構建下:
````c
typedef struct {
int sampleint;
char *samplestring;
} php_sample_globals;
php_sample_globals sample_globals;
PHP_MINIT_FUNCTION(sample)
{
php_sample_globals_ctor(&sample_globals TSRMLS_CC);
return SUCCESS;
}
````
首先注意到的是這里并沒有定義一個int型的標識去引用全局的結構定義,
只是簡單的在進程的全局空間定義了一個結構體。
也就是說SAMPLE_G(sampleint) = 5;展開后就是sample_globals.sampleint = 5; 簡單,快速,高效。
非線程構建還有進程隔離的優勢,這樣給定的請求碰到完全出乎意料的情況時,它也不會影響其他進程,
即便是產生段錯誤也不會導致整個webserver癱瘓。
實際上,Apache的MaxRequestsPerChild指令就是設計用來提升這個特性的,
它經常性的有目的性的kill掉子進程并產生新的子進程,來避免某些可能由于進程長時間運行“累積”而來的問題(比如內存泄露)。
## 訪問全局變量
在創建一個擴展時,你并不知道它最終的運行環境是否是線程安全的。幸運的是,你要使用的標準包含文件集合中已經包含了條件定義的ZTS預處理標記。當PHP因為SAPI需要或通過enable-maintainer-zts選項安裝等原因以線程安全方式構建時,這個值會被自動的定義,并可以用一組#ifdef ZTS這樣的指令集去測試它的值。
就像你前面看到的,只有在PHP以線程安全方式編譯時,才會存在線程安全池,只有線程安全池存在時,才會真的在線程安全池中分配空間。這就是為什么前面的例子包裹在ZTS檢查中的原因,非線程方式供非線程構建使用。
在本章前面PHP_MINIT_FUNCTION(myextension)的例子中,你可以看到#ifdef ZTS被用作條件調用正確的全局初始代碼。對于ZTS模式它使用ts_allocate_id()彈出myextension_globals_id變量,而非ZTS模式只是直接調用myextension_globals的初始化方法。這兩個變量已經在你的擴展源文件中使用Zend宏:DECLARE_MODULE_GLOBALS(myextension)聲明,它將自動的處理對ZTS的測試并依賴構建的ZTS模式選擇正確的方式聲明。
在訪問這些全局變量的時候,你需要使用前面給出的自定義宏SAMPLE_G()。在第12章,你將學習到怎樣設計這個宏以使它可以依賴ZTS模式自動展開。
## 即便你不需要線程也要考慮線程
正常的PHP構建默認是關閉線程安全的,只有在被構建的sapi明確需要線程安全或線程安全在./configure階段顯式的打開時,才會以線程安全方式構建。
給出了全局查找的速度問題和進程隔離的缺點后,你可能會疑惑為什么明明不需要還有人故意打開它呢?這是因為,多數情況下,擴展和SAPI的開發者認為你是線程安全開關的操作者,這樣做可以很大程度上確保新代碼可以在所有環境中正常運行。
當線程安全啟用時,一個名為tsrm_ls的特殊指針被增加到了很多的內部函數原型中。這個指針允許PHP區分不同線程的數據。回想一下本章前面ZTS模式下的SAMPLE_G()宏函數中就使用了它。沒有它,正在執行的函數就不知道查找和設置哪個線程的符號表;不知道應該執行哪個腳本,引擎也完全無法跟蹤它的內部寄存器。這個指針保留了線程處理的所有頁面請求。
這個可選的指針參數通過下面一組定義包含到原型中。當ZTS禁用時,這些定義都被展開為空;當ZTS開啟時,它們展開如下:
````c
#define TSRMLS_D void ***tsrm_ls
#define TSRMLS_DC , void ***tsrm_ls
#define TSRMLS_C tsrm_ls
#define TSRMLS_CC , tsrm_ls
````
非ZTS構建對下面的代碼看到的是兩個參數:int, char *。在ZTS構建下,原型則包含三個參數:int, char *, void ***。當你的程序調用這個函數時,只有在ZTS啟用時才需要傳遞第三個參數。下面代碼的第二行展示了宏的展開:
````c
int php_myext_action(int action_id, char *message TSRMLS_DC);
php_myext_action(42, "The meaning of life" TSRMLS_CC);
````
通過在函數調用中包含這個特殊的變量,php_myext_action就可以使用tsrm_ls的值和MYEXT_G()宏函數一起訪問它的線程特有全局數據。在非ZTS構建上,tsrm_ls將不可用,但是這是ok的,因為此時MYEXT_G()宏函數以及其他類似的宏都不會使用它。
現在考慮,你在一個新的擴展上工作,并且有下面的函數,它可以在你本地使用CLI SAPI的構建上正常工作,并且即便使用apache 1的apxs SAPI編譯也可以正常工作:
````c
static int php_myext_isset(char *varname, int varname_len)
{
zval **dummy;
if (zend_hash_find(EG(active_symbol_table),
varname, varname_len + 1,
(void**)&dummy) == SUCCESS) {
/* Variable exists */
return 1;
} else {
/* Undefined variable */
return 0;
}
}
````
所有的一切看起來都工作正常,你打包這個擴展發送給他人構建并運行在生產服務器上。讓你氣餒的是,對方報告擴展編譯失敗。
事實上它們使用了Apache 2.0的線程模式,因此它們的php構建啟用了ZTS。當編譯期碰到你使用的EG()宏函數時,它嘗試在本地空間查找tsrm_ls沒有找到,因為你并沒有定義它并且沒有在你的函數中傳遞。
修復這個問題非常簡單;只需要在php_myext_isset()的定義上增加TSRMLS_DC,并在每行調用它的地方增加TSRMLS_CC。不幸的是,現在對方已經有點不信任你的擴展質量了,這樣就會推遲你的演示周期。這種問題越早解決越好。
現在有了enable-maintainer-zts指令。通過在./configure時增加該指令來構建php,你的構建將自動的包含ZTS,哪怕你當前的SAPI(比如CLI)不需要它。打開這個開關,你可以避免這些常見的不應該出現的錯誤。
注意:在PHP4中,enable-maintainer-zts標記等價的名字是enable-experimental-zts;請確認使用你的php版本對應的正確標記。
## 尋回丟失的tsrm_ls
有時,我們需要在一個函數中使用tsrm_ls指針,但卻不能傳遞它。通常這是因為你的擴展作為某個使用回調的庫的接口,它并沒有提供返回抽象指針的地方。考慮下面的代碼片段:
````c
void php_myext_event_callback(int eventtype, char *message)
{
zval *event;
/* $event = array('event'=>$eventtype,
'message'=>$message) */
MAKE_STD_ZVAL(event);
array_init(event);
add_assoc_long(event, "type", eventtype);
add_assoc_string(event, "message", message, 1);
/* $eventlog[] = $event; */
add_next_index_zval(EXT_G(eventlog), event);
}
PHP_FUNCTION(myext_startloop)
{
/* The eventlib_loopme() function,
* exported by an external library,
* waits for an event to happen,
* then dispatches it to the
* callback handler specified.
*/
eventlib_loopme(php_myext_event_callback);
}
````
雖然你可能不完全理解這段代碼,但你應該注意到了回調函數中使用了EXT_G()宏函數,我們知道在線程安全構建下它需要tsrm_ls指針。修改函數原型并不好也不應該這樣做,因為外部的庫并不知道php的線程安全模型。那這種情況下怎樣讓tsrm_ls可用呢?
解決方案是前面提到的名為TSRMLS_FETCH()的Zend宏函數。將它放到代碼片段的頂部,這個宏將執行給予當前線程上下文的查找,并定義本地的tsrm_ls指針拷貝。
這個宏可以在任何地方使用并且不用通過函數調用傳遞tsrm_ls,盡管這看起來很誘人,但是,要注意到這一點:TSRMLS_FETCH調用需要一定的處理時間。這在單次迭代中并不明顯,但是隨著你的線程數增多,隨著你調用TSRMLS_FETCH()的點的增多,你的擴展就會顯現出這個瓶頸。因此,請謹慎的使用它。
注意:為了和c++編譯器兼容,請確保將TSRMLS_FETCH()和所有變量定義放在給定塊作用域的頂部(任何其他語句之前)。因為TSRMLS_FETCH()宏自身有多種不同的解析方式,因此最好將它作為變量定義的最后一行。
## links
* [目錄](<preface.md>)
* 上一節: [PHP的生命周期](<1.3.md>)
* 下一節: [小結](<1.5.md>)
- about
- 開始閱讀
- 目錄
- 1 PHP的生命周期
- 1.讓我們從SAPI開始
- 2.PHP的啟動與終止
- 3.PHP的生命周期
- 4.線程安全
- 5.小結
- 2 PHP變量在內核中的實現
- 1. 變量的類型
- 2. 變量的值
- 3. 創建PHP變量
- 4. 變量的存儲方式
- 5. 變量的檢索
- 6. 類型轉換
- 7. 小結
- 3 內存管理
- 1. 內存管理
- 2. 引用計數
- 3. 總結
- 4 動手編譯PHP
- 1. 編譯前的準備
- 2. PHP編譯前的config配置
- 3. Unix/Linux平臺下的編譯
- 4. 在Win32平臺上編譯PHP
- 5. 小結
- 5 Your First Extension
- 1. 一個擴展的基本結構
- 2. 編譯我們的擴展
- 3. 靜態編譯
- 4. 編寫函數
- 5. 小結
- 6 函數返回值
- 1. 一個特殊的參數:return_value
- 2. 引用與函數的執行結果
- 3. 小結
- 7 函數的參數
- 1. zend_parse_parameters
- 2. Arg Info 與類型綁定
- 3. 小結
- 8 使用HashTable與{數組}
- 1. 數組(C中的)與鏈表
- 2. 操作HashTable的API
- 3. 在內核中操作PHP語言中數組
- 4. 小結
- 9 PHP中的資源類型
- 1. 復合類型的數據——{資源}
- 2. Persistent Resources
- 3. {資源}自有的引用計數
- 4. 小結
- 10 PHP中的面向對象(一)
- 1. zend_class_entry
- 2. 定義一個類
- 3. 定義一個接口
- 4. 類的繼承與接口的實現
- 5. 小結
- 11 PHP中的面向對象(二)
- 1. 生成對象的實例與調用方法
- 2. 讀寫對象的屬性
- 3. 小結
- 12 啟動與終止的那點事
- 2. 小結
- 1. 關于生命周期
- 2. MINFO與phpinfo
- 3. 常量
- 4. PHP擴展中的全局變量
- 5. PHP語言中的超級全局變量
- 6. 小結
- 13 INI設置
- 1. 聲明和訪問ini設置
- 2. 小結
- 2. 小結
- 14 流式訪問
- 1. 概覽
- 2. 打開流
- 3. 訪問流
- 4. 靜態資源操作
- 5. 小結
- 15 流的實現
- 1. php流的表象之下
- 2. 包裝器操作
- 3. 實現一個包裝器
- 4. 操縱
- 5. 檢查
- 6. 小結
- 16 有趣的流
- 1. 上下文
- 2. 過濾器
- 3. 小結
- 17 配置和鏈接
- 1. autoconf
- 2. 庫的查找
- 3. 強制模塊依賴
- 4. Windows方言
- 5. 小結
- 18 擴展生成
- 1. ext_skel
- 2. PECL_Gen
- 3. 小結
- 19 設置宿主環境
- 1. 嵌入式SAPI
- 2. 構建并編譯一個宿主應用
- 3. 通過嵌入包裝重新創建cli
- 4. 老技術新用
- 5. 小結
- 20 高級嵌入式
- 1. 回調到php中
- 2. 錯誤處理
- 3. 初始化php
- 4. 覆寫INI_SYSTEM和INI_PERDIR選項
- 5. 捕獲輸出
- 6. 同時擴展和嵌入
- 7. 小結
- 約定