Tiếp theo "Kinh nghiệm làm việc với Mutex trong Golang - phần 1"
Bài viết gốc Dancing with Go’s Mutexes

Điều 4: Đóng - mở khóa Mutex càng chi tiết, tỷ mỷ bao nhiêu thì càng tối ưu được hiệu năng so với sử dụng đóng mở khóa mutex ở mức thô nhưng cách làm chi tiết cũng sẽ việc bảo trì code khó khăn hơn. Nếu bạn phải loay hay qua lâu với Mutext, thì nên cân nhắc chuyển sang sang kỹ thuật đồng bộ bằng channel.

Điều 5: Nên đóng gói những phương thức có đóng mở Mutex. Người dùng chỉ cần import lại package mà không phải bận tâm chi tiết việc chia sẻ trạng thái qua Mutex.

Chạy thử ví dụ dưới đây. Bạn sẽ thấy ứng dụng bị dead lock "fatal error: all goroutines are asleep - deadlock!".
Lỗi xảy trong trong hàm get. Logic của hàm get là trước khi lấy ra một value theo key thì kiểm tra số lượng phần tử của biến cache.
 đã lock Mutex lần nhứ nhất, sau đó vào hàm count lại tiếp tục lock Mutex lần 2.

package main

import (
	"fmt"
	"sync"
)

//DataStore là một key-value in memory caching
type DataStore struct {
	sync.Mutex // ← Mutex để đồng bộ truy xuất vào biến cache
	cache      map[string]string
}

//Khởi tạo DataStore
func New() *DataStore {
	return &DataStore{
		cache: make(map[string]string),
	}
}

//Gán một cặp key-value vào DataStore
func (ds *DataStore) set(key string, value string) {
	ds.Lock()
	defer ds.Unlock()
	ds.cache[key] = value
}

//Lấy giá trị từ caching
func (ds *DataStore) get(key string) string {
	ds.Lock() //Lock Mutex lần thứ nhất
	defer ds.Unlock()
	if ds.count() > 0 { //Hàm ds.count() tiếp tục lock Mutex lần nữa
		item := ds.cache[key]
		return item
	}
	return ""
}
func (ds *DataStore) count() int {
	ds.Lock()
	defer ds.Unlock()
	return len(ds.cache)
}
func main() {
	/*
		Running this below will deadlock because the get() method will take a lock and will call the count() method which will also take a lock before the set() method unlocks()
	*/
	store := New()
	store.set("Go", "Lang")
	result := store.get("Go")
	fmt.Println(result)
}

Cách xử lý lỗi Mutex lock lồng lẫn nhau, chạy thử code không còn lỗi Dead Lock ở đây

  1. Tạo public method, chú ý tên public method chữ cái là chữ hoa. Ví dụ: Set, Get, Count. Public method sẽ có khóa, mở mutex
    ds.Lock()
    defer ds.Unlock()
  2. Public method sẽ gọi vào các private method không chứa các lệnh khóa, mở mutex. Như vậy sẽ không xảy ra 2 lần khóa mutex liên tục gây ra Dead Lock (khóa chết)
package main

import (
	"fmt"
	"sync"
)

type DataStore struct {
	sync.Mutex // ← this mutex protects the cache below
	cache      map[string]string
}

func New() *DataStore {
	return &DataStore{
		cache: make(map[string]string),
	}
}
func (ds *DataStore) set(key string, value string) {
	ds.cache[key] = value
}
func (ds *DataStore) get(key string) string {
	if ds.count() > 0 {
		item := ds.cache[key]
		return item
	}
	return ""
}
func (ds *DataStore) count() int {
	return len(ds.cache)
}
func (ds *DataStore) Set(key string, value string) {
	ds.Lock()
	defer ds.Unlock()
	ds.set(key, value)
}
func (ds *DataStore) Get(key string) string {
	ds.Lock()
	defer ds.Unlock()
	return ds.get(key)
}
func (ds *DataStore) Count() int {
	ds.Lock()
	defer ds.Unlock()
	return ds.count()
}
func main() {
	store := New()
	store.Set("Go", "Lang")
	result := store.Get("Go")
	fmt.Println(result)
}

Điều 6: Bên cạnh sync.Mutex đảm bảo tại một thời điểm cho có 1 tác vụ được truy xuất đến dữ liệu không quan tâm là đọc hay ghi. Chúng ta có còn sync.RWMutex.
sync.Mutex khác gì với sync.RWMutex ? Hãy tham khảo thêm phần hỏi đáp này trên StackOverflow

A RWMutex is a reader/writer mutual exclusion lock. The lock can be held by an arbitrary number of readers or a single writer. The zero value for a RWMutex is an unlocked mutex ~ RWMutex được khóa bởi nhiều hơn một tác vụ đọc hoặc duy nhất một tác vụ ghi. Khi RWMutex có giá trị 0 thì khóa mở.
Trong trường hợp dùng RWMutex các tác vụ đọc không cần phải đợi nhau vì bản chất đọc không thay đổi dữ liệu. Do đó RWMutex cho phép nhiều tác vụ đọc cùng giữ khóa nhưng chỉ có một tác vụ ghi giữ khóa.

// Dùng RLock trong hàm count vì không thay đổi giá trị
func count(){
  rw.RLock() // <-- Chú ý R trong RLock (read-lock)
  defer rw.RUnlock()  <-- Chú ý R trong RUnlock()
  return len(sharedState)
}
// Phải dùng Lock vì hàm set thay đổi giá trị sharedState
func set(key string, value string){
  rw.Lock() // <-- đây là lock bình thường (write-lock)
  defer rw.Unlock() // <-- notice we Unlock() has no R in it
  sharedState[key] = value  // <-- Thay đổi dữ liệu
}

Điều 7 : Hãy tìm hiểu kỹ về Data Race Detector trong Golang. Data races là lỗi phổ biến và khó gỡ rỗi nhất trong hệ thống chạy đồng thời. Data race xảy ra khi hai goroutine cùng truy xuất đồng thời vào một biến, và có ít nhất một goroutine sửa đổi dữ liệu.

  • Nếu bạn chưa viết unit/integration test dùng Race Detector như là một phần của hệ thống CI/CD thì hãy học và viết ngay bây giờ
  • Nếu bạn không chủ động viết các test case để buộc hệ thống xử lý đồng thời vì dùng Race Detector cũng không có tác dụng
  • Không dùng data race detector trong môi trường chạy thật vì nó giảm hiệu năng hệ thống
  • Nếu race detector phát hiện có data race thì chắc chắn đây là lỗi thực sự, bạn phải sửa dứt điểm.
  • Tình trạng ganh đua vẫn có thể xảy đến với đồng bộ dùng channel