sync.Mutex và sync.RWMutex
Thuộc phần Lập trình hướng đối tượng trong hành trình GO của Coddy. Bài học 63 trên 107.
Dù channel là cách Go ưu tiên để phối hợp các goroutine, đôi khi bạn cần trực tiếp bảo vệ dữ liệu dùng chung. Gói sync cung cấp mutex: các khóa đảm bảo mỗi lần chỉ một goroutine truy cập tài nguyên.
sync.Mutex có hai phương thức: Lock() và Unlock(). Khi một goroutine gọi Lock(), nó có quyền truy cập độc quyền. Các goroutine khác gọi Lock() sẽ bị chặn cho đến khi Unlock() được gọi:
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Increment() {
c.mu.Lock()
c.value++
c.mu.Unlock()
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}Dùng defer c.mu.Unlock() giúp mở khóa ngay cả khi hàm trả về sớm hoặc gặp panic: đây là cách làm phổ biến và được khuyến nghị.
Khi lượt đọc diễn ra thường xuyên nhưng lượt ghi hiếm khi xảy ra, sync.RWMutex mang lại hiệu năng tốt hơn. Nó cho phép nhiều lượt đọc diễn ra đồng thời, nhưng lượt ghi cần quyền truy cập độc quyền:
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(key string) string {
c.mu.RLock() // Cho phép nhiều luồng đọc cùng lúc
defer c.mu.RUnlock()
return c.data[key]
}
func (c *Cache) Set(key, value string) {
c.mu.Lock() // Chỉ một luồng được truy cập để ghi
defer c.mu.Unlock()
c.data[key] = value
}Dùng RLock()/RUnlock() cho các thao tác đọc và Lock()/Unlock() cho các thao tác ghi. Cách này cho phép nhiều thao tác đọc diễn ra đồng thời, đồng thời đảm bảo các thao tác ghi an toàn.
Thử thách
DễHãy xây dựng một hệ thống quản lý hàng tồn kho an toàn khi chạy đồng thời, theo dõi số lượng sản phẩm trong kho. Hệ thống sẽ xử lý an toàn các thao tác đọc và ghi đồng thời bằng mutex, đảm bảo tính toàn vẹn của dữ liệu khi nhiều thao tác diễn ra cùng lúc.
Bạn sẽ tổ chức code trong hai tệp:
inventory.go: Định nghĩa hệ thống quản lý hàng tồn kho an toàn khi chạy đồng thời.Tạo một struct
Inventorylưu số lượng sản phẩm trong một map và dùngsync.RWMutexđể bảo vệ quyền truy cập. Hệ thống quản lý hàng tồn kho cần hỗ trợ các thao tác sau:NewInventory() *Inventory- Tạo một hệ thống quản lý hàng tồn kho mới với map đã được khởi tạoAddStock(product string, quantity int)- Thêm số lượng vào kho của một sản phẩm (dùng khóa độc quyền vì thao tác này sửa đổi dữ liệu)GetStock(product string) int- Trả về số lượng hiện có trong kho của một sản phẩm, hoặc 0 nếu không tìm thấy (dùng khóa đọc vì thao tác này chỉ đọc dữ liệu)RemoveStock(product string, quantity int) bool- Trừ số lượng khỏi kho nếu kho còn đủ hàng. Trả vềtruenếu thành công,falsenếu kho không đủ hàng (dùng khóa độc quyền)
Nhớ dùng
deferđể mở khóa, đảm bảo các khóa luôn được giải phóng đúng cách.main.go: Đọc các thao tác và minh họa hệ thống quản lý hàng tồn kho an toàn khi chạy đồng thời.Đọc số lượng thao tác, sau đó xử lý từng thao tác. Mỗi thao tác có một loại (
add,gethoặcremove), tên sản phẩm và số lượng đối với thao tácadd/remove.Với mỗi thao tác, in ra kết quả:
add: In raAdded [quantity] [product]get: In ra[product]: [stock] in stockremove: In raRemoved [quantity] [product]nếu thành công, hoặcInsufficient stock for [product]nếu không thành công
Đầu vào sẽ có định dạng như sau:
- Dòng 1: Số lượng thao tác (số nguyên)
- Các dòng tiếp theo: Với mỗi thao tác:
- Loại thao tác (
add,gethoặcremove) - Tên sản phẩm
- Số lượng (chỉ có với thao tác
addvàremove)
- Loại thao tác (
Ví dụ, với đầu vào:
5
add
apples
50
get
apples
remove
apples
30
remove
apples
25
get
applesĐầu ra của bạn sẽ là:
Added 50 apples
apples: 50 in stock
Removed 30 apples
Insufficient stock for apples
apples: 20 in stockRWMutex cho phép nhiều lệnh gọi GetStock đọc dữ liệu đồng thời, trong khi AddStock và RemoveStock có quyền truy cập độc quyền khi sửa đổi hàng tồn kho.
Tự làm thử
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Đọc số lượng thao tác
line, _ := reader.ReadString('\n')
numOps, _ := strconv.Atoi(strings.TrimSpace(line))
// Tạo kho hàng mới
inventory := NewInventory()
// Xử lý từng thao tác
for i := 0; i < numOps; i++ {
// Đọc loại thao tác
opLine, _ := reader.ReadString('\n')
opType := strings.TrimSpace(opLine)
// Đọc tên sản phẩm
productLine, _ := reader.ReadString('\n')
product := strings.TrimSpace(productLine)
// TODO: Xử lý từng loại thao tác (add, get, remove)
// Với "add" và "remove", đọc số lượng từ đầu vào
// Gọi phương thức (method) phù hợp của kho hàng
// In ra kết quả theo mô tả thử thách
switch opType {
case "add":
// TODO: Đọc số lượng, thêm hàng vào kho, in ra kết quả
case "get":
// TODO: Lấy số lượng hàng trong kho, in ra kết quả
case "remove":
// TODO: Đọc số lượng, lấy hàng khỏi kho, in ra kết quả phù hợp
}
}
}
Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.
Tất cả bài học trong Lập trình hướng đối tượng
1Nền tảng OOP trong Go
Tệp bên ngoàiWorkspace và module trong GoPackage và importTên được export và không được exportGiới thiệu về OOP trong GoStruct như lớpĐịnh nghĩa phương thức cho structReceiver kiểu con trỏ và kiểu giá trịKhởi tạo structHàm khởi tạoÔn tập: máy tính đơn giản4Interface
Giới thiệu về interfaceTriển khai ngầm địnhInterface như một hợp đồngInterface rỗng (any)Khẳng định kiểuswitch kiểuKết hợp interfaceInterface Stringer và ErrorÔn tập: Máy tính hình học7Tính đóng gói
Trường được xuất và không được xuấtĐóng gói ở cấp packagePhương thức getter và setterẨn thông tin trong GoÔn tập: hồ sơ học sinh10Generics (Go 1.18 trở lên)
Giới thiệu về GenericsTham số kiểuRàng buộc kiểuStruct tổng quátGiải pháp thay thế cho phương thức tổng quátÔn tập: tập hợp tổng quát13Các mẫu thiết kế phần 1
Giới thiệu về mẫu thiết kếMẫu SingletonMẫu FactoryMẫu Abstract FactoryMẫu ObserverMẫu Strategy2Tìm hiểu chuyên sâu về kiểu dữ liệu và struct
Kiểu cơ bản và kiểu tổng hợpĐịnh nghĩa kiểu dữ liệu tùy chỉnhThẻ structStruct ẩn danhStruct lồng nhauGiá trị zero và giá trị mặc địnhÔn tập: sổ liên lạc5Ưu tiên kết hợp hơn kế thừa
Vì sao Go không có kế thừaNhập môn nhúng structTự động nâng phương thứcNhúng nhiều structNhúng và kết tậpChe khuất phương thức được nhúngÔn tập: hệ phân cấp nhân viên8Xử lý lỗi và lập trình hướng đối tượng
Giao diện errorCác kiểu lỗi tùy chỉnhBọc lỗi với fmt.ErrorfLỗi sentinelerrors.Is() và errors.As()Panic, defer và recoverÔn tập: trình phân tích tệp3Con trỏ và bộ nhớ
Cơ bản về con trỏ trong GoCon trỏ đến structTruyền theo giá trị và tham chiếuHàm new()Thu gom rác trong GoÔn tập: xây dựng danh sách liên kết6Đa hình trong Go
Đa hình qua interfaceĐịnh kiểu vịt trong GoQuy tắc thỏa mãn interfaceCác tập hợp đa hìnhTiêm phụ thuộcÔn tập: bộ xử lý thanh toán9Lập trình đồng thời và lập trình hướng đối tượng
Cơ bản về goroutineChannel và giao tiếpChannel có bộ đệm và không có bộ đệmCâu lệnh selectsync.Mutex và sync.RWMutexsync.WaitGroupThiết kế struct an toàn luồngÔn tập: worker poolTự luyện tập: Trình biên dịch Go online