Concurrency là một khái niệm quan trọng trong lập trình hiện đại, cho phép các chương trình thực hiện nhiều tác vụ cùng một lúc. Trong Go, concurrency được hỗ trợ mạnh mẽ thông qua các goroutines và channels. Bài viết này sẽ cung cấp một cái nhìn tổng quan chi tiết về concurrency trong Go, bao gồm cú pháp, cách sử dụng, và các ví dụ minh họa cụ thể.
Khái Niệm Concurrency
Concurrency là khả năng của một chương trình để thực hiện nhiều tác vụ cùng một lúc. Điều này có nghĩa là chương trình có thể xử lý nhiều công việc đồng thời mà không cần phải chờ đợi một công việc hoàn thành trước khi bắt đầu công việc khác. Concurrency khác với parallelism ở chỗ concurrency là về việc quản lý nhiều tác vụ cùng một lúc, trong khi parallelism là về việc thực hiện nhiều tác vụ cùng một lúc.
Goroutines
Goroutines là các hàm hoặc phương thức chạy đồng thời với các hàm hoặc phương thức khác. Goroutines có thể được coi là các luồng nhẹ (lightweight threads). Chi phí để tạo một goroutine rất nhỏ so với một luồng, do đó, các ứng dụng Go thường có hàng ngàn goroutines chạy đồng thời.
Cú Pháp Tạo Goroutine
Để tạo một goroutine, bạn sử dụng từ khóa go
theo sau là lời gọi hàm.
Ví Dụ:
go
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
Trong ví dụ này, hàm say
được gọi như một goroutine với từ khóa go
, cho phép nó chạy đồng thời với hàm say
trong hàm main
.
Channels
Channels là các cấu trúc dữ liệu được sử dụng để giao tiếp giữa các goroutines. Channels cho phép một goroutine gửi dữ liệu đến một goroutine khác một cách an toàn và đồng bộ.
Cú Pháp Tạo Channel
Để tạo một channel, bạn sử dụng hàm make
với từ khóa chan
.
Ví Dụ:
go
package main
import "fmt"
func main() {
messages := make(chan string)
go func() { messages <- "ping" }()
msg := <-messages
fmt.Println(msg)
}
Trong ví dụ này, một channel messages
được tạo và sử dụng để gửi và nhận thông điệp giữa các goroutines.
Buffered Channels
Buffered channels cho phép bạn chỉ định số lượng giá trị có thể được lưu trữ trong channel trước khi nó bị chặn.
Ví Dụ:
go
package main
import "fmt"
func main() {
messages := make(chan string, 2)
messages <- "buffered"
messages <- "channel"
fmt.Println(<-messages)
fmt.Println(<-messages)
}
Trong ví dụ này, channel messages
có một bộ đệm với kích thước 2, cho phép lưu trữ hai giá trị trước khi nó bị chặn.
Select
Câu lệnh select
trong Go cho phép một goroutine chờ nhiều hoạt động trên nhiều channels. Câu lệnh select
sẽ chặn cho đến khi một trong các case có thể thực hiện được, sau đó nó sẽ thực hiện case đó.
Ví Dụ:
go
package main
import (
"fmt"
"time"
)
func main() {
c1 := make(chan string)
c2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
c1 <- "one"
}()
go func() {
time.Sleep(2 * time.Second)
c2 <- "two"
}()
for i := 0; i < 2; i++ {
select {
case msg1 := <-c1:
fmt.Println("received", msg1)
case msg2 := <-c2:
fmt.Println("received", msg2)
}
}
}
Trong ví dụ này, câu lệnh select
chờ cho đến khi một trong các channel c1
hoặc c2
nhận được một giá trị và sau đó thực hiện case tương ứng.
WaitGroup
WaitGroup
là một công cụ đồng bộ hóa được sử dụng để chờ cho một tập hợp các goroutines hoàn thành. WaitGroup
cung cấp ba phương thức chính: Add
, Done
, và Wait
.
Ví Dụ:
go
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait()
}
Trong ví dụ này, WaitGroup
được sử dụng để chờ cho tất cả các goroutines hoàn thành trước khi chương trình kết thúc.
Mutex
Mutex (mutual exclusion) là một công cụ đồng bộ hóa được sử dụng để bảo vệ truy cập vào các tài nguyên chia sẻ giữa các goroutines. Go cung cấp sync.Mutex
để thực hiện điều này.
Ví Dụ:
go
package main
import (
"fmt"
"sync"
)
type SafeCounter struct {
v map[string]int
mux sync.Mutex
}
func (c *SafeCounter) Inc(key string) {
c.mux.Lock()
c.v[key]++
c.mux.Unlock()
}
func (c *SafeCounter) Value(key string) int {
c.mux.Lock()
defer c.mux.Unlock()
return c.v[key]
}
func main() {
c := SafeCounter{v: make(map[string]int)}
for i := 0; i < 1000; i++ {
go c.Inc("somekey")
}
fmt.Println(c.Value("somekey"))
}
Trong ví dụ này, sync.Mutex
được sử dụng để bảo vệ truy cập vào bản đồ v
trong SafeCounter
.
Thực Tiễn Tốt Nhất
- Sử Dụng Goroutines và Channels Đúng Cách: Goroutines và channels là các công cụ mạnh mẽ, nhưng chúng cần được sử dụng đúng cách để tránh các vấn đề như deadlock và race conditions.
- Sử Dụng
select
để Chờ Nhiều Channels:select
là một công cụ mạnh mẽ để chờ nhiều channels và nên được sử dụng khi cần. - Sử Dụng
WaitGroup
để Đồng Bộ Hóa Goroutines:WaitGroup
là một công cụ hữu ích để chờ cho một tập hợp các goroutines hoàn thành. - Bảo Vệ Tài Nguyên Chia Sẻ với Mutex: Khi làm việc với các tài nguyên chia sẻ, hãy sử dụng
sync.Mutex
để bảo vệ truy cập vào các tài nguyên đó.
Kết Luận
Concurrency là một khái niệm quan trọng trong Go, giúp các chương trình thực hiện nhiều tác vụ cùng một lúc một cách hiệu quả. Bài viết này đã cung cấp một cái nhìn tổng quan chi tiết về concurrency trong Go, bao gồm cú pháp, cách sử dụng, và các ví dụ minh họa cụ thể. Hiểu rõ về concurrency sẽ giúp bạn viết mã Go hiệu quả và dễ bảo trì hơn.