sync.WaitGroup
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 64 trên 107.
Trong bài học Cơ bản về Goroutine, chúng ta đã dùng time.Sleep để chờ các goroutine chạy xong, một cách làm thiếu tin cậy. sync.WaitGroup cung cấp một cách phù hợp để chờ một nhóm goroutine hoàn thành công việc.
WaitGroup hoạt động như một bộ đếm. Bạn tăng bộ đếm trước khi chạy một goroutine, giảm bộ đếm khi goroutine kết thúc và chặn cho đến khi bộ đếm về 0:
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // Tăng bộ đếm
go func(id int) {
defer wg.Done() // Giảm bộ đếm khi hoàn tất
fmt.Printf("Worker %d finished\n", id)
}(i)
}
wg.Wait() // Chờ đến khi bộ đếm bằng 0
fmt.Println("All workers complete")
}Ba phương thức chính là Add(n) để tăng bộ đếm thêm n, Done() để giảm bộ đếm đi một (tương đương với Add(-1)) và Wait() để chặn cho đến khi bộ đếm về không. Dùng defer wg.Done() để đảm bảo bộ đếm giảm ngay cả khi goroutine gặp lỗi panic.
Một lỗi thường gặp là gọi Add bên trong goroutine thay vì trước khi tạo goroutine. Điều này tạo ra một tình trạng tranh chấp, trong đó Wait có thể trả về trước khi tất cả goroutine được đăng ký:
// Sai: điều kiện tranh chấp
go func() {
wg.Add(1) // có thể chạy sau Wait()
defer wg.Done()
}()
// Đúng: gọi Add trước khi khởi chạy
wg.Add(1)
go func() {
defer wg.Done()
}()WaitGroups thường được nhúng trong các struct để phối hợp những thao tác đồng thời, khiến chúng trở nên thiết yếu khi xây dựng các kiểu an toàn với luồng trong Go.
Thử thách
DễHãy xây dựng một trình quản lý lượt tải xuống phối hợp nhiều lượt tải tệp đồng thời bằng sync.WaitGroup. Trình quản lý sẽ theo dõi thời điểm tất cả lượt tải hoàn tất mà không dựa vào các bộ hẹn giờ ngủ tùy ý.
Bạn sẽ sắp xếp code trong hai tệp:
downloader.go: Định nghĩa logic phối hợp lượt tải xuống.Tạo một cấu trúc
Downloadvới các trườngID(int),Filename(string) vàSize(int, biểu thị KB).Triển khai phương thức
Process(wg *sync.WaitGroup)choDownloadđể mô phỏng việc tải xuống bằng cách ngủ trong một khoảng thời gian dựa trên kích thước tệp (dùngSize * 10mili giây). Khi lượt tải hoàn tất, hãy in:Downloaded: [Filename] ([Size]KB). Dùngdefer wg.Done()để đảm bảo bộ đếm của WaitGroup giảm đúng cách.Tạo hàm
StartDownloads(downloads []Download) intđể khởi chạy đồng thời tất cả lượt tải bằng goroutine và chờ chúng hoàn tất. Nhớ gọiwg.Add(1)trước khi khởi chạy mỗi goroutine, không gọi bên trong goroutine. Trả về tổng kích thước của tất cả lượt tải.main.go: Đọc thông tin lượt tải xuống và điều phối các lượt tải đồng thời.Đọc số lượt tải, sau đó đọc ID, tên tệp và kích thước của từng lượt tải. Tạo các lượt tải và truyền chúng cho
StartDownloads. Sau khi tất cả lượt tải hoàn tất, hãy in:All downloads complete: [total]KB total
Đầu vào sẽ có dạng sau:
- Dòng 1: Số lượt tải xuống (số nguyên)
- Các dòng tiếp theo: Với mỗi lượt tải, ba dòng gồm ID (số nguyên), tên tệp (chuỗi) và kích thước tính bằng KB (số nguyên)
Ví dụ, với đầu vào:
3
1
report.pdf
20
2
image.png
5
3
data.csv
10Đầu ra sẽ hiển thị các lượt tải hoàn tất (tệp nhỏ hơn hoàn tất trước vì thời gian ngủ ngắn hơn), sau đó là phần tổng kết:
Downloaded: image.png (5KB)
Downloaded: data.csv (10KB)
Downloaded: report.pdf (20KB)
All downloads complete: 35KB totalĐiểm khác biệt chính so với việc dùng time.Sleep là wg.Wait() chặn cho đến khi tất cả goroutine báo hiệu hoàn tất bằng Done(), giúp bạn đồng bộ chính xác bất kể mỗi lượt tải mất bao lâu.
Tự làm thử
package main
import (
"fmt"
)
func main() {
// Đọc số lượt tải xuống
var n int
fmt.Scanln(&n)
// Đọc thông tin tải xuống
downloads := make([]Download, n)
for i := 0; i < n; i++ {
var id int
var filename string
var size int
fmt.Scanln(&id)
fmt.Scanln(&filename)
fmt.Scanln(&size)
downloads[i] = Download{
ID: id,
Filename: filename,
Size: size,
}
}
// TODO: Gọi StartDownloads với lát cắt downloads
// TODO: In ra phần tổng kết: "All downloads complete: [total]KB total"
}
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