Coroutine
Coroutine là gì?
Hãy tưởng tượng bạn là một đầu bếp. Với cách làm truyền thống (blocking), bạn bỏ mì vào nước, rồi đứng nhìn nồi cho đến khi mì chín mới làm việc khác. Rất lãng phí thời gian.
Với coroutine, bạn bỏ mì vào nước, rồi trong lúc chờ mì chín bạn đi thái rau, xong lại quay lại vớt mì. Bạn vẫn chỉ là một người, nhưng làm được nhiều việc hơn bằng cách tận dụng thời gian chờ đợi.
Đó chính xác là cách coroutine hoạt động: một thread có thể xử lý nhiều coroutine đồng thời bằng cách tạm dừng coroutine đang chờ I/O và chạy coroutine khác.
Thread duy nhất:
─────────────────────────────────────────────────────────────>
Coroutine A: ██████░░░░░░░██████████░░░░░░░░░████
Coroutine B: ██████░░░░░░░█████░░░░░░████
Coroutine C: ██████████░░░░░████████
██ = đang chạy ░ = đang chờ I/O (yield)
Khi coroutine A đang chờ database trả kết quả (░), thread nhảy sang chạy coroutine B. Không có thời gian nào bị lãng phí.
| Thread | Coroutine | |
|---|---|---|
| Tạo | Nặng (~1MB stack mặc định) | Nhẹ (~8KB) |
| Switch | OS kernel context switch | User-space, cực nhanh |
| Đồng bộ | Race condition, mutex phức tạp | Cooperative, ít race condition hơn |
| Số lượng | Vài trăm | Hàng triệu |
Coroutine trong Swoole
Swoole tích hợp coroutine vào PHP runtime. Khi bạn gọi các hàm I/O như query database, gọi API, đọc file trong môi trường coroutine, Swoole tự động tạm dừng coroutine đó và chạy coroutine khác. Bạn viết code trông như synchronous nhưng thực ra là async.
// Code trông bình thường...
$user = DB::table('users')->where('id', 1)->first(); // yield tự động
$orders = DB::table('orders')->where('user_id', 1)->get(); // yield tự động
Phía sau, Swoole đã swap coroutine hai lần để cho các requests khác chạy trong lúc chờ database.
Tạo Coroutine
Coroutine::create()
use Swoole\Coroutine;
// Tạo một coroutine mới
$cid = Coroutine::create(function () {
echo "Coroutine bắt đầu, cid = " . Coroutine::getCid() . "\n";
// Giả lập tác vụ I/O
Coroutine::sleep(1);
echo "Coroutine kết thúc\n";
});
echo "Đây là main coroutine, cid mới = $cid\n";
go() - shorthand
Coroutine::create(function () {
Coroutine::sleep(0.5);
echo "Coroutine 1 xong\n";
});
Coroutine::create(function () {
Coroutine::sleep(0.2);
echo "Coroutine 2 xong\n";
});
// Output (chạy song song):
// Coroutine 2 xong ← chỉ mất 0.2s
// Coroutine 1 xong ← chỉ mất 0.5s tổng (không phải 0.7s)
Coroutine trong request handler
Trong framework, mỗi HTTP request đã tự động chạy trong một coroutine riêng. Bạn có thể tạo thêm sub-coroutine bên trong:
class ReportController
{
#[Get('/reports/dashboard')]
public function dashboard(): array
{
// Chạy song song 3 queries thay vì tuần tự
$results = [];
$wg = new \Swoole\Coroutine\WaitGroup();
$wg->add();
Coroutine::create(function () use (&$results, $wg) {
$results['revenue'] = DB::table('orders')->sum('total');
$wg->done();
});
$wg->add();
Coroutine::create(function () use (&$results, $wg) {
$results['users'] = DB::table('users')->count();
$wg->done();
});
$wg->add();
Coroutine::create(function () use (&$results, $wg) {
$results['products'] = DB::table('products')->where('active', 1)->count();
$wg->done();
});
$wg->wait(); // chờ cả 3 coroutine xong
return $results;
// Thời gian = max(t_revenue, t_users, t_products)
// thay vì t_revenue + t_users + t_products
}
}
Coroutine ID
Mỗi coroutine có một ID duy nhất:
Coroutine::getCid(); // ID của coroutine hiện tại (-1 nếu không trong coroutine)
Coroutine::getPcid(); // ID của coroutine cha
Coroutine::list(); // Danh sách tất cả coroutine đang chạy
Coroutine::stats(); // Thống kê: coroutine_num, coroutine_peak_num, ...
Yield và Resume
Coroutine có thể tự tạm dừng và tiếp tục:
$cid = Coroutine::create(function () {
echo "Bắt đầu\n";
Coroutine::yield(); // tạm dừng, trả quyền cho scheduler
echo "Được resume rồi!\n";
});
// Từ coroutine khác
Coroutine::resume($cid); // đánh thức coroutine $cid
Trong hầu hết trường hợp, bạn không cần gọi yield/resume thủ công. Các hàm I/O (database, Redis, HTTP client) đã tự động yield khi cần.
Coroutine::defer()
defer đăng ký một callback sẽ chạy khi coroutine hiện tại kết thúc, dù kết thúc bình thường hay có exception. Tương tự finally nhưng cho coroutine:
Coroutine::create(function () {
$conn = DB::getConnection();
Coroutine::defer(function () use ($conn) {
$conn->release(); // luôn được gọi, kể cả khi có exception
});
// Nếu có exception ở đây, defer vẫn chạy
$result = $conn->query('SELECT * FROM orders');
return $result;
});
Framework dùng defer để cleanup sau mỗi request:
// Trong request handler
Coroutine::defer([$this, 'terminateRequest']); // cleanup scoped instances, context...
Context Isolation
Mỗi coroutine có context riêng - dữ liệu trong một coroutine không ảnh hưởng coroutine khác. Framework dùng Swoole\Coroutine\Context để lưu dữ liệu theo từng coroutine:
// Coroutine A đang xử lý request của user 1
Context::set('user_id', 1);
// Coroutine B đang xử lý request của user 2 (cùng lúc)
Context::set('user_id', 2);
// Mỗi coroutine đọc được đúng user_id của mình
echo Context::get('user_id'); // A đọc: 1, B đọc: 2
Biến static và global được chia sẻ giữa các coroutine (và cả các request). Đây là nguồn gốc của nhiều bug khó tìm trong môi trường Swoole.
// ❌ Bug: user_id bị ghi đè bởi request khác
class Auth {
private static int $userId = 0;
public static function setUser(int $id): void { self::$userId = $id; }
public static function getUser(): int { return self::$userId; }
}
// ✅ Đúng: dùng Context để isolate theo coroutine
class Auth {
public static function setUser(int $id): void { Context::set('user_id', $id); }
public static function getUser(): int { return Context::get('user_id'); }
}
WaitGroup - Chờ nhiều coroutine
WaitGroup cho phép chờ một nhóm coroutine hoàn thành:
use Swoole\Coroutine\WaitGroup;
$wg = new WaitGroup();
$results = [];
foreach ($userIds as $userId) {
$wg->add();
Coroutine::create(function () use ($userId, &$results, $wg) {
$results[$userId] = fetchUserData($userId); // query song song
$wg->done();
});
}
$wg->wait(10.0); // chờ tối đa 10 giây
Tóm tắt
Swoole Worker Process
├── Coroutine 1 (Request A) ─── đang query DB ───> yield
│ ↓
├── Coroutine 2 (Request B) ←── scheduler chọn ───── chạy
│ ├── sub-coroutine 2a ─── HTTP call ──────> yield
│ └── sub-coroutine 2b ←── chạy song song
│ ↓
└── Coroutine 1 (Request A) ←── DB trả kết quả ─── resume
Coroutine giúp:
- Một worker process xử lý hàng nghìn requests đồng thời
- Tận dụng thời gian chờ I/O để làm việc khác
- Code vẫn đọc như synchronous, không callback hell