協(xié)程
webman是基于workerman開發(fā)的,所以webman可以使用workerman的協(xié)程特性。
協(xié)程支持Swoole
Swow
和Fiber
三種驅(qū)動(dòng)。
前提條件
- PHP >= 8.1
- Workerman >= 5.1.0 (
composer require workerman/workerman ~v5.1
) - webman-framework >= 2.1 (
composer require workerman/webman-framework ~v2.1
) - 安裝了swoole或者swow擴(kuò)展,或者安裝了
composer require revolt/event-loop
(Fiber) - 協(xié)程默認(rèn)是關(guān)閉的,需要單獨(dú)設(shè)置eventLoop開啟
開啟方法
webman支持為不同的進(jìn)程開啟不同的驅(qū)動(dòng),所以你可以在config/process.php
中通過eventLoop
配置協(xié)程驅(qū)動(dòng):
return [
'webman' => [
'handler' => Http::class,
'listen' => 'http://0.0.0.0:8787',
'count' => 1,
'user' => '',
'group' => '',
'reusePort' => false,
'eventLoop' => '', // 默認(rèn)為空自動(dòng)選擇Select或者Event,不開啟協(xié)程
'context' => [],
'constructor' => [
'requestClass' => Request::class,
'logger' => Log::channel('default'),
'appPath' => app_path(),
'publicPath' => public_path()
]
],
'my-coroutine' => [
'handler' => Http::class,
'listen' => 'http://0.0.0.0:8686',
'count' => 1,
'user' => '',
'group' => '',
'reusePort' => false,
// 開啟協(xié)程需要設(shè)置為 Workerman\Events\Swoole::class 或者 Workerman\Events\Swow::class 或者 Workerman\Events\Fiber::class
'eventLoop' => Workerman\Events\Swoole::class,
'context' => [],
'constructor' => [
'requestClass' => Request::class,
'logger' => Log::channel('default'),
'appPath' => app_path(),
'publicPath' => public_path()
]
]
// ... 其它配置省略 ...
];
提示
webman可以為不同的進(jìn)程設(shè)置不同的eventLoop,這意味著你可以選擇性的為特定進(jìn)程開啟協(xié)程。
例如上面配置中8787端口的服務(wù)沒有開啟協(xié)程,8686端口的服務(wù)開啟了協(xié)程,配合nginx轉(zhuǎn)發(fā)可以實(shí)現(xiàn)協(xié)程和非協(xié)程混合部署。
協(xié)程示例
<?php
namespace app\controller;
use support\Response;
use Workerman\Coroutine;
use Workerman\Timer;
class IndexController
{
public function index(): Response
{
Coroutine::create(function(){
Timer::sleep(1.5);
echo "hello coroutine\n";
});
return response('hello webman');
}
}
當(dāng)eventLoop
為Swoole
Swow
Fiber
時(shí),webman會(huì)為每個(gè)請(qǐng)求創(chuàng)建一個(gè)協(xié)程來運(yùn)行,在處理請(qǐng)求時(shí)可以繼續(xù)創(chuàng)建新的協(xié)程執(zhí)行業(yè)務(wù)代碼。
協(xié)程限制
- 當(dāng)使用Swoole Swow為驅(qū)動(dòng)時(shí),業(yè)務(wù)遇到阻塞IO協(xié)程會(huì)自動(dòng)切換,能實(shí)現(xiàn)同步代碼異步執(zhí)行。
- 當(dāng)使用Fiber驅(qū)動(dòng)時(shí),遇到阻塞IO時(shí),協(xié)程不會(huì)發(fā)生切換,進(jìn)程進(jìn)入阻塞狀態(tài)。
- 使用協(xié)程時(shí),不能多個(gè)協(xié)程同時(shí)對(duì)同一個(gè)資源進(jìn)行操作,例如數(shù)據(jù)庫(kù)連接,文件操作等,這可能會(huì)引起資源競(jìng)爭(zhēng),正確的用法是使用連接池或者鎖來保護(hù)資源。
- 使用協(xié)程時(shí),不能將請(qǐng)求相關(guān)的狀態(tài)數(shù)據(jù)存儲(chǔ)在全局變量或者靜態(tài)變量中,這可能會(huì)引起全局?jǐn)?shù)據(jù)污染,正確的用法是使用協(xié)程上下文
context
來存取它們。
其它注意事項(xiàng)
Swow底層會(huì)自動(dòng)hook php的阻塞函數(shù),但是因?yàn)檫@種hook影響了PHP的某些默認(rèn)行為,所以在你沒有使用Swow但卻裝了Swow時(shí)可能會(huì)產(chǎn)生bug。
所以建議:
- 如果你的項(xiàng)目沒有使用Swow時(shí),請(qǐng)不要安裝Swow擴(kuò)展
- 如果你的項(xiàng)目使用了Swow,請(qǐng)?jiān)O(shè)置
eventLoop
為Workerman\Events\Swow::class
協(xié)程上下文
協(xié)程環(huán)境禁止將請(qǐng)求相關(guān)的狀態(tài)信息存儲(chǔ)在全局變量或者靜態(tài)變量中,因?yàn)檫@可能會(huì)導(dǎo)致全局變量污染,例如
<?php
namespace app\controller;
use support\Request;
use Workerman\Timer;
class TestController
{
protected static $name = '';
public function index(Request $request)
{
static::$name = $request->get('name');
Timer::sleep(5);
return static::$name;
}
}
注意
協(xié)程環(huán)境下并非禁止使用全局變量或靜態(tài)變量,而是禁止使用全局變量或靜態(tài)變量存儲(chǔ)請(qǐng)求相關(guān)的狀態(tài)數(shù)據(jù)。
例如全局配置、數(shù)據(jù)庫(kù)連接、一些類的單例等需要全局共享的對(duì)象數(shù)據(jù)是推薦用全局變量或靜態(tài)變量存儲(chǔ)的。
將進(jìn)程數(shù)設(shè)置為1,當(dāng)我們連續(xù)發(fā)起兩個(gè)請(qǐng)求時(shí)
http://127.0.0.1:8787/test?name=lilei
http://127.0.0.1:8787/test?name=hanmeimei
我們期望兩個(gè)請(qǐng)求返回的結(jié)果分別是 lilei
和 hanmeimei
,但實(shí)際上返回的都是hanmeimei
。
這是因?yàn)榈诙€(gè)請(qǐng)求將靜態(tài)變量$name
覆蓋了,第一個(gè)請(qǐng)求睡眠結(jié)束時(shí)返回時(shí)靜態(tài)變量$name
已經(jīng)成為hanmeimei
。
正確但方法應(yīng)該是使用context存儲(chǔ)請(qǐng)求狀態(tài)數(shù)據(jù)
<?php
namespace app\controller;
use support\Request;
use support\Context;
use Workerman\Timer;
class TestController
{
public function index(Request $request)
{
Context::set('name', $request->get('name'));
Timer::sleep(5);
return Context::get('name');
}
}
support\Context
類用于存儲(chǔ)協(xié)程上下文數(shù)據(jù),當(dāng)協(xié)程執(zhí)行完畢后,相應(yīng)的context數(shù)據(jù)會(huì)自動(dòng)刪除。
協(xié)程環(huán)境里,因?yàn)槊總€(gè)請(qǐng)求都是單獨(dú)的協(xié)程,所以當(dāng)請(qǐng)求完成時(shí)context數(shù)據(jù)會(huì)自動(dòng)銷毀。
非協(xié)程環(huán)境里,context會(huì)在請(qǐng)求結(jié)束時(shí)會(huì)自動(dòng)銷毀。
局部變量不會(huì)造成數(shù)據(jù)污染
<?php
namespace app\controller;
use support\Request;
use support\Context;
use Workerman\Timer;
class TestController
{
public function index(Request $request)
{
$name = $request->get('name');
Timer::sleep(5);
return $name;
}
}
因?yàn)?code>$name是局部變量,協(xié)程之間無(wú)法互相訪問局部變量,所以使用局部變量是協(xié)程安全的。
Locker 鎖
有時(shí)候一些組件或者業(yè)務(wù)沒有考慮到協(xié)程環(huán)境,可能會(huì)出現(xiàn)資源競(jìng)爭(zhēng)或原子性問題,這時(shí)候可以使用Workerman\Locker
加鎖來實(shí)現(xiàn)排隊(duì)處理,防止并發(fā)問題。
<?php
namespace app\controller;
use Redis;
use support\Response;
use Workerman\Coroutine\Locker;
class IndexController
{
public function index(): Response
{
static $redis;
if (!$redis) {
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
}
// 如果不加鎖,Swoole下會(huì)觸發(fā)類似 "Socket#10 has already been bound to another coroutine#10" 錯(cuò)誤
// Swow下可能會(huì)觸發(fā)coredump
// Fiber下因?yàn)镽edis擴(kuò)展是同步阻塞IO,所以不會(huì)有問題
Locker::lock('redis');
$time = $redis->time();
Locker::unlock('redis');
return json($time);
}
}
Parallel 并發(fā)執(zhí)行
當(dāng)我們需要并發(fā)執(zhí)行多個(gè)任務(wù)并獲取結(jié)果時(shí),可以使用Workerman\Parallel
來實(shí)現(xiàn)。
<?php
namespace app\controller;
use support\Response;
use Workerman\Coroutine\Parallel;
class IndexController
{
public function index(): Response
{
$parallel = new Parallel();
for ($i=1; $i<5; $i++) {
$parallel->add(function () use ($i) {
// Do something
return $i;
});
}
$results = $parallel->wait();
return json($results); // Response: [1,2,3,4]
}
}
Pool 連接池
多個(gè)協(xié)程共用同一個(gè)連接會(huì)導(dǎo)致數(shù)據(jù)混亂,所以需要使用連接池來管理數(shù)據(jù)庫(kù)、redis等連接資源。
webman已經(jīng)提供了 webman/database webman/redis webman/cache webman/think-orm webman/think-cache等組件,它們都集成了連接池,支持在協(xié)程和非協(xié)程環(huán)境下使用。
如果你想改造一個(gè)沒有連接池的組件,可以使用Workerman\Pool
來實(shí)現(xiàn),參考如下代碼。
數(shù)據(jù)庫(kù)組件
<?php
namespace app;
use Workerman\Coroutine\Context;
use Workerman\Coroutine;
use Workerman\Coroutine\Pool;
class Db
{
private static ?Pool $pool = null;
public static function __callStatic($name, $arguments)
{
if (self::$pool === null) {
self::initializePool();
}
// 從協(xié)程上下文中獲取連接,保證同一個(gè)協(xié)程使用同一個(gè)連接
$pdo = Context::get('pdo');
if (!$pdo) {
// 從連接池中獲取連接
$pdo = self::$pool->get();
Context::set('pdo', $pdo);
// 當(dāng)協(xié)程結(jié)束時(shí),自動(dòng)歸還連接
Coroutine::defer(function () use ($pdo) {
self::$pool->put($pdo);
});
}
return call_user_func_array([$pdo, $name], $arguments);
}
private static function initializePool(): void
{
// 創(chuàng)建一個(gè)連接池,最大連接數(shù)為10
self::$pool = new Pool(10);
// 設(shè)置連接創(chuàng)建器(為了簡(jiǎn)潔,省略了配置文件讀取)
self::$pool->setConnectionCreator(function () {
return new \PDO('mysql:host=127.0.0.1;dbname=your_database', 'your_username', 'your_password');
});
// 設(shè)置連接關(guān)閉器
self::$pool->setConnectionCloser(function ($pdo) {
$pdo = null;
});
// 設(shè)置心跳檢測(cè)器
self::$pool->setHeartbeatChecker(function ($pdo) {
$pdo->query('SELECT 1');
});
}
}
使用
<?php
namespace app\controller;
use support\Response;
use app\Db;
class IndexController
{
public function index(): Response
{
$value = Db::query('SELECT NOW() as now')->fetchAll();
return json($value); // [{"now":"2025-02-06 23:41:03","0":"2025-02-06 23:41:03"}]
}
}
更多協(xié)程及相關(guān)組件介紹
協(xié)程與非協(xié)程混合部署
webman支持協(xié)程和非協(xié)程混合部署,例如非協(xié)程處理普通業(yè)務(wù),協(xié)程處理慢IO業(yè)務(wù),通過nginx轉(zhuǎn)發(fā)請(qǐng)求到不同的服務(wù)上。
例如 config/process.php
return [
'webman' => [
'handler' => Http::class,
'listen' => 'http://0.0.0.0:8787',
'count' => 1,
'user' => '',
'group' => '',
'reusePort' => false,
'eventLoop' => '', // 默認(rèn)為空自動(dòng)選擇Select或者Event,不開啟協(xié)程
'context' => [],
'constructor' => [
'requestClass' => Request::class,
'logger' => Log::channel('default'),
'appPath' => app_path(),
'publicPath' => public_path()
]
],
'my-coroutine' => [
'handler' => Http::class,
'listen' => 'http://0.0.0.0:8686',
'count' => 1,
'user' => '',
'group' => '',
'reusePort' => false,
// 開啟協(xié)程需要設(shè)置為 Workerman\Events\Swoole::class 或者 Workerman\Events\Swow::class 或者 Workerman\Events\Fiber::class
'eventLoop' => Workerman\Events\Swoole::class,
'context' => [],
'constructor' => [
'requestClass' => Request::class,
'logger' => Log::channel('default'),
'appPath' => app_path(),
'publicPath' => public_path()
]
],
// ... 其它配置省略 ...
];
然后通過nginx配置轉(zhuǎn)發(fā)請(qǐng)求到不同的服務(wù)上
upstream webman {
server 127.0.0.1:8787;
keepalive 10240;
}
# 新增一個(gè)8686 upstream
upstream task {
server 127.0.0.1:8686;
keepalive 10240;
}
server {
server_name webman.com;
listen 80;
access_log off;
root /path/webman/public;
# 以/tast開頭的請(qǐng)求走8686端口,請(qǐng)按實(shí)際情況將/tast更改為你需要的前綴
location /tast {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://task;
}
# 其它請(qǐng)求走原8787端口
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_set_header Connection "";
if (!-f $request_filename){
proxy_pass http://webman;
}
}
}