Coroutines
webman được phát triển dựa trên Workerman, do đó webman có thể sử dụng đặc điểm coroutines của Workerman.
Coroutines hỗ trợ ba driver: Swoole
, Swow
và Fiber
.
Điều kiện tiên quyết
- PHP >= 8.1
- Workerman >= 5.1.0 (
composer require workerman/workerman ~v5.1
) - webman-framework >= 2.1 (
composer require workerman/webman-framework ~v2.1
) - Đã cài đặt mở rộng swoole hoặc swow, hoặc cài đặt
composer require revolt/event-loop
(Fiber) - Coroutines mặc định bị tắt, cần thiết lập riêng
eventLoop
để mở.
Cách mở
webman hỗ trợ mở các driver khác nhau cho các tiến trình khác nhau, do đó bạn có thể cấu hình driver coroutines thông qua eventLoop
trong config/process.php
:
return [
'webman' => [
'handler' => Http::class,
'listen' => 'http://0.0.0.0:8787',
'count' => 1,
'user' => '',
'group' => '',
'reusePort' => false,
'eventLoop' => '', // Mặc định để trống tự động chọn Select hoặc Event, không mở coroutines
'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,
// Mở coroutines cần thiết lập thành Workerman\Events\Swoole::class hoặc Workerman\Events\Swow::class hoặc Workerman\Events\Fiber::class
'eventLoop' => Workerman\Events\Swoole::class,
'context' => [],
'constructor' => [
'requestClass' => Request::class,
'logger' => Log::channel('default'),
'appPath' => app_path(),
'publicPath' => public_path()
]
]
// ... Các cấu hình khác được bỏ qua ...
];
Mẹo
webman có thể thiết lậpeventLoop
khác nhau cho các tiến trình khác nhau, điều này có nghĩa là bạn có thể lựa chọn mở coroutine cho các tiến trình cụ thể.
Ví dụ, dịch vụ trên cổng 8787 trong cấu hình trên không mở coroutine, trong khi dịch vụ trên cổng 8686 đã mở coroutine, kết hợp với nginx chuyển tiếp có thể thực hiện triển khai hỗn hợp giữa coroutine và không coroutine.
Ví dụ về Coroutines
<?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');
}
}
Khi eventLoop
là Swoole
, Swow
hoặc Fiber
, webman sẽ tạo một coroutine cho mỗi yêu cầu để thực thi, trong quá trình xử lý yêu cầu có thể tiếp tục tạo các coroutine mới thực hiện mã nghiệp vụ.
Giới hạn về Coroutines
- Khi sử dụng Swoole hoặc Swow làm driver, nếu gặp phải IO bị chặn, coroutines sẽ tự động chuyển đổi, có thể thực hiện mã đồng bộ dưới dạng bất đồng bộ.
- Khi sử dụng driver Fiber, khi gặp IO bị chặn, coroutines sẽ không thực hiện chuyển đổi, tiến trình sẽ vào trạng thái bị chặn.
- Khi sử dụng coroutines, không thể thao tác nhiều coroutine cùng một lúc trên cùng một tài nguyên, như kết nối cơ sở dữ liệu, thao tác tệp, v.v., điều này có thể gây ra cạnh tranh tài nguyên; cách sử dụng đúng là sử dụng pool kết nối hoặc khóa để bảo vệ tài nguyên.
- Khi sử dụng coroutines, không thể lưu trữ dữ liệu trạng thái liên quan đến yêu cầu vào biến toàn cục hoặc biến tĩnh, điều này có thể gây ô nhiễm dữ liệu toàn cục; cách sử dụng đúng là sử dụng
context
của coroutine để lưu trữ và truy xuất chúng.
Những lưu ý khác
Swow tự động hook các hàm chặn của PHP, nhưng vì việc hook này ảnh hưởng đến một số hành vi mặc định của PHP, nên có thể xảy ra lỗi khi bạn không sử dụng Swow nhưng vẫn cài đặt Swow.
Vì vậy khuyên bạn:
- Nếu dự án của bạn không sử dụng Swow, vui lòng không cài đặt mở rộng Swow.
- Nếu dự án của bạn sử dụng Swow, hãy đặt
eventLoop
làWorkerman\Events\Swow::class
.
Ngữ cảnh Coroutine
Môi trường coroutine cấm lưu trữ thông tin trạng thái liên quan đến yêu cầu trong biến toàn cục hoặc biến tĩnh, vì điều này có thể dẫn đến ô nhiễm biến toàn cục, ví dụ:
<?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;
}
}
Lưu ý
Trong môi trường coroutine không cấm sử dụng biến toàn cục hoặc biến tĩnh, mà chỉ cấm sử dụng biến toàn cục hoặc biến tĩnh để lưu trữ dữ liệu trạng thái liên quan đến yêu cầu.
Ví dụ như cấu hình toàn cục, kết nối cơ sở dữ liệu, một số đối tượng như singleton cần chia sẻ toàn cục có thể được lưu trữ bằng biến toàn cục hoặc biến tĩnh.
Khi đặt số lượng tiến trình là 1, khi chúng ta liên tiếp gửi hai yêu cầu:
http://127.0.0.1:8787/test?name=lilei
http://127.0.0.1:8787/test?name=hanmeimei
Chúng ta hy vọng rằng kết quả trả về của hai yêu cầu lần lượt là lilei
và hanmeimei
, nhưng thực tế trả về đều là hanmeimei
.
Điều này là do yêu cầu thứ hai đã ghi đè biến tĩnh $name
, khi yêu cầu đầu tiên hoàn thành sau giấc ngủ, biến tĩnh $name
đã trở thành hanmeimei
.
Phương pháp đúng nhưng nên sử dụng context
để lưu trữ dữ liệu trạng thái yêu cầu
<?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');
}
}
Lớp support\Context
được sử dụng để lưu trữ dữ liệu ngữ cảnh của coroutine, khi coroutine hoàn thành, dữ liệu context tương ứng sẽ tự động bị xóa.
Trong môi trường coroutine, vì mỗi yêu cầu đều là một coroutine riêng biệt, nên khi yêu cầu hoàn thành, dữ liệu context sẽ tự động bị hủy.
Trong môi trường không phải coroutine, context sẽ tự động bị hủy khi yêu cầu kết thúc.
Biến cục bộ sẽ không gây ra ô nhiễm dữ liệu
<?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;
}
}
Vì $name
là biến cục bộ, các coroutine không thể truy cập vào biến cục bộ của nhau, vì vậy việc sử dụng biến cục bộ là an toàn trong coroutine.
Locker - Khóa
Đôi khi một số thành phần hoặc nghiệp vụ không xem xét đến môi trường coroutine có thể dẫn đến cạnh tranh tài nguyên hoặc vấn đề nguyên tử, lúc này có thể sử dụng Workerman\Locker
để khóa nhằm thực hiện xử lý theo hàng đợi và ngăn ngừa các vấn đề đồng thời.
<?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);
}
// Nếu không khóa, dưới Swoole sẽ gặp lỗi tương tự "Socket#10 has already been bound to another coroutine#10"
// Dưới Swow có thể gây ra coredump
// Dưới Fiber vì mở rộng Redis là IO chặn đồng bộ nên sẽ không gặp vấn đề
Locker::lock('redis');
$time = $redis->time();
Locker::unlock('redis');
return json($time);
}
}
Thực thi đồng thời
Khi chúng ta cần thực thi nhiều nhiệm vụ đồng thời và nhận kết quả, có thể sử dụng Workerman\Parallel
để thực hiệ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) {
// Làm việc gì đó
return $i;
});
}
$results = $parallel->wait();
return json($results); // Phản hồi: [1,2,3,4]
}
}
Pool - Kết nối Pool
Nhiều coroutine sử dụng cùng một kết nối có thể gây ra sự lộn xộn dữ liệu, do đó cần sử dụng pool kết nối để quản lý các tài nguyên kết nối như cơ sở dữ liệu, Redis, v.v.
webman đã cung cấp các thành phần như webman/database, webman/redis, webman/cache, webman/think-orm, webman/think-cache, tất cả đều đã tích hợp pool kết nối, hỗ trợ sử dụng trong môi trường coroutine và không phải coroutine.
Nếu bạn muốn cải tiến một thành phần không có pool kết nối, có thể sử dụng Workerman\Pool
để thực hiện, tham khảo mã sau.
Thành phần Cơ sở dữ liệu
<?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();
}
// Lấy kết nối từ ngữ cảnh coroutine, đảm bảo cùng một coroutine sử dụng cùng một kết nối
$pdo = Context::get('pdo');
if (!$pdo) {
// Lấy kết nối từ pool kết nối
$pdo = self::$pool->get();
Context::set('pdo', $pdo);
// Khi coroutine kết thúc, tự động trả lại kết nối
Coroutine::defer(function () use ($pdo) {
self::$pool->put($pdo);
});
}
return call_user_func_array([$pdo, $name], $arguments);
}
private static function initializePool(): void
{
// Tạo một pool kết nối, số lượng kết nối tối đa là 10
self::$pool = new Pool(10);
// Thiết lập trình tạo kết nối (để đơn giản, các bước đọc file cấu hình bị bỏ qua)
self::$pool->setConnectionCreator(function () {
return new \PDO('mysql:host=127.0.0.1;dbname=your_database', 'your_username', 'your_password');
});
// Thiết lập trình đóng kết nối
self::$pool->setConnectionCloser(function ($pdo) {
$pdo = null;
});
// Thiết lập trình kiểm tra sức khỏe
self::$pool->setHeartbeatChecker(function ($pdo) {
$pdo->query('SELECT 1');
});
}
}
Sử dụng
<?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"}]
}
}
Giới thiệu thêm về Coroutine và các thành phần liên quan
Tham khảo tài liệu Coroutine Workerman
Triển khai hỗn hợp giữa coroutine và không coroutine
webman hỗ trợ triển khai hỗn hợp giữa coroutine và không coroutine, ví dụ như không coroutine xử lý nghiệp vụ thông thường, coroutine xử lý nghiệp vụ IO chậm, thông qua nginx chuyển tiếp yêu cầu đến các dịch vụ khác nhau.
Ví dụ config/process.php
return [
'webman' => [
'handler' => Http::class,
'listen' => 'http://0.0.0.0:8787',
'count' => 1,
'user' => '',
'group' => '',
'reusePort' => false,
'eventLoop' => '', // Mặc định để trống tự động chọn Select hoặc Event, không mở coroutine
'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,
// Mở coroutine cần thiết lập thành Workerman\Events\Swoole::class hoặc Workerman\Events\Swow::class hoặc Workerman\Events\Fiber::class
'eventLoop' => Workerman\Events\Swoole::class,
'context' => [],
'constructor' => [
'requestClass' => Request::class,
'logger' => Log::channel('default'),
'appPath' => app_path(),
'publicPath' => public_path()
]
],
// ... Các cấu hình khác được bỏ qua ...
];
Sau đó cấu hình nginx để chuyển tiếp yêu cầu đến các dịch vụ khác nhau
upstream webman {
server 127.0.0.1:8787;
keepalive 10240;
}
# Thêm một upstream 8686
upstream task {
server 127.0.0.1:8686;
keepalive 10240;
}
server {
server_name webman.com;
listen 80;
access_log off;
root /path/webman/public;
# Các yêu cầu bắt đầu bằng /tast sẽ đi đến cổng 8686, hãy thay đổi /tast thành tiền tố bạn cần trên cơ sở thực tế
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;
}
# Các yêu cầu khác đi đến cổ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;
}
}
}