PHP 多執行緒與非阻塞實戰:台灣工程師的務實選擇
前言
身為 PHP 工程師,我們常會遇到需要處理耗時操作的場景,例如發送大量郵件、處理圖片或進行複雜運算。如果這些操作都循序執行,很容易造成程式卡住,使用者體驗當然不好。這時候,「非阻塞」與「多執行緒」的概念就顯得非常重要了。
PHP 本身在設計上,和 Java 或 Python 這些語言相比,對於「真正的」多執行緒支援確實比較不一樣。不過別擔心,這不代表 PHP 無法實現非阻塞的平行處理。今天這篇文章,就是要跟大家分享幾種在 PHP 中實現多執行緒或非阻塞方法的實用技巧,希望能幫助大家在專案中更有效地處理高併發和耗時任務。
方法 1:利用 pthreads 擴充套件,實現「真」多執行緒
如果您真的需要「多執行緒」這個概念,pthreads 擴充套件是 PHP 裡面比較接近傳統定義的選擇。它允許您在 PHP 中建立和管理執行緒,讓多個任務同時運行。
安裝 pthreads
首先,要使用 pthreads,您的 PHP 環境必須啟用 ZTS (Zend Thread Safety)。這點很重要,可以用 php -i | grep Thread 簡單檢查一下。確認是 ZTS 版本後,就能透過 pecl 安裝:
pecl install pthreads
安裝完畢別忘了在 php.ini 裡面啟用它:
extension=pthreads.so
範例:實際跑看看 pthreads
下面這個範例,模擬了多個耗時任務同時執行,主程式不會被卡住:
<?php
class MyThread extends Thread {
private $id;
public function __construct($id) {
$this->id = $id;
}
public function run() {
// 模擬耗時任務,睡個幾秒
sleep(rand(1, 5));
echo "Thread {$this->id} finished!\n";
}
}
// 建立多個執行緒
$threads = [];
for ($i = 0; $i < 5; $i++) {
$threads[$i] = new MyThread($i);
$threads[$i]->start(); // 啟動執行緒
}
// 等待所有執行緒完成
foreach ($threads as $thread) {
$thread->join(); // 主執行緒會等子執行緒跑完
}
echo "All threads completed!\n";
?>
說明與注意事項
這個方法讓每個 MyThread 實例都獨立跑一個任務。start() 負責啟動,而 join() 則讓主程式等待子執行緒完成。這樣的好處是任務可以真正並行,不會互相阻塞。
不過,pthreads 也有一些需要留意的地方:
只適用於 CLI 環境:在 Apache 或 Nginx 這種 Web 環境下,
pthreads是不適合使用的。版本相容性:
pthreads在 PHP 7.4 之前的版本支援度比較好,PHP 8.x 後,官方對它的支援度就有限了,使用前建議確認版本。資料共享的眉角:多執行緒間資料共享需要特別小心,避免發生競爭條件 (race condition) 導致資料出錯。
方法 2:擁抱 Swoole 的協程,為高併發而生
對於現代 PHP 應用,尤其需要處理高併發或大量 I/O 操作的場景,Swoole 是一個非常值得投入的解決方案。它透過「協程 (Coroutine)」的概念,實現了比 pthreads 更輕量且高效的非阻塞異步處理。
安裝 Swoole
安裝 Swoole 很簡單,一樣透過 pecl 即可:
pecl install swoole
然後在 php.ini 啟用:
extension=swoole.so
範例:用 Swoole 協程處理非阻塞任務
以下範例展示如何利用 Swoole 協程來模擬非阻塞任務的執行:
<?php
use Swoole\Coroutine;
Co\run(function () {
// 模擬多個並行任務
for ($i = 0; $i < 5; $i++) {
Coroutine::create(function () use ($i) {
// 模擬耗時操作,例如 HTTP 請求或檔案讀寫
Coroutine\sleep(rand(1, 3));
echo "Task {$i} finished!\n";
});
}
});
echo "All tasks completed!\n";
?>
說明與優點
Swoole 的 Co\run 是協程的入口,而 Coroutine::create 則會建立一個協程。Coroutine\sleep 是一個非阻塞的睡眠函數,很適合用來模擬網路請求或資料庫操作這類耗時任務。
Swoole 的優點非常顯著:
輕量高效:協程比執行緒輕量許多,適合處理大量併發請求。
非阻塞 I/O:特別適合處理 HTTP 請求、資料庫查詢等 I/O 密集型操作。
支援 PHP 8.x:在 PHP 8.x 環境下,
Swoole的穩定性相當不錯。
不過,要入門 Swoole,您可能需要花點時間熟悉它的協程 API,因為它的程式設計模式會和傳統 PHP 有些不同。
方法 3:pcntl_fork 進程級非阻塞,老派但實用
如果您在 CLI 環境下,又不方便安裝 pthreads 或 Swoole,PHP 內建的 pcntl 擴充套件可以透過「進程分支 (fork)」來實現非阻塞。雖然不是執行緒,但也能達到類似的效果。
安裝 pcntl
pcntl 大部分 Linux 發行版都已經預裝,您可以用 php -m | grep pcntl 來檢查是否啟用。
範例:使用 pcntl_fork 處理非阻塞
<?php
$pid = pcntl_fork();
if ($pid == -1) {
die("Could not fork!\n");
} elseif ($pid) {
// 父進程
echo "Parent process, waiting for child...\n";
pcntl_wait($status); // 等待子進程完成
echo "Child process completed!\n";
} else {
// 子進程
sleep(3); // 模擬耗時任務
echo "Child process finished!\n";
exit(0);
}
?>
說明與注意事項
pcntl_fork 會建立一個子進程,讓父子進程獨立運行。子進程處理耗時任務,父進程則可以繼續做自己的事,或者等待子進程完成。
這個方法雖然能實現非阻塞,但有幾個限制:
僅限 CLI 環境:和
pthreads一樣,pcntl也只適用於命令列介面。Windows 不支援:在 Windows 環境下無法使用。
資源消耗較大:進程比執行緒「重」很多,資源開銷會比較大。
進程間通訊較複雜:如果父子進程需要互相傳遞資料,會需要處理較複雜的進程間通訊 (IPC) 機制。
方法 4:異步函數庫(ReactPHP / Amp),事件驅動的優雅
如果您不希望依賴額外的 PHP 擴充套件,或者主要處理的是 I/O 密集型任務,那麼基於事件循環 (Event Loop) 的異步函數庫,例如 ReactPHP 或 Amp,會是非常不錯的選擇。
安裝 ReactPHP
透過 Composer 即可輕鬆安裝:
composer require react/event-loop
範例:用 ReactPHP 實現非阻塞
<?php
require 'vendor/autoload.php';
use React\EventLoop\Factory;
$loop = Factory::create();
for ($i = 0; $i < 5; $i++) {
$loop->addTimer($i + 1, function () use ($i) {
echo "Task {$i} executed!\n";
});
}
$loop->run();
echo "All tasks completed!\n";
說明與優點
ReactPHP 的核心概念是事件循環。它透過 addTimer 這類方法,將任務排入事件佇列,然後事件循環會非阻塞地執行這些任務。在實際應用中,這可以用來處理非阻塞的 HTTP 請求、檔案操作或資料庫連線。
這種方法的優勢是:
輕量且免 ZTS:純 PHP 實現,無需特殊編譯或 ZTS 支援。
環境彈性:適用於 Web 和 CLI 環境。
擅長 I/O 密集型任務:非常適合處理需要等待外部資源響應的任務。
缺點是需要學習事件循環的程式設計模式,對於 CPU 密集型任務的效果也比較有限。
補充:Python 中的多執行緒與非同步
既然提到了 PHP 的多執行緒與非阻塞,也順便聊聊 Python 是怎麼處理這些問題的。Python 在這方面,與 PHP 有些不同,但同樣提供了強大的工具。
1. Python 的多執行緒 (Threading)
Python 內建的 threading 模組可以很方便地實現多執行緒。不過,有個特別的機制叫做 GIL (Global Interpreter Lock),會讓同一個 Python 進程中,即使有多個執行緒,也只能有一個執行緒在任何時間點執行 Python 位元碼。
這代表什麼呢?
適合 I/O 密集型任務:當任務涉及大量等待(例如網路請求、文件讀寫),GIL 會在等待時釋放,讓其他執行緒有機會運行,所以多執行緒能發揮作用。
不適合 CPU 密集型任務:因為 GIL 的存在,CPU 密集型任務無法真正利用多核心,效能提升不明顯。對於這類任務,Python 更推薦使用多進程。
範例:Python 多執行緒
import threading
import time
import random
class MyThread(threading.Thread):
def __init__(self, id):
super().__init__()
self.id = id
def run(self):
# 模擬耗時任務
sleep_time = random.randint(1, 5)
time.sleep(sleep_time)
print(f"Thread {self.id} finished after {sleep_time} seconds!")
# 建立多個執行緒
threads = []
for i in range(5):
thread = MyThread(i)
threads.append(thread)
thread.start() # 啟動執行緒
# 等待所有執行緒完成
for thread in threads:
thread.join() # 主執行緒等待子執行緒完成
print("All threads completed!")
2. Python 的多進程 (Multiprocessing)
為了突破 GIL 的限制,Python 提供了 multiprocessing 模組。它透過創建新的進程來實現真正的並行,每個進程都有自己的 Python 解釋器和記憶體空間,因此不受 GIL 的影響。
適合 CPU 密集型任務:可以充分利用多核心處理器,提升 CPU 密集型任務的效能。
進程間通訊:需要處理進程間的資料傳遞和同步問題。
範例:Python 多進程
import multiprocessing
import time
import random
def task(id):
# 模擬耗時任務
sleep_time = random.randint(1, 5)
time.sleep(sleep_time)
print(f"Process {id} finished after {sleep_time} seconds!")
# 建立多個進程
processes = []
for i in range(5):
p = multiprocessing.Process(target=task, args=(i,))
processes.append(p)
p.start() # 啟動進程
# 等待所有進程完成
for p in processes:
p.join() # 主進程等待子進程完成
print("All processes completed!")
3. Python 的異步 I/O (Asyncio)
Python 3.5 引入的 asyncio 模組,透過協程 (coroutine) 的概念實現了單執行緒的非同步 I/O。它的工作原理和 PHP 的 Swoole 或 ReactPHP 類似,都是基於事件循環來管理任務。
適合 I/O 密集型任務:非常高效,能夠同時處理大量非阻塞的 I/O 操作(如網路請求、資料庫查詢)。
單執行緒:不涉及多執行緒或多進程的複雜性,但不能利用多核心來加速 CPU 密集型任務。
程式碼可讀性:
async/await語法讓非同步程式碼看起來像同步程式碼一樣,提高了可讀性。
範例:Python Asyncio
import asyncio
import time
import random
async def async_task(id):
# 模擬非阻塞耗時操作
sleep_time = random.randint(1, 3)
print(f"Task {id} started, sleeping for {sleep_time} seconds...")
await asyncio.sleep(sleep_time) # 非阻塞等待
print(f"Task {id} finished!")
async def main():
tasks = []
for i in range(5):
tasks.append(async_task(i))
await asyncio.gather(*tasks) # 同時運行所有異步任務
print("Starting asyncio tasks...")
asyncio.run(main())
print("All asyncio tasks completed!")
Python 總結與建議
I/O 密集型任務:
asyncio是首選,它提供了最高效的非阻塞 I/O。threading也可以用,但asyncio在大規模併發上通常表現更好。CPU 密集型任務:
multiprocessing是最佳選擇,可以突破 GIL 限制,充分利用多核心。
總結與個人建議
看到這裡,您會發現 PHP 雖然沒有像其他語言那樣「開箱即用」的多執行緒,但透過不同的工具和方法,我們還是能很好地實現非阻塞的平行處理。同時,我們也了解到 Python 在處理多執行緒和異步時的不同策略。
簡單總結一下,給您一些務實的建議:
處理 I/O 密集型任務 (例如網路請求、資料庫操作):
PHP 環境下,首選
Swoole:如果您追求高效能和高併發,且環境允許,Swoole絕對是第一選擇。它在 PHP 8.x 上表現穩定,是現代 PHP 開發的利器。PHP 環境下,考慮
ReactPHP或Amp:如果您不希望引入擴充套件,或者專案規模較小,這兩個異步函數庫會更輕量且容易整合。Python 環境下,強烈推薦
asyncio:這是處理 Python I/O 密集型任務最現代且高效的方式。
處理 CPU 密集型任務 (例如複雜運算、圖片處理):
PHP 環境下,
pthreads(需 ZTS):如果真的需要「多執行緒」來利用多核心,且環境支援 ZTS,可以考慮。但要注意版本相容性和穩定性。PHP 環境下,
pcntl_fork(僅 CLI,非 Windows):如果您只是在 CLI 環境下做簡單的進程級分離,這個方法也可以。Python 環境下,使用
multiprocessing:這是 Python 中處理 CPU 密集型任務,真正利用多核心的最佳方法。
環境限制:
Web 環境 (Apache/Nginx):PHP 優先選擇
Swoole或ReactPHP。CLI 環境:PHP 的
pthreads、Swoole或pcntl均可;Python 的threading、multiprocessing或asyncio也都適用。Windows 環境:PHP 請避免使用
pcntl,優先選擇Swoole或ReactPHP;Python 的multiprocessing在 Windows 下有些行為差異,但基本功能可用。
一點小結
沒有最好的工具,只有最適合您專案需求的方案。在選擇時,除了考量效能,也別忘了團隊的熟悉度、專案的長期維護性,以及環境的限制。希望這篇文章能幫助您在 PHP 和 Python 的非阻塞與多執行緒之路走得更順遂!
參考資源:
pthreads文件:https://www.php.net/manual/en/book.pthreads.php Swoole官方網站:https://www.swoole.com/ ReactPHP官方網站:https://reactphp.org/ Amp官方網站:https://amphp.org/ Python
threading模組:https://docs.python.org/zh-cn/3/library/threading.html Python
multiprocessing模組:https://docs.python.org/zh-cn/3/library/multiprocessing.html Python
asyncio模組:https://docs.python.org/zh-cn/3/library/asyncio.html
沒有留言:
張貼留言