Golang 10 - Concurrency - Mutex


Written on March 17, 2024

Critical section

Định nghĩa critical section trong lập trình concurrent: Khi chương trình đang chạy, một phần đoạn code có thể thay đổi resource dùng chung thì không nên được access bởi nhiều Goroutines tại cùng một thời điểm. Đoạn code modifies shared resource được gọi là critical section.

Ví dụ, ta có đoạn code cập nhật giá trị của x như sau:

x = x + 1

Nếu chỉ có một Goroutine truy cập => không có vấn đề gì xảy ra nhưng giả sử có 2 Goroutine cùng chạy đoạn code trên concurently.

Đoạn code trên khi được executed bởi system sẽ được chia nhỏ ra thành những step sau(có thể có nhiều step hơn nhưnng hay giả sử có 3 steps sau)

1. Lấy giá trị hiện tại của x
2. Tính toán x + 1
3. Gán gía trị của step 2 vào x

Hình minh hoạ nếu có 2 Goroutine chạy cùng một lúc vô critital resource

_config.yml

Trong trường hợp trên giả sử giá trị ban đầu của x là 0, Goroutine 1 bắt đầu thực thi, lấy giá trị ban đầu của x, tính toán x+1 và trước khi gán lại gía trị mới cho x system chuyển qua Goroutine 2. Bây giờ Goroutine 2 lấy giá trị ban đầu của x, vẫn là 0, tăng lên thành 1. Sau đó, system switch lại về Goroutin 1. Bây giờ, Goroutine 1 gán giá trị 1 cho x, và x có giá trị 1. Sau đó, Goroutine 2 tiếp tục thực thi và gán gía trị mà nó tính toán, 1, cho x. Sau khi cả 2 Goroutine chay, x vẫn mang giá trị 1.

_config.yml Trong kịch bản sau, Goroutine 1 chạy và kết thúc 3 steps, tăng giá trị lên của x lên 1. Sau đó, Goroutine 2 mới thực thi, bây giờ giá trị của x là 1, Goroutine 2 tăng thêm +1 và kết thúc. Giá trị cuối cùng của x là 2.

Từ 2 kịch bản trên, giá trị cuối cùng của x có thể là 1 hoặc 2, phụ thuộc vào cách system switch giữa các Goroutinne. Nó được gọi là race condition.

Trong ví dụ trên nếu race conditon có thể tránh được nếu chỉ một Goroutine được cho phép truy cập vào critical section tại bất kì thời điểm nào. Go có thể làm việc đó bằng cách sử dụng Mutex.

Mutex

Mutex là cơ chế lock để đảm bảo chỉ có duy nhất một Goroutine được chạy ở critical code tại bất kỳ thời điểm nào để tránh race conditions xảy ra.

Có 2 methods chính trong mutex là LockUnlock. Bất kì đoạn code nào ở giữa LockUnlock sẽ chỉ cho phép 1 Goroutine chạy để tránh race condition.

mutex.Lock()
x = x + 1
mutex.Unlock()

Nếu một Goroutine đang hold lock và bất kì Goroutinne nào khác cố truy cập vào lock, Goroutine mới sẽ bị block cho tới khi mutex unlock.

Chương trình với race condition

Chúng ta có chương trình sau:

package main
import (
	"fmt"
	"sync"
	)
var x  = 0
func increment(wg *sync.WaitGroup) {
	x = x + 1
	wg.Done()
}
func main() {
	var w sync.WaitGroup
	for i := 0; i < 1000; i++ {
		w.Add(1)		
		go increment(&w)
	}
	w.Wait()
	fmt.Println("final value of x", x)
}

Chương trình trên, chúng ta tạo 1000 increment Goroutine và chạy concurrently. Mỗi lần chay, chương trình sẽ in ra một kết quả khác nhau vì race condition.

Giải quyết race condtion using mutex

package main
import (
	"fmt"
	"sync"
	)
var x  = 0
func increment(wg *sync.WaitGroup, m *sync.Mutex) {
	m.Lock()
	x = x + 1
	m.Unlock()
	wg.Done()	
}
func main() {
	var w sync.WaitGroup
	var m sync.Mutex
	for i := 0; i < 1000; i++ {
		w.Add(1)		
		go increment(&w, &m)
	}
	w.Wait()
	fmt.Println("final value of x", x)
}

Với mutex, mỗi lần chạy chương trình đều in ra

final value of x 1000

Chú ý: Mutex phải pass vào funion dạng con trỏ. Nếu passed by value, mỗi Goroutine sẽ có một bản copy Mutex của riêng nó, race condition vẫn xảy ra.

Giải quyết bằng channel

Thay vì dùng mutex, chúng ta có thể dùng channel để giải quyết race condition.

package main
import (
	"fmt"
	"sync"
	)
var x  = 0
func increment(wg *sync.WaitGroup, ch chan bool) {
	ch <- true
	x = x + 1
	<- ch
	wg.Done()	
}
func main() {
	var w sync.WaitGroup
	ch := make(chan bool, 1)
	for i := 0; i < 1000; i++ {
		w.Add(1)		
		go increment(&w, ch)
	}
	w.Wait()
	fmt.Println("final value of x", x)
}

Chúng ta tạo ra 1 buffered channel với cap là 1. Channel này dùng để đảm bảo chỉ có 1 Goroutine truy cập vào critical section tại 1 thời điểm.

Mutex and channels.

Channel dùng khi cần giao tiếp giữa cách Goroutine, còn mutex sửa dụng khi chỉ cần một Goroutine truy cập vào critial section.