Trong lập trình đồng bộ (Concurrent Programming), data races luôn là một vấn đề khá đau đầu, cũng như khó debug.  Trong bài viết này, chúng ta sẽ cùng nhau tìm hiểu về data races trong golang cũng như cách phát hiện và xử lý chúng. Bài viết có sử dụng một số kiến thức đã được giới thiệu trước đó về lập trình đồng bộ với Golang, các bạn có thể xem lại tại đây

1. Data race là gì ?

Data race là hiện tượng xảy ra khi có nhiều hơn một goroutine cùng truy cập và thay đổi một dữ liệu, dẫn đến việc giá trị của dữ liệu đó không thay đổi như mong muốn ban đầu. Ví dụ ở đoạn code sau:

Với đoạn code trên giá trị trả về sẽ xảy ra hai trường hợp. i = 0i = 1 tùy vào routine nào hoàn thành trước:

Ở trường hợp đầu tiền, main routine hoàn thành và chương trình kết thúc trước khi routine thứ hai kịp thay đổi giá trị của biến nên kết quả trả về sẽ là i = 0.

Ngược lại, nếu routine thứ hai kịp thay đổi giá trị biến i trước khi main routine hoàn thành thì kết quả trả về sẽ là giá trị đã được thay đổi i = 1.

Điều này làm cho kết quả cuối cùng của chương trình trở nên không thể dự báo trước và cũng là nơi phát sinh ra những con bug cứng đầu, khó tìm thấy.

2. Phát hiện data race với Data Race Detector

Ở đoạn code trên, mọi thứ vẫn còn đơn giản, chúng ta chỉ cần debug 2 routine nên vẫn có thể dễ dàng phát hiện được data races. Nhưng nếu trên một hệ thống lớn và phức tạp, phải handle với hàng chục, hàng trăm goroutine cùng thì phải làm thế nào đây?

Từ kết quả trả về khi sử dụng Data Race Detector. Chúng ta có thể thấy:

  1. Địa chỉ 0x00c00006c068 ( địa chỉ của biến i) được thay đổi bởi gorountine 7
  2. Biến i được truy cập và đọc bởi main goroutine .
  3. goroutine 7 vẫn đang chạy mặc dù main goroutine đã kết thúc.

Như vậy kết quả trả về sẽ là trường hợp 1 (i = 0) giống như những gì ta đã dự đoán.

3. Khắc phục data race

Bởi vì data race xảy ra khi các goroutine cùng tương tác với data trong cùng một thời điểm nên cách khắc phục về cơ bản cũng sẽ giống như hình trên. Chùng ta sẽ block các goroutine lại và unblock chúng vào thời điểm thích hợp.

3.1 Sử dụng wait group

Cách đơn giản và trực tiếp nhất đó là, ta sẽ block main routine lại, chờ cho đến khi routine thay đổi giá trị biến i hoàn thành xong công việc thì main routine mới được đọc giá trị của biến i.

Đầu tiên, ta sẽ khởi tạo wait group (line 10), thêm vào counter của wait group 1 đơn vị (line 12). Sau đó tạo goroutine (line 13) và block main routine lại với wg.Wait() (line 18) . Wait group sẽ có nhiệm vụ block các routine gọi đến wg.Wait() và unblock khi counter của wait group = 0. Với mỗi lần wg.Done() được gọi, counter của wait group sẽ giảm 1 đơn vị. Như vậy đoạn code trên sẽ diễn ra như flow sau:

Với việc sử dụng wait group, data race đã không còn xuất hiện nữa.

3.2 Sử dụng channel

Bên cạnh wait group, golang còn cung cấp cho cúng ta một công cụ khác là channel.  Ở đoạn code trên, main routine sẽ được block cho đến khi channel nhận được data gửi từ routine increase. Code flow sẽ như hình bên dưới.

3.3 Sử dụng mutex

Với cách xử lý bằng channel và wait group, các routine sẽ phải giao tiếp với nhau để xác định khi nào thì routine này đã hoàn thành xong nhiệm vụ, để routine khác bắt đầu hoặc kết thúc v.v.... Nhưng nếu các routine không cần phải giao tiếp với nhau thì liệu có cách nào khác để khắc phục data race không ?

Để làm được điều đó thì chúng ta sẽ cần đến Mutex. Mutex được hiểu nôm na cũng như một toilet công cộng, còn các routine sẽ là người sử dụng. Khi toilet được sử dụng thì cửa toilet sẽ khóa lại, người khác sẽ không thể sử dụng nữa cho đến khi người trước đã sử dụng xong và mở cửa ra. Hãy cùng xem xét đoạn code sau:

Đầu tiên, chúng ta sẽ tạo một toilet chứa biến i = 0 và mutex (ta có thể xem mutex như cửa toilet). Toilet này sẽ có 2 method là GetIncrease.

Ở method Get, cửa toilet sẽ được khóa lại, giá trị i sẽ được đọc và sau đó mở cửa toilet ra. Tương tự vậy cho method Increase, đầu tiên cửa toilet cũng sẽ được khóa lại, giá trị i được tăng lên 1 và cửa toilet sẽ được mở ra khi mọi thứ đã xử lý xong. Code flow sẽ như hình bên dưới.

4. Lời kết.

Data race luôn là một vấn đề khó giải quyết, là nơi dễ phát sinh bug khó tìm ra. Nhưng may mắn thay golang đã cung cấp cho chúng ta công cụ để phát hiện chúng cũng như có khá nhiều cách để xử lý data race. Mỗi cách đều có một cái hay, cái dở riêng,  hãy dùng chúng một cách hợp lý nhất.

5. Tham khảo

https://www.sohamkamani.com/blog/2018/02/18/golang-data-race-and-how-to-fix-it/

https://tour.golang.org/concurrency/9

https://en.wikipedia.org/wiki/Mutual_exclusion