25/10/2026 · VietTechies · Biên tập

Một luồng thực thi (thread) trên Linux có thể lấy và nhả khóa khi không có tranh chấp mà không cần nhờ nhân hệ điều hành (kernel) thực hiện hai thao tác đó. Futex, viết tắt của fast userspace mutex, là cơ chế khóa nhanh trong không gian người dùng, cho phép phân chia công việc như vậy: chương trình tự xử lý khóa đang rảnh trong bộ nhớ của mình, còn kernel cung cấp thao tác chờ và đánh thức khi cần. Linux bắt đầu hỗ trợ futex từ phiên bản 2.5.7; cách hoạt động mà tài liệu hiện nay mô tả có từ Linux 2.5.40.
Ngay cả khi khóa đã có luồng giữ, không phải thao tác nào cũng cần đi qua kernel. Không gian người dùng (userspace) duy trì trạng thái đồng bộ, còn các thao tác futex kết nối trạng thái đó với việc các luồng ngủ và được đánh thức.
Một ô nhớ nhỏ nối chương trình với kernel
Tài liệu Linux mô tả ô nhớ futex là một số nguyên bốn byte, được căn chỉnh theo ranh giới bốn byte. Ô nhớ này vẫn có độ rộng 32 bit ngay cả trên hệ thống 64 bit. Các luồng trong cùng một tiến trình có thể dùng chung ô nhớ đó; các tiến trình riêng biệt có thể đặt ô nhớ vào vùng nhớ dùng chung.
Với một khóa đơn giản, chương trình trong userspace có thể dùng giá trị trong ô nhớ đó để biểu thị khóa có đang rảnh hay không. Một lệnh so sánh và hoán đổi nguyên tử (atomic) có thể chuyển trạng thái từ rảnh sang đã có luồng giữ, nhưng chỉ khi giá trị trước đó khớp với giá trị mà luồng dự kiến. Tính nguyên tử nghĩa là các luồng cạnh tranh không thể chen vào giữa bước đọc và bước ghi của thao tác ấy.
Nếu lấy được khóa, luồng tiếp tục chạy mà không cần thực hiện lời gọi hệ thống futex. Kernel không lưu thông tin nào về trạng thái khóa không có tranh chấp đó. Thao tác nhả khóa cũng có thể diễn ra hoàn toàn trong userspace khi giao thức đồng bộ xác định rằng không có luồng đang chờ nào cần được đánh thức.
Tài liệu futex gọi những thao tác này là các thành phần nền tảng. Các thư viện luồng ở cấp cao hơn dùng chúng để xây dựng mutex (khóa loại trừ tương hỗ), biến điều kiện, hàng rào đồng bộ và semaphore (cơ chế đồng bộ bằng bộ đếm). Tài liệu lý giải rằng các chương trình được thiết kế tốt thường cố tránh tranh chấp, nên các thao tác không có tranh chấp diễn ra trong userspace, còn kernel phân xử khi có tranh chấp. Phần lớn lập trình viên ứng dụng sử dụng các giao diện thư viện đó thay vì gọi trực tiếp futex.
Khoảng trống giữa kiểm tra và đi ngủ
Tranh chấp đặt ra một vấn đề mà riêng thao tác lấy khóa nguyên tử không thể giải quyết. Giả sử một luồng đang giữ khóa, còn luồng khác phát hiện khóa không rảnh. Luồng thứ hai quyết định đi ngủ. Trong khoảng thời gian từ lúc quyết định đến lúc thực sự ngủ, luồng giữ khóa có thể nhả khóa và gửi thông báo đánh thức.
Nếu sau đó luồng thứ hai đi ngủ mà không kiểm tra lại, luồng này có thể bỏ lỡ thông báo mình cần. Đó là vấn đề mất tín hiệu đánh thức. Khóa đã rảnh, nhưng luồng muốn lấy khóa lại đi ngủ sau khi tín hiệu đánh thức đã được gửi đi.
FUTEX_WAIT khép lại khoảng trống đó bằng cách nhận một giá trị dự kiến. Kernel đọc ô nhớ futex, so sánh với giá trị ấy rồi bắt đầu chờ; toàn bộ quá trình diễn ra nguyên tử so với các thao tác futex khác trên cùng ô nhớ. Nếu giá trị đã thay đổi, lời gọi trả về -1 ngay lập tức và đặt errno thành EAGAIN, báo rằng giá trị dự kiến không còn khớp, thay vì đưa luồng vào trạng thái ngủ.
Phép so sánh quan trọng không kém khả năng cho luồng ngủ. Chương trình trong userspace có thể đã quyết định dựa trên một lần đọc trước đó; kernel kiểm tra xem điều kiện để chờ có còn đúng tại thời điểm bắt đầu chờ hay không.
Được đánh thức là cơ hội kiểm tra lại
Khi nhả khóa, luồng giữ khóa cập nhật trạng thái trong userspace trước, rồi mới nhờ kernel đánh thức các luồng đang chờ nếu cần. FUTEX_WAKE nhận số luồng tối đa cần đánh thức. Tài liệu nêu hai lựa chọn thường gặp là đánh thức một luồng hoặc tất cả các luồng đang chờ.
Bản thân thông báo đó không xác nhận rằng luồng vừa trở về từ lời gọi đã nắm giữ khóa. Tài liệu FUTEX_WAIT yêu cầu bên gọi luôn tính đến khả năng ngay cả khi lời gọi trả về thành công, đó vẫn có thể là một lần thức dậy giả (spurious wake-up). Cơ chế đồng bộ trong userspace phải kiểm tra trạng thái và quyết định xem luồng có thể tiếp tục hay cần chờ lần nữa.
FUTEX_WAKE thông thường cũng không bảo đảm sẽ chọn luồng đang chờ nào. Một luồng có mức ưu tiên lập lịch cao hơn không được bảo đảm sẽ thức dậy trước luồng có mức ưu tiên thấp hơn. Ứng dụng không thể suy ra chính sách công bằng chỉ từ việc có sử dụng futex.
Kế thừa ưu tiên cần một quy ước khác
Linux cung cấp futex hỗ trợ kế thừa ưu tiên để xử lý một khó khăn khác: luồng có mức ưu tiên cao có thể phải chờ khóa do luồng có mức ưu tiên thấp giữ, trong khi công việc có mức ưu tiên trung gian ngăn luồng giữ khóa chạy. Luồng giữ khóa không thể nhả khóa cho đến khi được cấp thời gian thực thi.
Cơ chế kế thừa ưu tiên tạm thời nâng mức ưu tiên của luồng giữ khóa, để luồng đó có thể tiếp tục thực thi và tiến tới nhả khóa. Các thao tác futex này dùng một cách mã hóa được quy định cụ thể: số không nghĩa là chưa khóa, còn khóa đã có luồng giữ sẽ ghi lại mã định danh của luồng đó. Một bit riêng cho biết có luồng đang chờ.
Tài liệu kernel nhấn mạnh rằng cơ chế bổ sung này vẫn giữ cách xử lý trong userspace khi không có tranh chấp. Khi cần kernel hỗ trợ lấy khóa, kernel dùng một mutex nội bộ có hỗ trợ kế thừa ưu tiên. Hành vi càng đòi hỏi khắt khe thì càng cần quy ước cụ thể hơn về ô nhớ dùng chung.
Ghi nhận khóa khi luồng giữ khóa kết thúc
Thiết kế thông thường chỉ cần kernel lưu rất ít thông tin, nhưng điều đó cũng tạo ra một khó khăn nữa. Một luồng có thể kết thúc khi đang giữ khóa không có tranh chấp, trong khi kernel không có hàng đợi chờ nào cho khóa đó. Nếu không có thông tin bổ sung, kernel không thể nhận diện khóa đang được giữ ấy để dọn dẹp.
Robust futex, loại futex hỗ trợ xử lý khi luồng giữ khóa kết thúc, giải quyết vấn đề bằng một danh sách riêng cho mỗi luồng, ghi lại các khóa robust mà luồng đang giữ. Danh sách này được duy trì trong userspace và đăng ký với kernel. Khi luồng thoát, kernel có thể kiểm tra danh sách, đánh dấu các khóa liên quan bằng cờ báo luồng giữ khóa đã kết thúc và đánh thức một luồng đang chờ. Thư viện phía userspace và ứng dụng vẫn phải thực hiện phần việc khôi phục; thông báo không sửa chữa dữ liệu được khóa bảo vệ.
Những phần mở rộng này cho thấy giới hạn của cách phân chia công việc ban đầu. Ô nhớ dùng chung phục vụ thao tác nhanh; kernel cung cấp việc chờ, đánh thức và những hỗ trợ cụ thể khi giao thức cần đến. Khi không có tranh chấp, luồng thay đổi giá trị trong ô nhớ rồi tiếp tục chạy.
Bài liên quan
Đọc bằng ngôn ngữ khác: English
Nguồn
- Tài liệu Linux: lời gọi hệ thống futex
- Tài liệu Linux: các thành phần đồng bộ nền tảng của futex
- Tài liệu Linux: FUTEX_WAIT và vấn đề mất tín hiệu đánh thức
- Tài liệu Linux: FUTEX_WAKE
- Tài liệu kernel Linux: Lightweight PI-futexes
- Tài liệu kernel Linux: robust futexes
- Ảnh đại diện: Kurt Kaiser; Wikimedia Commons; CC0 (đã chỉnh sửa từ ảnh gốc)
