Khi lập trình các ứng dụng xử lý tải cao, chúng ta thường nghe các khẩu lệnh như :’Hãy mở thêm luồng (Thread) để tận dụng tối đa CPU đa lõi’. Nhưng trong thực tế, nếu mở quá nhiều luồng, CPU sẽ rơi vào tình trạng ‘bận rộn nhưng không làm được việc gì nên hồn.
Thủ phạm đứng sau hiện tượng này chính là Context Switching (Chuyển đổi ngữ cảnh).
Bản chất của chuyển đổi ngữ cảnh là gì?
Trên máy chủ của bạn, số lượng lõi CPU (Core) luôn có hạn, vd máy chủ có 4 Cores hoặc 8 Cores, thế nhưng, bạn lại có thể chạy hàng trăm, hàng nghìn luồng Threads cùng một lúc.
Làm sao 4 lõi có thể gánh được 1k luồng? Bộ lập lịch của hệ điều hành (OS Scheduler) phải thực hiện trò chơi chia sẻ thời gian (time-sharing). Nó cho phép luồng A chạy trên lõi 1 trong vòng vài mili-giây, sau đó là luồng A ra để cho luồng B vào chạy.
Quá trình ‘đổi người’ này diễn ra như sau:
Cất đồ cũ: CPU phải dừng luồng A lại, đóng gói và lưu toàn bộ trạng thái hiện tại của luồng A - bao gồm các giá trị trong các thanh ghi CPU Registers, con trỏ lệnh Program Counter vào một vùng nhớ gọi là PCB - Process Control Block hoặc TCB - Thread Control Block. Lấy đồ mới : CPU chạy đến vùng nhớ của luồng B, lôi đống hành lý cũ của Luồng B ra, nạp lại vào các thanh ghi của CPU. Chạy tiếp: Cho phép luồng B tiếp tục chạy từ vị trí nó bị dừng lần trước. Chi phí ngầm tàn khốc
Qúa trình đóng gói và mở gói hành lý chính là Context Switching. Bản thân việc chuyển đổi này không hề xử lý một dòng code nào trên trang web của bạn, nó thuần tuý là cài chi phi quản lý (overhead) của hệ điều hành.
Nếu hệ thống có quá nhiều luồng tranh chấp, CPU sẽ dành tới 60%-70% sức mạnh chỉ để ngồi đóng gói và hoán đổi hành lý cho các luồng, khiến thời gian xử lý API thực tế bị bóp nghẹt.
Nguy hiểm hơn, khi luồng B vào thay luồng A, tonaf bộ dữ liệu L1/L2 Cache mà luồng A vừa cất công bốc từ RAM lên sẽ bị xoá sạch để nạp dữ liệu cho luồng B. Hiện tượng này gọi là Cache Pollution (ô nhiễm Cache), khiến tốc độ truy cập dữ liệu bị kéo tụt thảm hại do dính Cache Miss liên tục.
Bài tập: Khi bạn viết Backend cho Vistastory bằng Python (Fast API) kết hợp với Uvicorn/Gunicorn, bạn phát hiện ra:
Nếu bạn dùng mô hình Multi-threading, mỗi khi có 1 request nạp tiền đi vào hệ thống, hệ thống lại sinh ra một luồng Threa mới để xử lý. Khi có 10k người vào cùng lúc, hệ thống dính context switching nặng nề và sập.Nhưng khi bạn chuyển sang viết code theo phong cách Asynchronous (sử dùng async/await), hệ thống của bạn chỉ chạy trên một luồng duy nhất (single thread- event lôp) nhưng lại có thể cân mượt cả 10k request đó mà không hề bị nghẽn context switching của hệ điều hành. ⇒ Tại sao mô hình Async/Await có thể làm được điều này?
Bản chất: Event Loop vẫn chuyển đổi tác vụm nhưung ở tầng ừng dụng - User Space. Khi có 10k người cùng vào nạp tiền, mô hình Async/Await không bắt hộ phải xếp hàng chừo người này nạp tiền xong mới đến người khác (nếu thế thì web sẽ bị nghẽn mạch ngay). Thay vào đó, nó vẫn hoán đổi qua lại giữ 10k ngừoi này, nhưng theo một cách thông mình hơn:
Trong mô hình đa luồng (multi-thread): quyền quyết định thuộc về Linux Kernel. Hệ điều hành giồng như người quản lý, cứ đến giờ là gõ kẻng ép luồng A dừng lại để luồng B vào, bất chấp luồng A đang làm việc dở. Việc đóng gói hành lý (context switching) này cực kỳ tốn kém. Trong mô hình Async/Await: Quyền quyết định thuộc về Event Loop (nằm ngay trong ứng dụng FastAPI). Event Loop giống như người điều phối giao thông minh chỉ chạy trên 1 luồng duy nhất. VD:
Khi luồng duy nhất này xử lý yêu cầu nạp tiền của độc giả 1:
Gặp dòng lệnh : await ngân_hàng.trừ_tiền() - Đây là tác vụ I/O, phải đợi mạng phản hồi. Thay vì ngồi im đóng băng đợi, đoạn code async sẽ tự động giải phóng CPU và bảo với Event Loop:’Tôi đang đợi ngân hàng phản hồi, anh cứ đi phục vụ người khác đi.’Event Loop lập tức chuyển sang xử lý yêu cầu của độc giả 2 mà không cần hệ thống điều hành can thiệp để đóng goi thanh ghi hay coá bộ nhớ đẹm Cache. Vì tất cả cùng diễn ra trên cùng một luồng, hành lý của CPu vẫn nguyên vẹn. Khi ngân hành báo về độc giả 1 đã trừ tiền xong, Event Loop sẽ xếp độc giả 1 vào hàng đợi và quay lại xử lý nốt khi rảnh.