Skip to main content

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í.

So sánh với Thread
ThreadCoroutine
TạoNặng (~1MB stack mặc định)Nhẹ (~8KB)
SwitchOS kernel context switchUser-space, cực nhanh
Đồng bộRace condition, mutex phức tạpCooperative, ít race condition hơn
Số lượngVài trămHà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
Thực tế trong framework

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

Biến staticglobal đượ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