Tìm hiểu sự khác biệt giữa từ khóa global và mảng $GLOBALS trong PHP

Bối cảnh vấn đề

Khi bảo trì một mã nguồn di sản cũ, đội ngũ kỹ thuật từng gặp phải tình huống xung đột khi chia sẻ kết nối cơ sở dữ liệu. Một hàm được viết trước đó nhằm tái sử dụng kết nối thông qua biến toàn cục:

function connectToDB() {
    global $dbConnection;
    if ($dbConnection) {
        unset($dbConnection);
    }
    $dbConnection = mysqli_connect('host', 'user', 'pass');
    return $dbConnection;
}

Vấn đề phát sinh khi một lập trình viên mới tích hợp thêm một phương thức truy vấn khác cũng dựa vào cơ chế global $dbConnection. Về lý thuyết, biến toàn cục này phải duy nhất ở mọi nơi, nhưng thực tế lại gây lỗi nghiêm trọng. Điều này dẫn ra câu hỏi về sự khác biệt thực sự giữa từ khóa global$GLOBALS.

Hiện tượng minh họa

Xét đoạn mã kiểm chứng hành vi của biến địa phương so với biến toàn cục:

$resource = 100;

function processScope() {
    global $variable;
    $variable = 50;
    unset($variable);
    unset($GLOBALS['resource']);
}

processScope();

// Kết quả hiển thị
echo $variable . "\n";           // In ra giá trị gì?
print_r($GLOBALS['variable']);   // Kiểm tra mảng toàn cục
var_dump($resource);             // Biến resource còn tồn tại không?

Dựa trên hành vi tương tự như ví dụ gốc:

  • echo $variable sẽ in ra 50.
  • $GLOBALS['variable'] cũng trả về giá trị 50.
  • var_dump($resource) sẽ cho thấy giá trị là null.

Lý giải cơ chế hoạt động

Khi khai báo global $variable bên trong hàm, PHP tạo ra một bản sao tham chiếu (alias) trong bảng ký hiệu cục bộ trỏ tới biến toàn cục tương ứng. Nếu biến toàn cục chưa tồn tại, nó sẽ được khởi tạo trước.

Hành động unset($variable) tại đây chỉ xóa bỏ bản ghi trong bảng ký hiệu cục bộ của hàm đó, không tác động đến biến thật nằm trong phạm vi toàn cục. Do đó, sau khi thoát khỏi hàm, biến $variable vẫn giữ nguyên giá trị ở tầng global.

Ngược lại, $GLOBALS là một siêu biến mang tên mảng chứa tất cả các biến toàn cục. Khi sử dụng unset($GLOBALS['resource']), bạn đang trực tiếp thao tác lên phần tử trong bảng ký hiệu chính của chương trình, khiến biến $resource bị xóa hoàn toàn khỏi vùng nhớ toàn cục.

Mặc dù global $a$a = &$GLOBALS['a'] cho kết quả giống nhau về mặt ngữ nghĩa, cách triển khai nội bộ khác nhau. Từ khóa global thường tối ưu hơn do ít thao tác tìm kiếm mảng hơn.

Cấu trúc nội bộ: Zval

Để hiểu sâu hơn, cần nắm vững cấu trúc zval (Zend Variable Container). Đây là khối xây dựng cốt lõi lưu trữ mọi giá trị biến trong PHP. Do PHP là ngôn ngữ kiểu động, zval đóng vai trò định danh loại dữ liệu và giá trị.

Cấu trúc zval bao gồm:

  • value: Chứa dữ liệu thực tế (số nguyên, chuỗi, đối tượng...).
  • type_info: Xác định kiểu dữ liệu hiện hành.
  • refcount: Đếm số lượng biến cùng trỏ đến zval này để quản lý bộ nhớ.
  • is_ref: cờ xác định xem biến có phải là tham chiếu hay không.

Mỗi biến trong PHP cuối cùng đều dẫn hướng đến một đối tượng zval. Cơ chế Copy-On-Write (COW) giúp giảm tải bộ nhớ khi biến được gán lại mà chưa sửa đổi giá trị.

Bảng ký hiệu và HashTable

PHP quản lý biến thông qua Symbol Table (Bảng ký hiệu), thực chất là một Hash Table (Zend Array). Mỗi phạm vi (scope) có bảng riêng:

  • Toàn cục (Global): Được khởi tạo khi script bắt đầu. Chính là $GLOBALS.
  • Cục bộ (Local): Sinh ra khi vào hàm và hủy khi kết thúc hàm.

Cơ chế ánh xạ: Tên biến (Key) $\rightarrow$ Con trỏ tới Zval (Value).

Cấu trúc HashTable bên dưới dùng để tổ chức dữ liệu dạng mảng hoặc bảng ký hiệu:

struct _zend_array {
    zend_refcounted_h gc;
    union { /* Flags... */ } u;
    uint32_t nTableMask;
    union {
        Bucket *arData; /* Dữ liệu bucket */
        zval *arPacked; /* Mảng packed */
    };
    uint32_t nNumOfElements;
    // ... Các trường điều khiển khác
};

Vấn đề đa luồng và Swoole

Trong môi trường chạy nền cao hiệu năng như Swoole, các coroutine chia sẻ cùng một tiến trình (thread). Biến trạng thái thực thi của Zend Engine (EG) lưu trữ bảng ký hiệu chính. Vì cấu trúc này thường chia sẻ trong cùng một thread, nếu nhiều coroutine cùng can thiệp vào $GLOBALS, $_SESSION, v.v. mà không có cơ chế cách ly, sẽ xảy ra nhiễu dữ liệu (race condition).

Đó là lý do Swoole cung cấp các tiện ích riêng để tách biệt trạng thái toàn cục cho từng coroutine thay vì dùng biến toàn cục PHP truyền thống.

So sánh: global $a vs $a = &$GLOBALS['a']

Dưới đây là sự phân tích chi tiết giữa hai cách tiếp cận:

  1. Cơ chế ràng buộc: global $a cập nhật con trỏ trong bảng ký hiệu cục bộ để trỏ thẳng tới biến toàn cục. Trong khi đó, &$GLOBALS['a'] thực hiện phép gán tham chiếu thủ công thông qua việc truy xuất mảng.
  2. Thao tác bộ nhớ: Cách dùng global tối giản hơn, chỉ cần thiết lập lại bảng ký hiệu. Việc dùng $GLOBALS đòi hỏi bước tìm kiếm khóa trong mảng trước khi gán tham chiếu.
  3. Hiệu suất: global thường nhanh hơn một chút do giảm bớt overhead của việc truy cập mảng siêu biến $GLOBALS.

Thẻ: php internal-implementation zval symbol-table global-variable

Đăng vào ngày 22 tháng 5 lúc 12:57