Site logo

Generators Và Lazy Evaluation Trong PHP: Xử Lý Dataset Lớn Tiết Kiệm RAM

5:00 read

Ở bài trước về cấu hình PHP-FPM và PHP CLI, chúng ta đã biết memory_limit giới hạn RAM tối đa mà một request/script được phép cấp phát, và việc vượt ngưỡng này sẽ khiến PHP ném lỗi Fatal error: Allowed memory size exhausted. Cách xử lý thông thường của nhiều người khi gặp lỗi này là... tăng memory_limit lên. Nhưng đó chỉ là chữa triệu chứng, không chữa nguyên nhân.

Nguyên nhân thật sự thường nằm ở cách code load dữ liệu: đọc một file CSV 2GB bằng file(), load toàn bộ 500,000 dòng từ database vào một mảng bằng fetchAll(), hay build một mảng kết quả khổng lồ trước khi trả về — tất cả đều buộc PHP phải giữ toàn bộ dữ liệu trong RAM cùng một lúc, bất kể bạn có thực sự cần dùng tất cả cùng lúc hay không.

Generatorlazy evaluation giải quyết đúng vấn đề này: thay vì tính toán/tải hết dữ liệu trước rồi mới xử lý (eager evaluation), generator chỉ tạo ra từng phần tử một, đúng lúc cần dùng, giữ mức sử dụng RAM gần như không đổi bất kể dataset lớn cỡ nào.

Nội dung bài viết:

  • Vấn đề của eager evaluation với array truyền thống
  • yield hoạt động như thế nào bên dưới
  • So sánh RAM giữa array và Generator qua ví dụ đo thực tế
  • yield key => value, yield from, send(), return()
  • Ứng dụng thực tế: đọc file lớn, stream kết quả database, pipeline xử lý dữ liệu
  • Giới hạn của Generator và khi nào không nên dùng
  • Kết hợp Generator với memory_limit để xử lý dataset lớn an toàn

Vấn Đề Của Eager Evaluation

Xét ví dụ đọc một file log 3GB để đếm số dòng chứa từ khóa "ERROR":

<?php
// Cách làm SAI — load toàn bộ file vào RAM trước khi xử lý
function countErrors(string $path): int
{
    $lines = file($path); // toàn bộ 3GB file nằm trong RAM cùng lúc
    $count = 0;
    foreach ($lines as $line) {
        if (str_contains($line, 'ERROR')) {
            $count++;
        }
    }
    return $count;
}

Hàm file() đọc toàn bộ nội dung file thành một mảng các dòng, nghĩa là để xử lý file 3GB, PHP cần tối thiểu 3GB+ RAM chỉ để giữ dữ liệu thô, chưa kể overhead của cấu trúc mảng PHP (mỗi phần tử mảng tốn nhiều hơn kích thước chuỗi gốc do cấu trúc zval bên trong). Với memory_limit = 256M như đã cấu hình ở bài trước, script này chết ngay lập tức.

Vấn đề cốt lõi: chúng ta chỉ cần đếm dòng, không cần giữ tất cả dòng trong bộ nhớ cùng lúc. Đây chính là lúc Generator phát huy tác dụng.

Generator Và yield Hoạt Động Như Thế Nào

<?php
function readLines(string $path): Generator
{
    $handle = fopen($path, 'r');
    while (($line = fgets($handle)) !== false) {
        yield $line;
    }
    fclose($handle);
}

function countErrors(string $path): int
{
    $count = 0;
    foreach (readLines($path) as $line) {
        if (str_contains($line, 'ERROR')) {
            $count++;
        }
    }
    return $count;
}

Giải thích cơ chế:

  • Khi hàm chứa từ khóa yield, PHP không thực thi hàm ngay khi gọi như hàm bình thường. Thay vào đó, PHP trả về ngay một đối tượng Generator (implement interface Iterator), và code bên trong hàm chỉ chạy khi được yêu cầu (khi vòng foreach gọi next()).
  • Mỗi lần yield được thực thi, hàm tạm dừng hoàn toàn tại đúng vị trí đó, trả giá trị ra ngoài cho vòng lặp xử lý, và giữ nguyên toàn bộ state (biến cục bộ, vị trí con trỏ file $handle...) cho đến khi được gọi tiếp (next()), lúc đó nó chạy tiếp từ ngay sau điểm yield vừa rồi — chứ không chạy lại từ đầu hàm.
  • Kết quả: tại bất kỳ thời điểm nào, RAM chỉ giữ một dòng hiện tại đang xử lý, không phải toàn bộ file. Dù file 3GB hay 300GB, mức RAM sử dụng gần như không đổi.

Đây chính là ý nghĩa của lazy evaluation: giá trị chỉ được tính toán/tải đúng lúc cần dùng (on demand), thay vì tính hết trước (eager).

Yêu cầu đăng nhập

Vui lòng đăng nhập để truy cập nội dung này

Additional Resources

Course Guide

Comprehensive PDF guide with examples

GitHub Repository

Example code for all lessons

Discussion

Have a question about this lesson? Post it here and get answers from instructors and peers.