在PHP領域,多執行緒的概念並不像其他語言那麼為人所知。本以為PHP 一般都是單執行緒模型,並不適合多執行緒領域。在翻了一些多線程的專案原始碼之後,發現 PHP 的多線程也有不過的用處。活用起來,用來解決某些問題竟然非常適合。
多執行緒
執行緒
首先說下執行緒:
#執行緒(thread) 是作業系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以並發多個線程,每條線程並行執行不同的任務.
使用多線程主要是因為它在執行效率上有很大優勢。由於執行緒是作業系統能夠進行調度的最小單位:
一個多執行緒程式比單執行緒程式被作業系統調度的機率更大,所以多執行緒程式一般會比單執行緒程式更有效率;
多執行緒程式的多個執行緒可以在多核心CPU 的多個核心同時運行,可以將完全發揮機器多核心的優勢;
同時對比多進程程序,多執行緒有以下特點:
線程的創建和切換的系統開銷都比進程要小,所以一定程度上會比多進程更有效率;
線程天生的共享記憶體空間,線程間的通訊更簡單,避免了進程IPC引進新的複雜度。
適用場景
多執行緒的最佳化是很多,可是無腦使用多執行緒並不能提升程式的執行效率,因為執行緒的建立與銷毀、上下文切換、執行緒同步等也是有效能損耗的,耗費時間可能比順序執行的程式碼還多。如:
sumSmall是一個從1累加到50000的函數。
上圖是在主執行緒內執行了三次sumSmall 和三個執行緒分別執行sumSmall ,再將結果同步到一個執行緒的時間對比,我們會發現只在主執行緒執行的時間反而更短,三個執行緒創建、切換、同步的時間遠遠大過了執行緒非同步執行節省的時間。
而函數sumLarge 從1累加到5000000,下圖同一執行緒執行三次和三個執行緒執行的耗時:
這次,多執行緒終於有效率優勢了。
是否使用多執行緒還需要根據具體需求而定,一般考慮以下兩種情況:
I/O 阻塞會使作業系統發生任務調度,阻塞目前任務,所以程式碼中I/O 多的情況下,使用多執行緒時可以將程式碼並行。例如多次讀取整塊的文件,或請求多個網路資源。
多執行緒能充分利用 CPU,所以當多處大運算量程式碼時,也可以使用多執行緒讓他們並行執行,例如上文中後一個範例。
PHP中的多線程
PHP 預設不支援多線程,要使用多線程需要安裝pthread 擴展,而要安裝pthread 擴展,必須使用--enable-maintainer-zts 參數重新編譯PHP,這個參數是指定編譯PHP 時使用執行緒安全方式。
線程安全性
多執行緒是讓程式變得不安分的一個因素,在使用多執行緒之前,首先要考慮執行緒安全性問題:
執行緒安全:執行緒安全性是程式設計中的術語,指某個函數、函數庫在多執行緒環境中被呼叫時,能夠正確地處理多個執行緒之間的共享變量,使程式功能正確完成。
在傳統多執行緒中,由於多個執行緒共享變量,所以可能會導致出現如下問題:
#存在一個全域數組$arr = array('a');;
A 執行緒取得陣列長度為1;
B 執行緒取得陣列長度為1;
A 執行緒pop 出數組元素$a = array_pop($arr); $a = 'a';;
B 線程也pop 數組元素$b = array_pop($arr); $a = null;;
此時B 線程內就出現了靈異事件,明明陣列長度大於0,或沒有pop 出東西;
PHP 實作
PHP 實作的執行緒安全性主要是使用TSRM 機制對全域變數和靜態變數進行了隔離,將全域變數和靜態變數給每個執行緒都複製了一份,各執行緒使用的都是主執行緒的一個備份,從而避免了變數衝突,也就不會出現執行緒安全性問題。
PHP 對多執行緒的封裝保證了執行緒安全,程式設計師不用考慮對全域變數加各種鎖來避免讀寫衝突了,同時也減少了出錯的機會,寫出的程式碼更加安全。
但由此導致的是,子執行緒一旦開始運行,主執行緒便無法再對子執行緒運行細節進行調整了,執行緒一定程度上失去了執行緒之間透過全域變數進行訊息傳遞的能力。
同時PHP 開啟執行緒安全選項後,使用TSRM 機制指派和使用變數時也會有額外的損耗,所以在不需要多執行緒的PHP 環境中,使用PHP 的ZTS (非執行緒安全性) 版本就好。
類別和方法
PHP 將執行緒封裝成了Thread 類,執行緒的建立透過實例化一個執行緒物件來實現,由於類別的封裝性,變數的使用只能透過建構函式傳入,而執行緒運算結果也需要透過類別變數傳出。
下面介紹幾個常用的Thread 類別方法:
run():此方法是一个抽象方法,每个线程都要实现此方法,线程开始运行后,此方法中的代码会自动执行; start():在主线程内调用此方法以开始运行一个线程; join():各个线程相对于主线程都是异步执行,调用此方法会等待线程执行结束; kill():强制线程结束; isRunning():返回线程的运行状态,线程正在执行run()方法的代码时会返回 true;
因為線程安全的實現,PHP 的多線程開始運行後,無法再通過共享內存空間通信,線程也無法通過線程間通訊復用,所以我認為PHP 的「線程池」並沒有什麼意義。擴充內自帶的Pool 類別是一個對多執行緒分配管理的類,這裡也不再多介紹了。
實例程式碼
下面是一個執行緒類,用來請求某一介面。接下來根據它寫兩個多執行緒的應用實例:
class Request extends Thread { public $url; public $response; public function __construct($url) { $this->url = $url; } public function run() { $this->response = file_get_contents($this->url); } }
非同步請求
將同步的請求拆分為多個執行緒非同步調用,以提升程式的運作效率。
$chG = new Request("www.google.com"); $chB = new Request("www.baidu.com"); $chG ->start(); $chB ->start(); $chG->join(); $chB->join(); $gl = $chG->response; $bd = $chB->response;
超時控制
偶然間發現公司網站某一網頁上的一塊內容時有時無,不知道具體實現,但這給了我使用多線程的靈感:利用線程異步實現快速失敗和超時控制。
我們在使用 curl 請求某個位址時,可以透過 CURLOPT_CONNECTTIMEOUT / CURLOPT_TIMEOUT 參數分別設定 curl 的連線逾時時間和讀取資料逾時時間,但總的逾時時間不好控制。而且在進行資料庫查詢時的逾時時間無法設定(鳥哥部落格:為MySQL設定查詢逾時)。
這時我們便可以藉用多執行緒來實作此功能:執行執行緒類別的start() 方法後,不呼叫join() 方法,讓執行緒一直處於非同步狀態,不阻塞主執行緒的執行。
此時主線程相當於旗艦,而各子線程相當於巡航艦,旗艦到達某地後不必要一直等待巡航艦也歸來,等待一段時間後離開即可,從而避免巡航艦意外時旗艦白白空等。
程式碼:
$chG = new Request("www.google.com"); $chB = new Request("www.baidu.com"); $chG->start(); $chB->start(); $chB->join(); // 此处不对chG执行join方法 sleep(1); // sleep一个能接受的超时时间 $gl = $chG->response; $bd = $chB->response; $bd->kill(); if (!$gl) { $gl = ""; // 处理异常,或在线程类内给$gl一个默认值 }
總結
PHP 對多執行緒進行的封(yan)裝(ge),讓人用執行緒用得非常不盡興。雖然安全,也保持 PHP 簡單易用的一貫風格,卻無法完全發揮多執行緒的能力。不過各個語言各有特色和重點,也不必強求,愛她就要包容她 =_=。
相關推薦:
#以上是PHP對多執行緒程式設計的支援以及使用的詳細內容。更多資訊請關注PHP中文網其他相關文章!