Tiến trình và luồng: vì sao mở 100 tab máy vẫn chạy
Ảnh: Medium

Tiến trình và luồng: vì sao mở 100 tab máy vẫn chạy

Máy có 8 nhân mà chạy hàng trăm tiến trình — không phép màu, chỉ là nhân cắt thời gian thành lát mỏng và luân phiên. Tiến trình khác luồng ở đâu, và vì sao 100 tab vẫn mượt.

Máy bạn có 8 nhân xử lý nhưng ngay lúc này đang chạy hàng trăm tiến trình cùng lúc. Không có phép màu nào ở đây, và cũng không có chuyện "chạy song song thật" hàng trăm thứ trên 8 nhân. Điều thực sự xảy ra đơn giản hơn và khéo hơn: nhân hệ điều hành cắt thời gian của mỗi CPU thành những lát rất mỏng rồi luân phiên trao cho từng luồng, nhanh đến mức bạn có ảo giác mọi thứ chạy đồng thời.

Tiến trình khác luồng ở đâu

Đây là phân biệt nền tảng, và hiểu sai nó dẫn tới cả một loạt quyết định thiết kế tồi. Khác biệt cốt lõi nằm ở một chuyện duy nhất: bộ nhớ.

Tiến trình có không gian địa chỉ riêng và cách ly nhau; các luồng trong cùng tiến trình dùng chung bộ nhớ.
Tiến trình có không gian địa chỉ riêng và cách ly nhau; các luồng trong cùng tiến trình dùng chung bộ nhớ.

Tiến trình có không gian địa chỉ riêng. Hai tiến trình không đọc được bộ nhớ của nhau — bảng ánh xạ của cái này đơn giản không có lối tới vùng nhớ của cái kia. Hệ quả tốt: một tiến trình sập thì cái còn lại vẫn sống nguyên, vì chúng không chia sẻ gì để kéo nhau xuống. Hệ quả phải trả: muốn hai tiến trình trao đổi dữ liệu thì phải đi qua ống dẫn, socket, hoặc một vùng bộ nhớ chia sẻ được thiết lập tường minh — đều tốn kém hơn hẳn so với đọc thẳng một biến.

Luồng nằm trong cùng một tiến trình và dùng chung toàn bộ bộ nhớ của tiến trình đó. Trao đổi dữ liệu giữa hai luồng chỉ là đọc ghi cùng một vùng nhớ — nhanh gần như tức thì. Nhưng chính sự dùng chung đó sinh ra cái giá lớn nhất của lập trình đa luồng: khi hai luồng cùng sửa một dữ liệu vào cùng một lúc, kết quả có thể sai theo những cách rất khó tái hiện. Toàn bộ sự phức tạp của khoá, của đồng bộ hoá, đều mọc ra từ đây.

Một cách nhớ gọn: chọn tiến trình khi bạn cần cách ly và an toàn; chọn luồng khi bạn cần tốc độ chia sẻ và chấp nhận tự lo chuyện tranh chấp.

Lát thời gian và chuyển ngữ cảnh

Mỗi luồng sẵn sàng chạy được nhân cấp cho một lát thời gian, thường vài mili giây. Hết lát, nhân phải chuyển CPU sang luồng khác, và đây là lúc một chi phí ẩn xuất hiện.

Nhân lưu trạng thái luồng cũ rồi nạp trạng thái luồng mới ở mỗi lần chuyển — thao tác này không miễn phí.
Nhân lưu trạng thái luồng cũ rồi nạp trạng thái luồng mới ở mỗi lần chuyển — thao tác này không miễn phí.

Để chuyển, nhân phải lưu lại toàn bộ thanh ghi của luồng đang chạy, rồi nạp thanh ghi của luồng kế tiếp — thao tác gọi là chuyển ngữ cảnh. Bản thân việc lưu và nạp đó tốn cỡ vài microgiây, nghe nhỏ nhưng chưa phải toàn bộ chi phí. Cái đắt hơn thường là thiệt hại kèm theo: luồng mới có dữ liệu và mã nằm ở những vùng nhớ khác, nên bộ đệm CPU vốn đang chứa đầy thứ hữu ích cho luồng cũ giờ trở nên vô dụng — nó bị "nguội", và những lần truy cập đầu tiên của luồng mới phải chờ nạp lại từ RAM.

Từ đây suy ra một nghịch lý mà người mới hay vấp: tạo quá nhiều luồng lại làm chậm. Nếu bạn có nhiều luồng bận rộn hơn số nhân CPU, phần lớn thời gian không được dùng để tính toán thật mà để chuyển qua chuyển lại giữa chúng. Có một điểm ngọt, thường không xa số nhân là bao, mà vượt qua thì thêm luồng chỉ thêm chi phí chứ không thêm việc làm được.

Vậy vì sao mở 100 tab mà máy vẫn mượt

Câu trả lời làm sáng tỏ cả bức tranh: phần lớn trong số hàng trăm luồng đó đang chờ chứ không hề tính toán. Một tab đang chờ dữ liệu từ mạng về, một tab chờ bạn cuộn chuột, một tab chờ đồng hồ hẹn giờ điểm. Một luồng đang chờ thì không tiêu CPU — nhân bỏ nó ra khỏi hàng đợi thực thi và chỉ đánh thức nó dậy khi sự kiện nó chờ thật sự đến. Nói cách khác, 100 tab nhưng có thể chỉ một hai luồng thực sự cần CPU tại bất kỳ khoảnh khắc nào.

Đây cũng là lý do một chẩn đoán phổ biến thường sai. Khi máy chậm, người ta hay đổ cho "thiếu CPU". Nhưng máy chậm hiếm khi vì thiếu sức tính — nó thường vì thiếu RAM nên hệ điều hành phải tráo đổi bộ nhớ ra ổ đĩa (chuyện của bài bộ nhớ ảo), hoặc vì một tiến trình nào đó đang chiếm dụng ổ đĩa và mọi thứ khác phải xếp hàng chờ. CPU thường vẫn còn rảnh; thứ đang tắc nghẽn nằm ở chỗ khác.

Xem thử ngay trên máy mình

ps -eLf | wc -l          # tổng số luồng đang tồn tại trên máy
vmstat 1 5               # cột cs = số lần chuyển ngữ cảnh mỗi giây
pidstat -w 1 3           # tiến trình nào chuyển ngữ cảnh nhiều nhất

Con số đáng chú ý nhất là cột cs trong vmstat. Trên một máy nhàn rỗi, nó thường ở mức vài nghìn mỗi giây — bình thường. Nếu nó vọt lên hàng trăm nghìn mỗi giây mà máy không làm việc gì nặng nhìn thấy được, gần như chắc chắn có một phần mềm đang bận rộn một cách vô ích: hai luồng tranh nhau một khoá, một vòng lặp thăm dò liên tục thay vì chờ sự kiện, hay một cấu hình sai khiến thứ gì đó quay cuồng không cần thiết. Cột cs cao bất thường là một trong những dấu hiệu rẻ nhất để phát hiện phần mềm đang lãng phí CPU của bạn.

Chia sẻ

Thảo luận