PHP Fibers & Lập Trình Async: Vượt Ra Khỏi Request-Response

· 11 min read

PHP đã đồng bộ từ khi sinh ra. Một request, một thread, chạy từ trên xuống dưới. PHP 8.1 giới thiệu Fibers — primitives cho cooperative multitasking, cho phép tạm dừng và resume thực thi. Đây không phải threads. Không phải parallelism. Đây là structured concurrency trong một thread.

Bài viết này giải thích Fibers hoạt động thế nào, tại sao chúng quan trọng, và cách các framework (Laravel Octane, AMPHP, ReactPHP) tận dụng chúng để tăng hiệu suất I/O.

Fiber Là Gì?

Fiber là block code có thể tự tạm dừng (suspend) và được resume sau bởi caller. Khác với threads, fibers không chạy song song — chúng chia sẻ cùng một thread nhưng nhường quyền thực thi theo cách cooperative.

$fiber = new Fiber(function (): void {
    $value = Fiber::suspend('Hello');
    echo "Fiber nhận: $value\n";
});

$result = $fiber->start();      // Bắt đầu fiber, chạy đến suspend
echo "Main nhận: $result\n";    // "Main nhận: Hello"
$fiber->resume('World');         // Resume fiber với giá trị
                                 // In: "Fiber nhận: World"

Giải thích flow:

  1. $fiber->start() — bắt đầu thực thi function bên trong Fiber
  2. Fiber chạy đến Fiber::suspend('Hello') — tạm dừng, trả 'Hello' cho caller
  3. Caller nhận 'Hello', in ra, rồi gọi $fiber->resume('World')
  4. Fiber tỉnh dậy, $value bây giờ là 'World', in ra và kết thúc

Analogy: Hãy tưởng tượng bạn đang đọc sách (main code). Bạn đặt bookmark (suspend), đi pha cà phê (do something else), rồi quay lại đọc tiếp từ bookmark (resume). Sách không tự đọc khi bạn đi pha cà phê — đó là cooperative, không phải parallel.

Vòng Đời Fiber

$fiber = new Fiber(fn () => 'result');

$fiber->isStarted();    // false
$fiber->isRunning();    // false
$fiber->isSuspended();  // false
$fiber->isTerminated(); // false

$fiber->start();        // Chạy fiber đến khi suspend hoặc kết thúc

$fiber->isTerminated(); // true (fiber kết thúc)
$fiber->getReturn();    // 'result'

Các trạng thái:

Created → Started → Running ↔ Suspended → Terminated
                      ↑                       ↓
                      └── resume() ←──────────┘ (nếu chưa terminated)

Tại Sao Fibers Quan Trọng

Vấn Đề: Blocking I/O

Phần lớn thời gian trong web request không phải CPU computation — mà là chờ đợi: chờ database response, chờ API bên ngoài, chờ file I/O.

// PHP truyền thống — tuần tự, blocking
$user = Http::get('https://api.example.com/users/1');       // Chờ 200ms
$orders = Http::get('https://api.example.com/orders/1');    // Chờ 300ms
$recommendations = Http::get('https://api.example.com/rec/1'); // Chờ 150ms
// Tổng: ~650ms (tuần tự, phần lớn thời gian là chờ network)

Thread đang idle trong 650ms — chỉ chờ, không làm gì. Nếu ba requests này không phụ thuộc nhau, tại sao không chạy đồng thời?

Giải Pháp: Concurrent I/O

Với Laravel HTTP Client Pool (nội bộ dùng Guzzle promises, không phải Fibers trực tiếp):

use Illuminate\Http\Client\Pool;

$responses = Http::pool(fn (Pool $pool) => [
    $pool->as('user')->get('https://api.example.com/users/1'),
    $pool->as('orders')->get('https://api.example.com/orders/1'),
    $pool->as('recommendations')->get('https://api.example.com/rec/1'),
]);

$user = $responses['user']->json();
$orders = $responses['orders']->json();
$recommendations = $responses['recommendations']->json();
// Tổng: ~300ms (chỉ bằng request chậm nhất)

Kết quả: Giảm từ 650ms → 300ms. Giảm 54% latency mà không thay đổi logic. Trong production với nhiều API calls, cải thiện này rất đáng kể.

AMPHP — Framework Fiber-Based

AMPHP v3 là framework async PHP xây dựng hoàn toàn trên Fibers. Nó cung cấp event loop drive Fiber scheduling.

composer require amphp/http-client amphp/amp

Concurrent HTTP Requests

use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;
use function Amp\async;
use function Amp\Future\await;

$client = HttpClientBuilder::buildDefault();

// Mỗi async() wrap function trong một Fiber
$futures = [
    'user' => async(fn () => $client->request(new Request('https://api.example.com/users/1'))),
    'orders' => async(fn () => $client->request(new Request('https://api.example.com/orders/1'))),
    'posts' => async(fn () => $client->request(new Request('https://api.example.com/posts'))),
];

// await() chờ tất cả futures hoàn thành
$responses = await($futures);

$userData = $responses['user']->getBody()->buffer();
$ordersData = $responses['orders']->getBody()->buffer();

Giải thích cơ chế bên trong:

  1. async() tạo một Fiber cho mỗi HTTP request
  2. Khi Fiber gọi network I/O, nó tự suspend() — nhường quyền thực thi
  3. Event loop detect I/O ready → resume() Fiber tương ứng
  4. Code trong Fiber trông synchronous nhưng thực thi concurrent

Concurrent Database Queries

use Amp\Mysql\MysqlConfig;
use Amp\Mysql\MysqlConnectionPool;
use function Amp\async;
use function Amp\Future\await;

$pool = new MysqlConnectionPool(MysqlConfig::fromAuthority('localhost', 'user', 'pass', 'mydb'));

$futures = [
    'users' => async(fn () => $pool->query("SELECT * FROM users WHERE active = 1")),
    'stats' => async(fn () => $pool->query("SELECT COUNT(*) as total FROM orders")),
    'recent' => async(fn () => $pool->query("SELECT * FROM logs ORDER BY id DESC LIMIT 100")),
];

$results = await($futures);
// 3 queries chạy đồng thời trên 3 connections từ pool

ReactPHP — Event-Driven Alternative

ReactPHP ra đời trước Fibers, dùng event loop + promises (callback-based). Từ v1.x có adapter cho Fibers.

composer require react/http react/event-loop
use React\Http\Browser;
use function React\Async\await;
use function React\Async\async;

$browser = new Browser();

// Style mới: dùng Fibers cho code synchronous-looking
$response = await($browser->get('https://api.example.com/users/1'));
echo $response->getBody();

// Concurrent
$promises = [
    $browser->get('https://api.example.com/users/1'),
    $browser->get('https://api.example.com/orders/1'),
];

$responses = await(React\Promise\all($promises));

AMPHP vs ReactPHP:

Đặc điểm AMPHP v3 ReactPHP
Concurrency model Fibers-native Promise-based (Fiber adapter)
API style Sync-looking Promise chains hoặc Fiber adapter
Maturity Mới hơn Lâu đời hơn, ecosystem rộng hơn
Learning curve Dễ hơn (code trông sync) Promise chains khó debug hơn

Laravel Octane: Fibers Trong Production

Laravel Octane (với Swoole hoặc FrankenPHP) giữ application trong memory, xử lý requests bằng coroutines/fibers — không boot lại mỗi request.

composer require laravel/octane
php artisan octane:install --server=frankenphp

Concurrent Tasks Trong Octane

use Laravel\Octane\Facades\Octane;

// Chạy 3 tasks đồng thời trong cùng request
[$users, $orders, $stats] = Octane::concurrently([
    fn () => User::active()->get(),
    fn () => Order::today()->sum('total'),
    fn () => Cache::get('dashboard_stats'),
]);

Giải thích: Octane dùng Swoole coroutines (tương tự Fibers) để chạy 3 closures đồng thời. Khi một closure chờ database, coroutine khác chạy. Kết quả trả về khi tất cả hoàn thành.

Lưu Ý Quan Trọng Với Octane

// ❌ NGUY HIỂM: Static/global state persist giữa requests
class BadService
{
    private static array $cache = []; // Giữ giá trị giữa requests!
}

// ✅ AN TOÀN: Dùng request-scoped state
class GoodService
{
    public function __construct(
        private array $cache = [],
    ) {}
}

Octane không boot lại app mỗi request → static properties, singletons, global state persist giữa requests. Đây là nguồn bugs phổ biến nhất khi chuyển sang Octane.

Fibers vs Threads vs Processes

Đặc điểm Fibers Threads Processes
Mô hình Cooperative Preemptive Preemptive
Memory Chia sẻ Chia sẻ (race conditions!) Riêng biệt
Overhead ~8KB mỗi fiber ~1MB mỗi thread ~10MB+ mỗi process
I/O tối ưu Tuyệt vời Tốt Tốt
CPU parallel Không
Complexity Thấp Cao (locks, deadlocks) Trung bình
PHP support 8.1+ native ext-parallel pcntl_fork, proc_open

Insight quan trọng: Fibers dành cho I/O concurrency — chờ network, disk, database. Code "chạy" concurrent nhưng thực tế chỉ nhường khi chờ I/O.

Cho CPU-heavy work (xử lý ảnh, mã hóa, machine learning), dùng processes/queues — Fibers không giúp ích vì không tạo parallelism thật.

Error Handling Trong Async Code

use function Amp\async;
use function Amp\Future\await;

$futures = [
    'users' => async(function () {
        // Nếu throw exception...
        return Http::get('https://api.example.com/users')->throw()->json();
    }),
    'orders' => async(function () {
        return Http::get('https://api.example.com/orders')->throw()->json();
    }),
];

try {
    $results = await($futures);
} catch (\Exception $e) {
    // Exception từ BẤT KỲ future nào sẽ bubble up ở đây
    Log::error('Async request failed', ['error' => $e->getMessage()]);
}

Timeout handling:

use Amp\TimeoutCancellation;

$response = $client->request(
    new Request('https://slow-api.example.com/data'),
    new TimeoutCancellation(5), // Timeout 5 giây
);

Khi Nào Dùng Fibers

Trực Tiếp: Gần Như Không Bao Giờ

Fibers là low-level primitive cho tác giả thư viện. Bạn không nên gọi new Fiber() trong application code — giống như bạn không dùng socket() trực tiếp mà dùng HTTP client.

Gián Tiếp (Qua Thư Viện):

Use Case Solution
Concurrent HTTP calls Http::pool() (Laravel) hoặc AMPHP
Concurrent DB queries Octane concurrently()
Event-driven apps ReactPHP
Real-time server Swoole/FrankenPHP + Octane
Background processing Laravel Queues (vẫn tốt nhất cho hầu hết cases)
WebSocket server Ratchet (ReactPHP) hoặc Swoole

Decision Flowchart

Bạn cần xử lý nhiều I/O operations không phụ thuộc nhau?
├── Có → Dùng Http::pool() hoặc Octane::concurrently()
│        (Không cần hiểu Fibers — thư viện handle)
└── Không →
    Bạn cần event-driven server (WebSocket, real-time)?
    ├── Có → Dùng Octane (Swoole) hoặc ReactPHP
    └── Không →
        Bạn build thư viện async?
        ├── Có → Dùng Fibers trực tiếp hoặc AMPHP primitives
        └── Không → Bạn không cần async. Dùng queues.

Benchmark: Sequential vs Concurrent

// Setup: 5 API calls, mỗi call mất ~200ms

// Sequential
$start = microtime(true);
for ($i = 0; $i < 5; $i++) {
    Http::get("https://httpbin.org/delay/0.2");
}
$sequential = microtime(true) - $start; // ~1000ms

// Concurrent
$start = microtime(true);
Http::pool(fn (Pool $pool) => array_map(
    fn ($i) => $pool->get("https://httpbin.org/delay/0.2"),
    range(1, 5),
));
$concurrent = microtime(true) - $start; // ~200ms

// Kết quả: 5x faster cho I/O-bound operations

Kết Luận

PHP Fibers là nền tảng, không phải feature dùng hàng ngày. Chúng cho phép thư viện cung cấp async APIs với code trông synchronous — không callback hell, không promise chains.

Cho hầu hết Laravel developers:

  1. Dùng Http::pool() cho concurrent API calls — đây là cách đơn giản nhất
  2. Dùng Octane concurrently() cho concurrent tasks trong cùng request
  3. Dùng Laravel Queues cho background processing — vẫn là giải pháp tốt nhất
  4. Hiểu Fibers khi debug async libraries hoặc khi đọc source code framework

Fibers không thay thế queues. Queues xử lý tasks nền, retry, scheduling. Fibers xử lý concurrent I/O trong cùng request. Hai mục đích khác nhau, bổ sung cho nhau.

Bình luận