Hình dung áp lực nước trong một chung cư. Có một cái van "ưu tiên" cho căn hộ của bạn. Vào giờ cao điểm, khi cả tòa nhà cùng mở vòi, cái van đó cho bạn dòng nước mạnh hơn hàng xóm — nó chia phần nước khan hiếm. Nhưng lúc 3 giờ sáng chỉ mình bạn tắm, cái van chẳng đổi được gì: nước vốn đã chảy hết cỡ, "ưu tiên" hay không cũng thế. Và nếu van chỉ nhích lên một nấc, bạn gần như không thấy khác biệt — phải vặn mạnh hẳn mới cảm nhận được. nice của Linux hành xử đúng như cái van đó, và cả hai điều — khi nào nó có tác dụng lẫn bao nhiêu — đều khác trực giác.
Khi nhiều tiến trình cùng muốn CPU, hệ điều hành phải quyết ai chạy bao nhiêu. Công cụ người dùng chỉnh được là nice value — một số từ -20 (giành nhất) tới +19 (nhường nhất), mặc định 0. Trực giác đơn giản: đặt nice thấp thì tiến trình chạy nhanh hơn, đặt nice cao thì nó nhường. Nghe rõ ràng, nhưng cả khi nào nice có tác động lẫn bao nhiêu đều khác trực giác. Tôi đo trong container gcc:13 (10 lõi, ARM) — buộc cạnh tranh bằng cách ghim tiến trình vào một lõi — và phát hiện nice là công cụ chia phần, không phải nút tăng tốc.
nice chia CPU tranh chấp, không tăng tốc
Điểm dễ hiểu sai nhất: nice không làm một tiến trình nhanh hơn — nó chỉ quyết cách chia CPU khi có cạnh tranh. Nếu hệ thống còn lõi rảnh, mọi tiến trình sẵn sàng chạy đều được chạy full, và nice chẳng thay đổi gì — đúng cái van lúc 3 giờ sáng. nice chỉ có nghĩa khi CPU bão hòa: nhiều tiến trình muốn chạy hơn số lõi có, và bộ lập lịch phải chọn ai được nhiều thời gian hơn.
Bộ lập lịch Linux (CFS — Completely Fair Scheduler) biến nice thành một trọng số: mỗi bậc nice thay đổi trọng số khoảng 1,25 lần. Tiến trình trọng số cao hơn nhận tỉ lệ CPU cao hơn tương ứng — nhưng vì trọng số là hàm mũ của nice, tác động không tuyến tính theo con số nice.
Để đo, tôi chạy hai tiến trình cùng đua CPU, đếm số vòng lặp mỗi bên làm được trong một khoảng thời gian cố định — tỉ lệ đó là tỉ lệ CPU chúng nhận. Trước hết ghim cả hai vào một lõi (buộc bão hòa).
Đo: 50/50 khi rảnh, 96/4 khi chênh lớn
GHIM 1 LÕI (bão hòa):
nice 0 vs nice 0 : 50 / 50
nice 0 vs nice 5 : 50 / 50 (chênh nhỏ ~ không đổi)
nice 0 vs nice 10 : 75 / 25 (tỉ lệ 3,05×)
nice 0 vs nice 19 : 96 / 4 (tỉ lệ 21,75×)
KHÔNG GHIM (10 lõi rảnh):
nice 0 vs nice 19 : 50 / 50 (nice VÔ NGHĨA!)
Hai kết quả đều đáng chú ý. Thứ nhất, tác động phi tuyến và yếu ở chênh nhỏ: nice 0 so nice 5 vẫn chia 50/50 — chênh 5 bậc gần như không đổi gì. Chỉ khi chênh tới 10 bậc mới thấy lệch rõ (75/25), và chênh 19 bậc mới thành áp đảo (96/4, một bên nhận gần 22 lần bên kia). Tăng nice "một chút" thường chẳng thay đổi gì đáng kể — phải chênh lớn mới có hiệu ứng mạnh.
Thứ hai, và quan trọng hơn: bỏ ghim lõi đi thì nice hoàn toàn vô nghĩa. Hai tiến trình trên 10 lõi rảnh, dù nice chênh nhau tối đa (0 vs 19), vẫn chia 50/50 — mỗi tiến trình được một lõi riêng và chạy full, nice không đụng tới. nice chỉ ra tay khi CPU bị tranh.
(Môi trường đo: container gcc:13 trên host ARM AArch64, 10 lõi, hai tiến trình đua CPU, tỉ lệ suy từ số vòng lặp. Con số tỉ lệ đổi theo cấu hình CFS; cái bền vững là hai điều: nice vô nghĩa khi còn lõi rảnh, và tác động phi tuyến — chênh nhỏ gần như không đổi, chênh lớn mới áp đảo.)
Hai niềm tin đối nghịch, cùng sai
Tôi vào đo với hai niềm tin. Thứ nhất: "đặt nice thấp thì tiến trình chạy nhanh hơn". Thứ hai, ngầm hơn: "nice luôn có tác động — cứ chỉnh là thấy khác". Phép đo sửa cả hai. nice không làm tiến trình nhanh hơn theo nghĩa tuyệt đối — nó chỉ cho tiến trình một phần lớn hơn của cái CPU đang bị tranh. Bằng chứng: trên 10 lõi rảnh, tiến trình nice 0 và nice 19 chạy y như nhau (50/50), vì không có gì để tranh. Và ngay cả khi có cạnh tranh, chỉnh nice một chút (5 bậc) gần như không đổi kết quả; phải chênh lớn mới thấy.
Nhưng có một niềm tin ngược, dễ rơi vào sau khi thấy "nice 0 vs 19 mà vẫn 50/50", cũng sai: "nice chả tác dụng gì, đồ vô dụng, quên đi". Nhìn lại cột ghim-một-lõi: khi CPU thật sự bão hòa, nice 0 vs 19 cho 96/4 — một bên nhận gần 22 lần bên kia, cực kỳ mạnh. nice không vô dụng; nó chỉ ngủ yên khi không có tranh chấp. Sự thật nằm ở điều kiện: một "nút chỉnh" chỉ có tác dụng trong đúng điều kiện của nó (ở đây: CPU bão hòa), và có đường cong riêng (ở đây: phi tuyến, yếu ở chênh nhỏ). Kết luận "nice làm nhanh hơn" hay "nice vô dụng" đều đến từ việc đo ở một điểm rồi khái quát — chỉ đo cả hai điều kiện (rảnh/bão hòa) và cả đường cong (chênh 5/10/19) mới ra bức tranh đúng. Đây cùng tinh thần đo tác động đúng cách: một tham số chỉ có ý nghĩa trong ngữ cảnh của nó.
Về đến code hằng ngày
Điều đầu tiên: dùng nice để chia CPU khi có tranh chấp, không để tăng tốc một tiến trình đơn lẻ. Đặt một job nền nặng CPU (backup, nén, build) ở nice cao (+10 tới +19) là cách tốt để nó nhường các tác vụ tương tác khi máy bận — nhưng khi máy rảnh nó vẫn chạy full, không bị phạt. Ngược lại, đừng kỳ vọng đặt một tiến trình ở nice thấp sẽ "tăng tốc" nó nếu CPU chưa bão hòa: nó vốn đã có đủ CPU.
Điều thứ hai: muốn nhường thật rõ thì phải chênh nice lớn, và nếu cần đảm bảo cứng thì nice không đủ. Vì tác động phi tuyến, tăng một job nền từ 0 lên 5 gần như không giúp; muốn nó thật sự lùi lại khi máy bận, đặt nice cao hẳn (15-19). Và nếu cần đảm bảo cứng (job này không bao giờ được đụng CPU khi có việc quan trọng hơn, hay giới hạn nó ở đúng X% lõi), nice là chưa đủ — cần cgroup cpu.weight/cpu.max như các phần về container đã đo.
Chỗ này cái "nice thật ra yếu và có điều kiện" giải thích một hố ngã kinh điển ở tầng cao. Thread.setPriority() của Java là ví dụ nổi tiếng nhất: trên Linux nó ánh xạ (rất mờ nhạt) sang nice, nên đặt một luồng Thread.MAX_PRIORITY gần như chẳng làm gì — vô số người tưởng nó "tăng tốc luồng" rồi ngã ngửa. Go thì bỏ hẳn khái niệm ưu tiên goroutine vì lý do tương tự (khó dùng đúng, dễ hiểu sai). Ở tầng vận hành, đây là vì sao điều phối tài nguyên nghiêm túc dùng cgroup cpu.shares/cpu.weight (chia tỉ lệ, đúng tinh thần nice nhưng tường minh và cô lập được) thay vì rải nice, và có cả ionice cho I/O vì nice chỉ quản CPU. Mẫu số chung: "ưu tiên" gần như luôn là chia phần khi khan hiếm, không phải tăng tốc — và chỉ mạnh khi tài nguyên thật sự bị tranh.
Muốn tự thấy thì nếu bạn từng nice -n 10 ./job-nền hay renice một tiến trình để "cho nó nhanh/chậm hơn", hỏi: lúc đó máy có bão hòa CPU không? Nếu số lõi nhiều hơn số tiến trình đang chạy, nice của bạn có thể chẳng làm gì. Thử: chạy hai vòng lặp CPU vô hạn cùng lúc, ghim cả hai vào một lõi (taskset -c 0), rồi renice một cái lên +19 và xem top — bạn sẽ thấy một cái ~95% và cái kia ~5%. Rồi bỏ ghim (cho chúng dùng cả máy) và xem lại — hai cái lại 50/50 dù nice vẫn chênh. Vài phút đó dạy bạn điều mà "nice = ưu tiên" giấu đi: nice không phải thứ tự tuyệt đối, mà là tỉ lệ chia phần khi phải tranh — và nếu không ai phải tranh, nó im lặng.