Menu
CoddyTech

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.

challenge icon

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 Download với các trường ID (int), Filename (string) và Size (int, biểu thị KB).

    Triển khai phương thức Process(wg *sync.WaitGroup) cho Download để 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ùng Size * 10 mili giây). Khi lượt tải hoàn tất, hãy in: Downloaded: [Filename] ([Size]KB). Dùng defer 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ọi wg.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"
}
quiz iconTự kiểm tra

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

Tự luyện tập: Trình biên dịch Go online