Nhiều lập trình viên đến từ các ngôn ngữ GC cũ mang một nỗi sợ: "stop-the-world" — khoảnh khắc bộ thu gom rác đóng băng cả chương trình để dọn dẹp, có thể kéo dài hàng trăm mili-giây. GC của Go vẫn có STW, nhưng nó nhỏ tới mức đáng kinh ngạc. Bài này chỉ ra chính xác GC còn dừng chương trình ở đâu, đo thời gian dừng thật là bao nhiêu, và điều gì làm nó dài ra.
Hai điểm STW còn lại
GC hiện đại của Go chạy phần lớn công việc (đánh dấu, quét, thậm chí quét stack goroutine) đồng thời với chương trình. Nhưng vẫn còn hai khoảnh khắc mỗi chu kỳ mà nó phải dừng tất cả goroutine:
- Sweep termination (đầu chu kỳ): dừng mọi goroutine, kết thúc phần quét dở của chu kỳ trước, và bật write barrier để chuẩn bị đánh dấu.
- Mark termination (cuối chu kỳ): dừng mọi goroutine, chốt việc đánh dấu, tắt write barrier, và chuẩn bị pha quét.
Giữa hai điểm này, pha mark (đánh dấu) chạy đồng thời — chương trình vẫn chạy bình thường. Sau mark termination, pha sweep (thu hồi rác) cũng chạy đồng thời ở nền.

Hình 1: Hai điểm STW mỗi chu kỳ — sweep termination (đầu) và mark termination (cuối). Giữa chúng, mark chạy đồng thời; sau đó sweep cũng đồng thời.
Vì sao vẫn cần STW
Câu hỏi tự nhiên: nếu Go đã chạy đồng thời được gần hết, sao không bỏ luôn STW? Vì chuyển pha cần một khoảnh khắc mọi goroutine đồng ý về trạng thái GC. Bật/tắt write barrier (bài trước) không thể làm giữa lúc các goroutine đang tự do sửa con trỏ — nếu một goroutine thấy barrier bật còn goroutine khác thấy tắt, bất biến tricolor sẽ vỡ. STW tạo một điểm đồng bộ ngắn để mọi goroutine cùng chuyển trạng thái an toàn. Go tối thiểu hoá STW bằng cách đẩy gần hết việc (mark, sweep, quét stack) ra ngoài STW.
Đo thật: STW theo số goroutine
Câu hỏi thực dụng: STW có phình lên khi có nhiều goroutine không? (Historically, quét stack tất cả goroutine từng là thủ phạm làm STW dài.) Đo với số goroutine sống khác nhau, đọc runtime.MemStats.PauseNs:

Hình 2: STW trung bình tăng nhẹ từ 38,9 us (100 goroutine) lên 160,7 us (100k), nhưng luôn dưới 1 ms kể cả nửa triệu goroutine. gctrace cho thấy hai điểm STW (số đầu + số cuối phần clock).
Kết quả đo:
- 100 goroutine: STW trung bình 38,9 µs, max 112,8 µs.
- 10.000: trung bình 56,1 µs, max 226 µs.
- 100.000: trung bình 160,7 µs, max 315 µs.
- 500.000: trung bình 136 µs, max 229 µs.
STW có tăng theo số goroutine — từ ~39 µs (100 goroutine) lên ~161 µs (100k) — nhưng luôn dưới 1 mili-giây, kể cả với nửa triệu goroutine. Đây là thành tựu lớn: các GC cũ dừng hàng trăm mili-giây và tệ hơn khi heap/số thread lớn; Go giữ STW ở mức micro-giây gần như hằng số nhờ đẩy việc quét stack (phần từng làm STW dài) ra chạy đồng thời.
Nhìn gctrace với 100k goroutine: gc 1: 0.21+2.5+0.14 ms clock — số đầu (0,21 ms, sweep termination) và số cuối (0,14 ms, mark termination) là hai lần STW; số giữa (2,5 ms, mark) chạy đồng thời không dừng.
Ứng dụng thực tế
Go phù hợp cho dịch vụ độ trễ đuôi (tail latency) nghiêm ngặt. Vì STW dưới 1 ms kể cả ở quy mô lớn, một API server Go không bị "khựng" gây spike độ trễ p99/p999 như hệ thống GC stop-the-world cũ. Đây là lý do Go được chọn cho hạ tầng cần độ trễ ổn định.
Nhiều goroutine làm STW dài hơn một chút — cân nhắc ở quy mô cực lớn. Nếu bạn có hàng trăm nghìn goroutine sống và độ trễ đuôi cực kỳ quan trọng, ~160 µs STW có thể đáng chú ý. Giảm số goroutine sống đồng thời (worker pool giới hạn) giữ STW nhỏ hơn. Nhưng với đa số ứng dụng, đây không phải vấn đề.
Đo STW thật bằng PauseNs hoặc PauseTotalNs. runtime.MemStats.PauseNs là vòng đệm 256 mẫu thời gian dừng gần nhất — công cụ chính xác để giám sát STW trong production. Kết hợp với gctrace để thấy STW nằm ở pha nào. Nếu STW vượt vài ms, có gì đó bất thường.
Đánh đổi cần cân nhắc
STW nhỏ đổi bằng độ phức tạp và chi phí đồng thời. Để giữ STW dưới 1 ms, Go trả giá bằng write barrier (bài trước), CPU cho mark đồng thời, và một mô hình GC phức tạp. Đây là đánh đổi có chủ đích: độ trễ thấp quan trọng hơn thông lượng thô cho phần lớn dịch vụ Go. Nếu bạn cần thông lượng tuyệt đối (batch processing), GC đồng thời hơi phí so với STW đơn giản.
STW không phải là toàn bộ chi phí GC. ~160 µs STW nghe nhỏ, nhưng tổng chi phí GC còn gồm mark đồng thời (chia sẻ CPU) và write barrier. Đừng nhầm "STW nhỏ" với "GC rẻ" — đo tổng bằng cột %CPU trong gctrace.
Không thể loại bỏ STW hoàn toàn. Có những nghiên cứu GC không-STW, nhưng chúng phức tạp hơn nhiều và có đánh đổi riêng. Go chọn "STW cực nhỏ" thay vì "không STW" — đơn giản hơn, đủ tốt cho gần như mọi workload. Đừng kỳ vọng STW = 0.
Ba ý mang về
- GC Go dừng chương trình ở đúng hai điểm mỗi chu kỳ: sweep termination (đầu, bật write barrier) và mark termination (cuối, tắt write barrier) — cần thiết để mọi goroutine đồng bộ chuyển pha an toàn, không thể làm giữa lúc goroutine sửa con trỏ.
- STW cực nhỏ và gần như không phụ thuộc quy mô: đo thật vài chục tới vài trăm micro-giây, tăng nhẹ từ 39 us (100 goroutine) lên 161 us (100k) nhưng luôn dưới 1 ms kể cả nửa triệu goroutine — nhờ đẩy quét stack ra chạy đồng thời.
- Đây là lý do Go hợp cho dịch vụ độ trễ đuôi nghiêm ngặt: STW dưới 1 ms không gây spike p99 như GC cũ — giám sát bằng
PauseNs, và giảm số goroutine sống nếu cần STW nhỏ hơn nữa ở quy mô cực lớn.
Phần sau ta gom mọi thứ đã học về GC thành kỹ thuật thực chiến: Phần sau tổng hợp cách giảm áp lực GC trong thực tế — kết hợp giảm allocation, tái dùng, chỉnh GOGC/GOMEMLIMIT, và đo hiệu quả trên workload thật.