Cách sử dụng Golang WaitGroup hiệu quả

17 phút đọc1 lượt xem

Quản lý concurrency trong Golang? WaitGroup là công cụ hỗ trợ bạn đồng bộ hóa nhiều goroutine. Bài viết này hướng dẫn bạn những điều cơ bản về cách sử dụng WaitGroup một cách hiệu quả. Tìm hiểu cách đặt số lượng goroutine, hoàn thành signal và thậm chí tránh những cạm bẫy phổ biến.

Golang’s WaitGroup là một công cụ đơn giản nhưng cần thiết để quản lý các tác vụ đồng thời. Nó đảm bảo rằng nhiều goroutines hoàn thành thực thi của chúng trước khi tiếp tục. Bằng cách hiểu cơ chế của nó, các nhà phát triển có thể tận dụng đồng thời với độ chính xác và dễ dàng.

Dựa trên nội dung từ bài viết “How to Use Golang WaitGroup Effectively” trên MarketSplash, đây là những thông tin quan trọng:

  • WaitGroup trong Go hoạt động như một bộ đếm để quản lý đồng thời, đảm bảo tất cả các goroutines hoàn thành trước khi tiếp tục.
  • Sử dụng WaitGroup.Add để đặt số lượng goroutines đảm bảo đồng bộ chính xác trong các tác vụ đồng thời.
  • Defer WaitGroup.Done trong mỗi goroutine đảm bảo rằng bộ đếm được giảm khi hoàn thành.
  • WaitGroup.Wait block chương trình chính cho đến khi tất cả các goroutines hoàn thành, quan trọng cho luồng thực thi được kiểm soát.

Hy vọng điều này giúp ích cho bạn! Hãy cho tôi biết nếu bạn có bất kỳ câu hỏi nào khác.

Trong thế giới của Golang, concurency là một tính năng nổi bật. Một trong những công cụ hỗ trợ quản lý concurency là WaitGroup từ gói sync. Đó là một cơ chế đơn giản nhưng hiệu quả để đảm bảo rằng nhiều goroutines hoàn thành trước khi chương trình tiếp tục

WaitGroup hoạt động như thế nào?

Một WaitGroup trong Go hoạt động như một bộ đếm. Khi bạn khởi chạy một goroutine, bạn tăng giá trị của bộ đếm này. Sau khi goroutine hoàn thành nhiệm vụ của nó, nó giảm giá trị của bộ đếm. Hàm chính hoặc bất kỳ hàm nào khác có thể chờ đợi bộ đếm này giảm về 0, cho thấy rằng tất cả các goroutines đã hoàn thành.

Đây là cú pháp cơ bản để sử dụng WaitGroup:

var wg sync.WaitGroup wg.Add(1) // Increment the WaitGroup counter by 1 
go func() { 
	defer wg.Done() // Decrement the WaitGroup counter when goroutine completes 
	// Your goroutine code here 
}() 
wg.Wait() // Wait for all goroutines to complete

wg.Add(1): Đây là hàm tăng giá trị của WaitGroup. Số lượng bạn truyền vào Add là số lần bạn muốn tăng giá trị của WaitGroup.

defer wg.Done(): Thông thường, hàm này được đặt ở cuối goroutine để đảm bảo rằng nó chạy khi goroutine kết thúc nhiệm vụ của nó.

wg.Wait(): Hàm này chặn goroutine hiện tại (thường là goroutine chính) cho đến khi giá trị của WaitGroup trả về 0.

Cách sử dụng thực tế của WaitGroup

Một trong những ứng dụng thực tế của WaitGroup trong Go là khi bạn muốn lấy dữ liệu từ nhiều URL đồng thời. Thay vì chờ từng URL lấy dữ liệu tuần tự, bạn có thể sử dụng goroutines để lấy chúng song song và sử dụng WaitGroup để chờ tất cả các lấy dữ liệu hoàn thành 

var wg sync.WaitGroup 
urls := []string{"http://example1.com", "http://example2.com", "http://example3.com"} 
for _, url := range urls { 
	wg.Add(1) 
	go func(u string) { 
		defer wg.Done() // Fetch data from URL 
		fmt.Println("Fetched data from", u) 
	}(url) 
} 
wg.Wait() 
fmt.Println("All URLs fetched!")

Chúng ta lặp lại từng URL, khởi chạy một goroutine mới cho mỗi URL.
Bên trong goroutine, chúng ta lấy dữ liệu từ URL và sau đó gọi Done để đánh dấu hoàn thành.
Hàm main chờ đợi tất cả các goroutines hoàn thành trước khi in ra “All URLs fetched!”

Bằng cách hiểu những điều cơ bản về WaitGroup, bạn có thể quản lý và đồng bộ hóa các goroutine của mình một cách hiệu quả, đảm bảo các hoạt động trơn tru và đồng thời trong các ứng dụng Golang của bạn.

Tạo một WaitGroup

Tạo một WaitGroup mới là một bước quan trọng khi làm việc với goroutines trong Golang. Điều này đảm bảo rằng tất cả các tác vụ đồng thời đã hoàn thành trước khi tiếp tục. Đó là lý do tại sao WaitGroup từ gói sync được sử dụng. Nó cung cấp một cách để chờ đợi một tập hợp các goroutines hoàn thành thực thi

Khởi tạo WaitGroup

Để bắt đầu sử dụng WaitGroup, trước tiên bạn cần khai báo và khởi tạo nó. Giá trị 0 của WaitGroup rất hữu ích, nghĩa là bạn không cần khởi tạo nó bằng một hàm đặc biệt. Đây là cách bạn khai báo một WaitGroup mới:

Đây là cách bạn khai báo một WaitGroup mới:

var wg sync.WaitGroup

var wg sync.WaitGroup: Dòng này khai báo một biến wg thuộc loại WaitGroup từ gói đồng bộ hóa.

Sử dụng WaitGroup

Sau khi khai báo WaitGroup của mình, bạn có thể bắt đầu sử dụng nó với goroutines. Đây là một ví dụ đơn giản:

var wg sync.WaitGroup 
wg.Add(1) 
go func() { 
	defer wg.Done() 
	fmt.Println("Executing goroutine!") 
}() 
wg.Wait()

wg.Add(1): Trước khi khởi chạy goroutine, chúng ta thông báo với WaitGroup rằng chúng ta còn một goroutine nữa để chờ.

defer wg.Done(): Bên trong goroutine, chúng ta trì hoãn lệnh gọi tới Done để đảm bảo nó được gọi sau khi goroutine hoàn thành công việc của nó.

wg.Wait(): Điều này sẽ chặn cho đến khi tất cả goroutines được gọi là Done và bộ đếm bên trong WaitGroup trở về 0.

Tại sao nên sử dụng WaitGroup?

Việc sử dụng WaitGroup đảm bảo rằng hàm chính hoặc bất kỳ phần nào khác trong mã của bạn không tiếp tục cho đến khi tất cả các goroutine được chỉ định đã hoàn thành nhiệm vụ của chúng. Đó là một cách rõ ràng và hiệu quả để xử lý đồng thời mà không cần dùng đến các cơ chế đồng bộ hóa phức tạp hơn. 
Về bản chất, WaitGroup là một bộ đếm theo dõi số lượng goroutine mà bạn đang chờ đợi. Bằng cách hiểu cách khởi tạo và cách sử dụng nó, bạn có thể quản lý nhiều goroutine một cách hiệu quả và đảm bảo rằng mã của bạn chạy như mong đợi.

Sử dụng Add để đặt số lượng Goroutines

Ở Golang, khi xử lý các tác vụ đồng thời bằng goroutine, điều quan trọng là phải theo dõi có bao nhiêu tác vụ đang chạy. WaitGroup cung cấp phương thức Add để phục vụ mục đích này. Bằng cách sử dụng Add, bạn có thể chỉ định số lượng goroutine mà WaitGroup sẽ đợi.

Vai trò của phương thức Add

Phương thức Add tăng bộ đếm nội bộ của WaitGroup. Bộ đếm này biểu thị số lượng goroutine chưa hoàn thành nhiệm vụ của chúng. Khi khởi chạy một goroutine mới, bạn nên tăng bộ đếm này cho phù hợp. Đây là cách cơ bản để sử dụng phương thức Add:

wg.Add(numberOfGoroutines)

wg.Add(numberOfGoroutines): Điều này làm tăng bộ đếm của WaitGroup theo số được chỉ định.

Ví dụ về việc sử dụng Add

Hãy xem xét một tình huống trong đó bạn muốn xử lý đồng thời một danh sách các mục. Đây là cách bạn có thể sử dụng phương pháp Add:

var wg sync.WaitGroup 
items := []string{"item1", "item2", "item3"} 
for _, item := range items { 
	wg.Add(1) 
	go func(i string) { 
		defer wg.Done() 
		fmt.Println("Processing:", i) 
	}(item) 
} 
wg.Wait()

Chúng tôi lặp lại từng mục và với mỗi mục, chúng tôi tăng bộ đếm của WaitGroup bằng cách sử dụng wg.Add(1). 
Bên trong goroutine, sau khi xử lý xong mục, chúng ta gọi Done để giảm bộ đếm. 
Cuối cùng, wg.Wait() đảm bảo rằng hàm main không tiếp tục cho đến khi tất cả các mục đã được xử lý.

Thực hiện Done để hoàn thành Signal

Trong khái niệm concurency của Golang, việc báo hiệu sự hoàn thành của một goroutine cũng quan trọng như việc bắt đầu nó. WaitGroup cung cấp phương thức Done, một cách đơn giản để chỉ ra rằng goroutine đã hoàn thành nhiệm vụ của nó.

Mục đích của phương thức Done

Phương thức Done đóng một vai trò đơn giản nhưng quan trọng: nó làm giảm bộ đếm nội bộ của WaitGroup. Khi bộ đếm này đạt đến 0, mọi goroutine đang chờ, thường là goroutine chính, sẽ được bỏ chặn và được phép tiếp tục.

Sử dụng phương thức Done rất đơn giản:

wg.Done()

wg.Done(): Điều này làm giảm bộ đếm của WaitGroup đi một.

Ví dụ về việc sử dụng Done

Hãy tưởng tượng bạn đang tải xuống nhiều tệp cùng một lúc. 
Đây là minh họa về cách sử dụng phương thức Done:

var wg sync.WaitGroup 
files := []string{"file1.txt", "file2.txt", "file3.txt"} 
for _, file := range files { 
	wg.Add(1) 
	go func(f string) { 
		defer wg.Done() // Simulate downloading a file
		fmt.Println("Downloaded:", f) 
	}(file) 
} 
wg.Wait()

Đối với mỗi tệp, chúng tôi tăng bộ đếm của WaitGroup. 
Trong goroutine, sau khi mô phỏng quá trình tải xuống tệp, chúng tôi sử dụng defer wg.Done() để đảm bảo bộ đếm bị giảm sau khi tác vụ hoàn tất. 
wg.Wait() giữ chức năng chính cho đến khi tất cả các tệp được tải xuống.

Áp dụng Wait để Block cho đến khi tất cả Goroutine kết thúc

Trong lập trình concurrent với Golang, việc quản lý việc hoàn thành nhiều goroutine là điều cần thiết. WaitGroup cung cấp phương thức Wait, một cơ chế chặn goroutine đang gọi cho đến khi tất cả các goroutine khác báo hiệu sự hoàn thành của chúng.

Vai trò của phương thức Wait

Chức năng chính của phương thức Wait là tạm dừng việc thực thi goroutine hiện tại cho đến khi bộ đếm bên trong của WaitGroup giảm xuống 0. Điều này đảm bảo rằng chương trình không tiến hành quá sớm, cho phép hoàn thành tất cả các tác vụ đồng thời. 
Việc sử dụng phương thức Wait:

wg.Wait()

wg.Wait(): Điều này chặn goroutine hiện tại cho đến khi bộ đếm của WaitGroup bằng 0.

Ví dụ về việc sử dụng Wait

Giả sử bạn đang xử lý đồng thời một loạt mục nhập dữ liệu. 
Đây là cách bạn có thể áp dụng phương pháp Wait:

var wg sync.WaitGroup 
dataBatch := []int{10, 20, 30, 40, 50} 
for _, data := range dataBatch { 
	wg.Add(1) 
	go func(d int) { 
		defer wg.Done() // Simulate dataprocessing
		fmt.Println("Processed data:", d) 
	}(data) 
} 
wg.Wait() 
fmt.Println("All data processed!")

Chúng tôi lặp lại từng mục nhập dữ liệu, khởi chạy một goroutine cho mỗi mục nhập. 

Sau khi mô phỏng quá trình xử lý dữ liệu trong goroutine, chúng ta gọi là Done. 

Hàm chính sử dụng wg.Wait() để đảm bảo nó không tiếp tục cho đến khi tất cả các mục nhập dữ liệu được xử lý.
 

Bằng cách tận dụng phương pháp Wait, các nhà phát triển có thể đạt được luồng đồng bộ hóa trong các ứng dụng Golang đồng thời của họ. Đó là một công cụ cơ bản đảm bảo tất cả các nhiệm vụ được hoàn thành trước khi chuyển sang các bước tiếp theo, mang lại độ tin cậy và hiệu quả trong các hoạt động đồng thời.

Những sai lầm thường gặp và cách tránh chúng

Làm việc với goroutines và đồng bộ hóa ở Golang có thể khó khăn. Mặc dù WaitGroup cung cấp một cơ chế mạnh mẽ để quản lý đồng thời nhưng vẫn có những cạm bẫy phổ biến mà các nhà phát triển có thể gặp phải. Hãy cùng khám phá những sai lầm này và tìm hiểu cách vượt qua chúng.

Không sử dụng Add trước khi khởi chạy Goroutines

Một lỗi thường xuyên xảy ra là quên gọi phương thức Add trước khi bắt đầu goroutine. Điều này có thể dẫn đến tình trạng race condition trong đó phương thức Wait có thể quay trở lại trước khi tất cả goroutine hoàn thành.

var wg sync.WaitGroup 
go func() { defer wg.Done() // Some code here }() 
wg.Wait()

Trong đoạn mã trên, không có lệnh gọi wg.Add(1). Điều này có thể khiến wg.Wait() quay trở lại ngay lập tức mà không cần đợi goroutine kết thúc.
Luôn gọi wg.Add(1) ngay trước khi khởi chạy một goroutine mới.

Không khớp khi gọi Add và Done

Một lỗi phổ biến khác là mất cân bằng giữa số lượng gọi Add và Done. Điều này có thể khiến chương trình của bạn bị treo vô thời hạn hoặc tiến hành sớm.

var wg sync.WaitGroup 
wg.Add(2) 
go func() { defer wg.Done() // Some code here }() 
wg.Wait()

Ở đây, chúng tôi đã thêm 2 vào bộ đếm WaitGroup nhưng chỉ giảm nó một lần. Điều này sẽ khiến chương trình bị treo. 
Đảm bảo rằng số lượng lệnh gọi Thêm khớp với số lượng goroutine bạn đang khởi chạy và mỗi goroutine gọi Xong sau khi hoàn tất.
 

Không sử dụng Defer với Done

Việc quên sử dụng defer với phương thức Done có thể dẫn đến việc nó không được gọi nếu goroutine được giải phóng do lỗi hoặc câu lệnh return.

var wg sync.WaitGroup 
wg.Add(1) 
go func() { if someCondition { return } wg.Done() }() 
wg.Wait()

Nếu someCondition là true, goroutine sẽ thoát mà không gọi Done, khiến chương trình bị treo. 
Luôn sử dụng defer wg.Done() khi bắt đầu goroutine của bạn để đảm bảo nó được gọi bất kể goroutine thoát ra như thế nào.

Ví dụ thực tế về WaitGroup

Khi đi sâu vào mô hình Concurency của Golang, WaitGroup là công cụ cơ bản để đồng bộ hóa. Hãy cùng khám phá một số ví dụ thực tế để xem WaitGroup có thể được sử dụng hiệu quả như thế nào trong các tình huống thực tế.

Xử lý dữ liệu song song

Trong các ứng dụng yêu cầu xử lý các tập dữ liệu lớn, việc phân phối khối lượng công việc trên nhiều goroutine có thể tăng tốc đáng kể hoạt động. Đây là cách bạn có thể sử dụng WaitGroup để đảm bảo tất cả dữ liệu được xử lý trước khi tiếp tục.

var wg sync.WaitGroup 
data := []int{1, 2, 3, 4, 5} 
for _, value := range data { 
	wg.Add(1) 
	go func(v int) { 
		defer wg.Done() // Simulate data processing
		fmt.Println("Processed:", v) 
	}(value) 
} 
wg.Wait() 
fmt.Println("All data processed!")

Chúng tôi lặp lại dữ liệu và khởi chạy một goroutine cho từng giá trị. 

Defer wg.Done() đảm bảo rằng bộ đếm WaitGroup sẽ giảm đi sau khi quá trình xử lý hoàn tất.

Rút trích nội dung trang web

WaitGroup cũng tiện dụng khi quét nhiều trang web cùng một lúc. Bằng cách khởi chạy một goroutine cho mỗi URL, bạn có thể tìm nạp dữ liệu song song, giúp thao tác nhanh hơn.

var wg sync.WaitGroup 
urls := []string{"http://example.com", "http://example.org", "http://example.net"} 
for _, url := range urls { 
	wg.Add(1) 
	go func(u string) { 
		defer wg.Done() // Simulate web scraping 
		content := fetchContent(u) 
		fmt.Println("Fetched content from:", u, "Content:", content) }(url) } 
wg.Wait() 
fmt.Println("All URLs scraped!")

Đối với mỗi URL, một goroutine được khởi chạy để tìm nạp nội dung của nó. 
Hàm FetchContent là một trình giữ chỗ cho logic quét web thực tế.

Batch File

Khi xử lý các thao tác trên tệp, chẳng hạn như đọc hoặc ghi nhiều tệp, WaitGroup có thể đảm bảo rằng tất cả các thao tác trên tệp được hoàn thành trước khi chương trình tiếp tục.

var wg sync.WaitGroup 
files := []string{"file1.txt", "file2.txt", "file3.txt"} 
for _, file := range files { 
	wg.Add(1) 
	go func(f string) { defer wg.Done() // Simulate file operation
		content := readFile(f) 
		fmt.Println("Read content from:", f, "Content:", content)(file) 
} 
wg.Wait() 
fmt.Println("All file operations completed!")

Mỗi tệp được đọc đồng thời bằng cách sử dụng một goroutine riêng. 

Hàm readFile là một trình giữ chỗ cho logic đọc tệp thực tế.

Bằng cách hiểu những ví dụ thực tế này, các nhà phát triển có thể khai thác sức mạnh của mô hình đồng thời của Golang và WaitGroup để xây dựng các ứng dụng hiệu quả và phản hồi nhanh. Dù xử lý dữ liệu, thu thập nội dung web hay xử lý tệp, WaitGroup đều đảm bảo tính đồng bộ và trật tự trong các hoạt động đồng thời.

Case study: Quét web hiệu quả với Golang WaitGroup John, một nhà phát triển, được giao nhiệm vụ thu thập dữ liệu từ nhiều trang web. Anh ấy đã chọn Golang vì khả năng xử lý đồng thời của nó. Tuy nhiên, anh phải đối mặt với một thách thức: đảm bảo tất cả các goroutine quét web được hoàn thành trước khi hợp nhất dữ liệu.

Giải pháp: John đã sử dụng WaitGroup của Golang để đồng bộ hóa các goroutine của mình. Đối với mỗi URL, anh ấy tăng bộ đếm WaitGroup và khởi chạy một goroutine. Sau khi dữ liệu được tìm nạp, anh ta giảm bộ đếm bằng Done(). Cuối cùng, anh ấy sử dụng Wait() để đảm bảo tất cả các goroutine được hoàn thành trước khi tiếp tục.

Ví dụ:

package main 
import ( 
	"fmt" 
	"net/http" 
	"sync" 
) 
func fetchURL(url string, wg *sync.WaitGroup) { 
	defer wg.Done() 
	_, err := http.Get(url) 
	if err != nil { 
		fmt.Println("Error fetching:", url, "Error:", err) 
		return 
	} 
	fmt.Println("Successfully fetched:", url)
} 

func main() { 
	var wg sync.WaitGroup 
	urls := []string{"http://example1.com", "http://example2.com", "http://example3.com"} 
	for _, url := range urls { 
		wg.Add(1) 
		go fetchURL(url, &wg) 
	} 
	wg.Wait() 
	fmt.Println("All URLs fetched!") 
}

Kết quả: Công việc quét web của John trở nên hiệu quả và đồng bộ. Anh đảm bảo rằng tất cả dữ liệu đều được tìm nạp trước khi xử lý, tránh nguy cơ xảy ra mâu thuẫn dữ liệu hoặc bỏ sót các điểm dữ liệu. Việc sử dụng WaitGroup khiến mã của anh ấy sạch hơn, dễ đọc hơn và đáng tin cậy hơn.

Tham khảo

  1. https://marketsplash.com/tutorials/go/golang-waitgroup/

Chủ đề

Bạn thấy bài này thế nào?