Rust: quyền sở hữu giải quyết bài toán gì mà GC không giải được
Ảnh: Wikimedia Commons

Rust: quyền sở hữu giải quyết bài toán gì mà GC không giải được

'Nhanh như C mà an toàn' bỏ sót phần thú vị nhất: cơ chế giúp Rust an toàn cũng chính là cơ chế chặn lỗi đồng thời — thứ không bộ gom rác nào làm được.

Rust hay được giới thiệu gọn trong một câu: "nhanh như C mà an toàn". Cách nói đó đúng, nhưng nó bỏ sót phần thú vị nhất và cũng là phần bị hiểu nhầm nhiều nhất. Cơ chế giúp Rust an toàn về bộ nhớ không phải một tính năng tách rời — nó cũng chính là cơ chế ngăn được phần lớn lỗi lập trình đồng thời, thứ mà không một bộ gom rác nào chạm tới được. Hiểu điều này là hiểu vì sao Rust đáng để trả cái giá học dốc của nó.

Ba quy tắc nền

Toàn bộ mô hình sở hữu đứng trên ba quy tắc đơn giản đến bất ngờ:

  • Mỗi giá trị có đúng một chủ sở hữu tại một thời điểm.
  • Khi chủ sở hữu ra khỏi phạm vi, giá trị được giải phóng — trình biên dịch chèn sẵn lệnh giải phóng đó vào đúng chỗ, ngay lúc dịch, không cần bộ gom rác chạy nền.
  • Tại một thời điểm, hoặc có nhiều tham chiếu chỉ đọc, hoặc có đúng một tham chiếu ghi được — không bao giờ cả hai cùng lúc.

Hai quy tắc đầu lo chuyện giải phóng bộ nhớ đúng lúc, không rò rỉ, không giải phóng hai lần. Nhưng quy tắc thứ ba mới là chỗ Rust làm được điều đặc biệt.

Quy tắc thứ ba là quy tắc quan trọng nhất

Nhiều tham chiếu đọc cùng lúc thì được; một tham chiếu ghi độc quyền thì được; nhưng vừa đọc vừa ghi cùng lúc thì bị cấm.
Nhiều tham chiếu đọc cùng lúc thì được; một tham chiếu ghi độc quyền thì được; nhưng vừa đọc vừa ghi cùng lúc thì bị cấm.

Hãy nhớ lại tranh chấp dữ liệu là gì: nó xảy ra khi hai luồng cùng chạm vào một vùng nhớ và ít nhất một bên đang ghi. Giờ đọc lại quy tắc thứ ba — "nhiều đọc hoặc một ghi, không bao giờ cả hai" — và bạn sẽ thấy điều kỳ diệu: quy tắc đó chính xác là định nghĩa phủ định của tranh chấp dữ liệu. Cấm được tình huống "vừa có kẻ đọc vừa có kẻ ghi" nghĩa là cấm được chính điều kiện sinh ra tranh chấp.

Hệ quả thực tế đáng kinh ngạc: trình biên dịch Rust từ chối biên dịch những chương trình có tranh chấp dữ liệu — ngay cả khi bạn không hề nghĩ tới chuyện đa luồng lúc viết. Cùng một bộ quy tắc bạn học để quản lý bộ nhớ đơn luồng, khi mở rộng sang đa luồng, tự động chặn luôn cả lớp lỗi đồng thời khét tiếng khó tìm. Đây chính là thứ bộ gom rác không thể làm.

Vì sao bộ gom rác không giải được bài toán này

Bộ gom rác trả lời câu 'khi nào giải phóng'; quyền sở hữu trả lời câu 'ai được chạm vào, lúc nào' — hai câu hỏi khác nhau.
Bộ gom rác trả lời câu 'khi nào giải phóng'; quyền sở hữu trả lời câu 'ai được chạm vào, lúc nào' — hai câu hỏi khác nhau.

Điểm mấu chốt là bộ gom rác và quyền sở hữu trả lời hai câu hỏi khác nhau. Bộ gom rác quản lý "khi nào giải phóng một vùng nhớ" — nó theo dõi xem còn ai trỏ tới vùng đó không, và thu hồi khi không còn. Nó làm việc này rất tốt, giải phóng lập trình viên khỏi một gánh nặng thật.

Nhưng bộ gom rác hoàn toàn không quản lý "ai được chạm vào vùng nhớ, và lúc nào". Với nó, hai luồng cùng ghi vào một đối tượng là chuyện bình thường — đối tượng vẫn còn được trỏ tới, nên vẫn sống, nên không có gì để gom. Tranh chấp dữ liệu xảy ra ngay bên dưới tầm nhìn của bộ gom rác, và nó không có công cụ nào để ngăn. Đó là lý do các ngôn ngữ có bộ gom rác vẫn cần khoá, cần kỷ luật của lập trình viên, và vẫn sinh ra những lỗi đồng thời chỉ hiện ra dưới tải cao vào lúc bất tiện nhất. Rust dời việc kiểm tra đó lên tận lúc biên dịch, nơi lỗi bị bắt trước khi chương trình kịp chạy.

Cái giá phải trả

Không có gì miễn phí, và sức mạnh của mô hình sở hữu đến kèm hai cái giá thật. Thứ nhất, những cấu trúc dữ liệu có nhiều đường trỏ chéo vào nhau — danh sách hai chiều, đồ thị, cây có con trỏ ngược lên cha — trở nên khó diễn đạt, vì chúng vi phạm ngay quy tắc "một chủ sở hữu". Để làm chúng, bạn phải chuyển sang đếm tham chiếu, dùng chỉ số thay cho con trỏ, hoặc viết một khối mã "không an toàn" được đánh dấu rõ ràng và tự chịu trách nhiệm. Đây không phải khuyết điểm ngẫu nhiên; nó là cái giá trực tiếp của sự bảo đảm mà mô hình mang lại.

Thứ hai, giai đoạn học dốc thật — nhiều người mới thấy như trình biên dịch cố tình làm khó, từ chối những đoạn mã "rõ ràng là đúng". Nhưng có một sự thật an ủi mà hầu hết lập trình viên Rust đều nhận ra sau vài tuần: phần lớn những lần "trình biên dịch làm khó" hoá ra là trình biên dịch đang chỉ cho bạn một lỗi thật — một lỗi mà ở ngôn ngữ khác bạn vẫn sẽ mắc, chỉ có điều nó sẽ nổ ra lúc chạy, trên máy người dùng, vào đúng lúc khó gỡ nhất. Cảm giác bị làm phiền lúc biên dịch, đổi lại, là những đêm không phải thức dậy vì một lỗi đồng thời không tài nào tái hiện.

Chia sẻ

Thảo luận