標籤:細節 一段 contents 長度 ipc file 機制 道具 col
前言
前些天幫同事查一個問題,第一次接觸到了 PHP 的多線程,原以為 PHP 普遍都是單執行緒模式,並不適合多線程領域,花些時間翻了幾個多線程的項目源碼之後,發現 PHP 的多線程也頗有可取之處,活用起來,用來解決某些問題竟然非常適合。
於是找了幾篇文章看了下 PHP 多線程 TSRM 機制的實現,也有所收穫,詳情可以查看下面的參考文章。本文對比多進程介紹了下多線程的優勢和適用情境,提出了一種巧用方案,並使用 PHP 代碼實現了多線程的常見用法。
文章歡迎轉載,但請註明來源:http://www.cnblogs.com/zhenbianshu/p/7978835.html, 謝謝。
多線程線程
首先說下線程:
線程(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 簡單易用的一貫風格,卻無法完全發揮多線程的能力。不過各個語言各有特色和側重點,也不必強求,愛她就要包容她 =_=。
最近在重學作業系統和 Linux 核心方面的知識,對程式的認知有了很大提升,感覺非常有必要總結一下,敬請期待。
關於本文有什麼問題可以在下面留言交流,如果您覺得本文對您有協助,可以點擊下面的 推薦 支援一下我,部落格一直在更新,歡迎 關注 。
參考:
深入研究PHP及Zend Engine的安全執行緒模型
PHP進階編程之多線程
多線程編程 - PHP 實現