0

Go Zero to Hero - Bài 51: Concurrency vs Parallelism - Go giải quyết bài toán đa luồng thế nào?

Golang nổi tiếng toàn cầu bởi khả năng xử lý đồng thời cực kỳ mạnh mẽ. Khác với các ngôn ngữ truyền thống, Go quản lý tài nguyên đa luồng theo một triết lý hoàn toàn khác biệt.


1. Concurrency (Đồng thời) vs Parallelism (Song song)

Rob Pike, cha đẻ của Go, từng phát biểu một câu kinh điển:

"Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once." (Concurrency là đối phó với nhiều việc cùng lúc. Parallelism là thực sự làm nhiều việc cùng lúc.)

Hãy tưởng tượng hệ thống máy chủ của bạn là một Quán Cà Phê:

  • Tuần tự (Sequential): Quán có 1 nhân viên. Khách A vào gọi món -> Nhân viên xay hạt -> Pha cà phê -> Thu tiền -> Đưa nước. Sau đó mới quay sang hỏi Khách B muốn uống gì. (Quá chậm!).
  • Đồng thời (Concurrency): Vẫn chỉ có 1 nhân viên (1 CPU Core), nhưng nhân viên này rất thông minh. Trong lúc chờ máy pha cà phê tự chảy nước (I/O chờ Database), nhân viên lập tức quay sang hỏi Khách B muốn uống gì, rồi quay lại thu tiền Khách A. Rõ ràng nhân viên chỉ làm 1 việc tại 1 thời điểm, nhưng họ chuyển đổi qua lại (Context Switching) quá nhanh, khiến khách hàng có cảm giác được phục vụ "cùng lúc".
  • Song song (Parallelism): Quán thuê 2 nhân viên (2 CPU Cores). Cả hai phục vụ 2 khách hàng hoàn toàn độc lập trong cùng một tíc tắc.

Chốt lại:

Concurrency là một cách thiết kế hệ thống (chia nhỏ công việc để không ai bị block). Còn Parallelism là cách thực thi vật lý (phụ thuộc vào số lõi CPU của phần cứng). Go là ngôn ngữ được sinh ra để viết code Concurrency cực kỳ dễ dàng, và tự động nâng cấp lên Parallelism nếu máy chủ của bạn có nhiều Core.


2. Nỗi đau của lập trình đa luồng truyền thống (OS Threads)

Trong các ngôn ngữ như Java hay C++, khi bạn muốn xử lý đa luồng, bạn sẽ tạo ra các OS Threads (Luồng của Hệ điều hành). Phương pháp này có 2 tử huyệt cực lớn đối với các hệ thống chịu tải cao:

  1. Vắt kiệt RAM: Mỗi OS Thread khi sinh ra sẽ "ăn" ngay từ 1MB đến 2MB bộ nhớ RAM làm Stack, dù nó chưa làm gì cả. Nếu hệ thống của bạn có 10,000 thiết bị kết nối đồng thời, tạo ra 10,000 OS Threads sẽ ngốn bay 10 - 20 GB RAM chỉ để khởi động!
  2. Chết đuối vì chuyển ngữ cảnh (Context Switching): Khi CPU phải chuyển đổi qua lại giữa hàng ngàn OS Threads, hệ điều hành (Linux/Windows) phải làm một đống thủ tục: Lưu trạng thái thanh ghi, quét bộ nhớ, nạp trạng thái mới... Quá trình này diễn ra chậm chạp và làm CPU hao tổn hiệu năng cực kỳ lãng phí.

3. Giải pháp của Go: "Phép thuật" Goroutines

Go từ chối sử dụng trực tiếp OS Threads cho từng tác vụ. Thay vào đó, nó đẻ ra một khái niệm gọi là Goroutines (Luồng trong Go).

Goroutine là các luồng siêu nhẹ (Lightweight Threads) do chính Go Runtime quản lý, chứ không giao cho Hệ điều hành quản lý.

  • Siêu nhẹ về RAM: Một Goroutine khi khởi tạo chỉ tốn khoảng 2 KB (nhỏ hơn 1000 lần so với OS Thread). Bạn có thể dễ dàng sinh ra hàng trăm ngàn (thậm chí hàng triệu) Goroutines trên một máy chủ thông thường mà không sợ tràn RAM.
  • Khởi động siêu tốc: Tính bằng nano-giây.
  • Linh hoạt bành trướng: Nếu 2 KB không đủ, tự Go Runtime sẽ kéo giãn vùng nhớ đó ra khi cần thiết, và thu hẹp lại khi xong việc.

4. Bí mật hậu trường: M:N Scheduler (Mô hình GMP)

Làm sao Go có thể chạy hàng trăm ngàn Goroutines nếu máy chủ của bạn chỉ có 8 lõi CPU (tương đương 8 OS Threads thực sự hoạt động cùng lúc)?

Go sử dụng một kiến trúc bộ lập lịch (Scheduler) vô cùng tinh vi gọi là M:N Scheduler. Nó map (ánh xạ) M Goroutines lên N OS Threads. Bộ lập lịch của Go hoạt động dựa trên 3 trụ cột (Mô hình GMP):

  1. G (Goroutine): Các tác vụ đa luồng mà bạn viết bằng từ khóa go.
  2. M (Machine): Các OS Threads thực sự của hệ điều hành.
  3. P (Processor): Bộ quản lý ngữ cảnh (tương ứng với số lõi CPU), mỗi P sẽ cầm một danh sách hàng đợi các Goroutine (Local Queue).

Cơ chế Work Stealing (Ăn cắp việc):

Giả sử có 2 CPU (P1 và P2). P1 đang thực thi các Goroutine của nó rất nhanh và rảnh rỗi, trong khi P2 đang bị kẹt vì hàng đợi quá dài.

Lập tức, P1 sẽ "lẻn" sang hàng đợi của P2 và ăn cắp một nửa số Goroutine mang về tự xử lý! Nhờ cơ chế này, Go đảm bảo tất cả các lõi CPU của bạn luôn hoạt động 100% công suất, không có một lõi nào rảnh rỗi trong khi lõi khác phải kêu cứu.


Tổng kết

Bạn chỉ cần nhớ 3 gạch đầu dòng về sức mạnh cốt lõi của Go:

  1. Concurrency ≠\neq Parallelism: Concurrency là nghệ thuật chia nhỏ việc để không bị khóa (Blocking). Parallelism là khả năng phần cứng chạy việc đó cùng một tíc tắc.
  2. Goroutines siêu nhẹ: 2KB khởi điểm. Cho phép xử lý hàng trăm ngàn kết nối I/O, WebSockets, hay Consumer Message Broker một cách nhẹ nhàng.
  3. M:N Scheduler thông minh: Tự động ép phần cứng (CPU) phải vắt kiệt sức lao động thông qua cơ chế Work Stealing.

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí