0

Go Zero to Hero - Bài 58: Khái niệm Race Condition và công cụ go run -race

1. Race Condition là gì? Cú lừa của CPU

Race Condition (Điều kiện tương tranh) xảy ra khi có từ 2 Goroutine trở lên cùng truy cập vào một vùng nhớ (một biến) CÙNG MỘT LÚC, và có ít nhất 1 Goroutine thực hiện thao tác GHI (Thay đổi dữ liệu).

Hãy tưởng tượng một biến đếm số lượng vé bán ra tại ga tàu: ticketCount.

package main

import (
    "fmt"
    "sync"
)

func main() {
    var ticketCount int = 0
    var wg sync.WaitGroup

    // Tung ra 1000 Goroutine, mỗi Goroutine bán 1 vé
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            
            // Tăng số vé lên 1
            ticketCount++ 
        }()
    }

    wg.Wait() // Chờ tất cả 1000 luồng chạy xong
    
    // Bạn kỳ vọng in ra bao nhiêu? 1000 đúng không?
    fmt.Println("Tổng số vé đã bán:", ticketCount)
}

Nếu bạn chạy đoạn code trên vài lần (dùng lệnh go run main.go), kết quả in ra sẽ là:

  • Lần 1: Tổng số vé đã bán: 981
  • Lần 2: Tổng số vé đã bán: 994
  • Lần 3: Tổng số vé đã bán: 978

Tại sao kết quả không bao giờ đủ 1000? Vé của tôi bốc hơi đi đâu rồi?

Bí mật nằm ở phép toán ticketCount++. Đối với mắt người, đó là 1 lệnh. Nhưng đối với CPU, nó phải làm 3 bước rời rạc (Non-atomic):

  1. READ: Đọc giá trị hiện tại của ticketCount từ RAM (Giả sử đang là 10).
  2. MODIFY: Lấy 10 + 1 = 11 ở trong não (thanh ghi CPU).
  3. WRITE: Ghi số 11 đè lại vào RAM.

Thảm họa xảy ra khi:

  • Goroutine A nhảy vào đọc: Thấy ticketCount là 10. Nó nhẩm tính 10 + 1 = 11.
  • Nhưng trước khi Goroutine A kịp ghi số 11 xuống RAM, Goroutine B nhảy vào đọc. Goroutine B VẪN THẤY số 10. Nó cũng tính 10 + 1 = 11.
  • Cả Goroutine A và Goroutine B sau đó cùng ghi số 11 đè vào RAM.

Đáng lý ra 2 vé được bán thì số đếm phải lên 12, nhưng cuối cùng nó chỉ lên 11. Chúng ta đã làm thất thoát 1 chiếc vé do GHI ĐÈ LẪN NHAU!


2. Race Detector: Kính lúp săn quỷ của Go

Bug do Race Condition là loại bug kinh tởm nhất trong thế giới phần mềm. Tại sao?

Bởi vì khi bạn code và chạy thử trên máy của bạn (ít luồng, cấu hình thấp), nó sẽ luôn chạy đúng. Nó chỉ bắt đầu sai khi bạn đẩy lên hệ thống Production chịu tải cao với hàng vạn User cùng quẹt thẻ. Lỗi xuất hiện ngẫu nhiên, lúc bị lúc không, hoàn toàn không để lại dấu vết.

Để cứu vãn sinh mạng của các kỹ sư, Go trang bị một cỗ máy dò mìn siêu hạng ngay trong trình biên dịch: Data Race Detector.

Bạn không cần viết thêm dòng code nào cả. Chỉ cần gắn thêm cờ -race vào các lệnh build, run hoặc test của Go.

Thử chạy lại đoạn code trên với lệnh:

go run -race main.go

Boom! Màn hình Terminal của bạn sẽ đỏ rực với một thông báo cảnh báo (Warning) chí mạng:

==================
WARNING: DATA RACE
Read at 0x00c0000a6010 by goroutine 8:
  main.main.func1()
      /path/to/your/main.go:19 +0x44

Previous write at 0x00c0000a6010 by goroutine 7:
  main.main.func1()
      /path/to/your/main.go:19 +0x5c

Goroutine 8 (running) created at:
  main.main()
      /path/to/your/main.go:15 +0x7a
==================

Đọc hiểu thông báo này:

  • WARNING: DATA RACE: Báo động có xung đột!
  • Read at... by goroutine 8: Goroutine số 8 đang cố gắng đọc một vùng nhớ (dòng 19: ticketCount++).
  • Previous write... by goroutine 7: Nhưng vùng nhớ đó vừa bị Goroutine số 7 ghi đè lên trước đó tíc tắc (cũng tại dòng 19).

Công cụ chỉ đích danh file nào, dòng code thứ mấy gây ra thảm họa.


3. Quy luật sinh tồn với Race Detector

  • Chi phí hiệu năng: Khi bạn dùng cờ -race, chương trình của bạn sẽ chạy chậm hơn khoảng 2-10 lần, và tốn nhiều RAM hơn. Go phải gắn các "camera giám sát" ngầm vào từng biến nhớ của bạn.
  • Môi trường sử dụng: Tuyệt đối KHÔNG BAO GIỜ build code với cờ -race để đem lên Production. Cờ này chỉ dùng khi chạy Unit Test trên máy tính cá nhân hoặc trên các luồng CI/CD (Pipeline).
  • Tôn chỉ hành động: Dù chỉ là 1 cảnh báo Data Race, bạn BẮT BUỘC phải sửa code. Trong lập trình đa luồng, một cảnh báo Data Race không phải là "lời khuyên", nó là một quả bom nổ chậm chắc chắn sẽ kéo sập hệ thống.

💡 Tổng kết

Bạn đã nắm thóp được kẻ thù giấu mặt của hệ thống Backend:

  • Bản chất: Race Condition sinh ra khi nhiều luồng cùng xúm vào thao tác với một biến dữ liệu, và thao tác đó không phải là nguyên tử (Atomic).
  • Hậu quả: Ghi đè dữ liệu, sai lệch logic nghiệp vụ, bug ngẫu nhiên không thể tái hiện bằng tay.
  • Công cụ: Luôn bật cờ go run -race hoặc go test -race khi kiểm thử để Go tự động phát hiện và chỉ điểm vị trí xung đột.

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í