引言
并发是 Go 语言最强大的特性之一。Go 的并发模型基于 CSP(Communicating Sequential Processes),核心理念是"不要通过共享内存来通信,而要通过通信来共享内存"。本文从 Goroutine 和 Channel 的基础出发,逐步深入到锁、Context 和 Worker Pool 等高级模式。
一、Goroutine:轻量级线程
Goroutine 是 Go 运行时管理的轻量级线程,初始栈大小只有 2KB(相比之下,操作系统线程通常为 1MB),这使得你可以在单机上轻松运行数十万个 Goroutine。
package main
import (
"fmt"
"time"
)
func sayHello(name string) {
for i := 0; i < 3; i++ {
fmt.Printf("Hello, %s!\n", name)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
go sayHello("Alice") // 启动一个 Goroutine
go sayHello("Bob") // 启动另一个 Goroutine
time.Sleep(1 * time.Second) // 等待 Goroutine 执行完毕
fmt.Println("Done")
}
二、Channel:Goroutine 间的通信
Channel 是 Go 并发模型的核心,用于在 Goroutine 之间传递数据。
2.1 无缓冲 Channel
ch := make(chan int) // 无缓冲 Channel
go func() {
ch <- 42 // 发送:阻塞直到有接收者
}()
value := <-ch // 接收:阻塞直到有发送者
fmt.Println(value) // 42
2.2 有缓冲 Channel
ch := make(chan int, 3) // 缓冲大小为 3
ch <- 1 // 不阻塞
ch <- 2 // 不阻塞
ch <- 3 // 不阻塞,缓冲区已满
// ch <- 4 // 阻塞!缓冲区已满,等待接收者
fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2
fmt.Println(<-ch) // 3
2.3 Select 多路复用
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "from ch1"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "from ch2"
}()
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println("Received:", msg1)
case msg2 := <-ch2:
fmt.Println("Received:", msg2)
case <-time.After(3 * time.Second):
fmt.Println("Timeout!")
return
}
}
}
三、同步原语
3.1 sync.Mutex 和 sync.RWMutex
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
// RWMutex:读多写少场景
type SafeMap struct {
mu sync.RWMutex
data map[string]string
}
func (m *SafeMap) Get(key string) string {
m.mu.RLock() // 读锁,允许多个 Goroutine 同时持有
defer m.mu.RUnlock()
return m.data[key]
}
func (m *SafeMap) Set(key, value string) {
m.mu.Lock() // 写锁,独占
defer m.mu.Unlock()
m.data[key] = value
}
3.2 sync.WaitGroup
func main() {
var wg sync.WaitGroup
urls := []string{"https://golang.org", "https://google.com"}
for _, url := range urls {
wg.Add(1) // 增加计数
go func(u string) {
defer wg.Done() // 完成时减 1
resp, err := http.Get(u)
if err != nil {
fmt.Printf("Error fetching %s: %v\n", u, err)
return
}
fmt.Printf("%s: %d\n", u, resp.StatusCode)
resp.Body.Close()
}(url)
}
wg.Wait() // 等待所有 Goroutine 完成
fmt.Println("All done")
}
四、context.Context:取消与超时
func worker(ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d: cancelled\n", id)
return
default:
fmt.Printf("Worker %d: working...\n", id)
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
// 带超时的 Context
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
for i := 1; i <= 3; i++ {
go worker(ctx, i)
}
<-ctx.Done() // 等待 2 秒超时
fmt.Println("Main: timeout reached")
}
五、Worker Pool 模式
func workerPool(numWorkers int, jobs <-chan int, results chan<- int) {
var wg sync.WaitGroup
for w := 1; w <= numWorkers; w++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for job := range jobs {
fmt.Printf("Worker %d processing job %d\n", id, job)
results <- job * 2 // 模拟处理
}
}(w)
}
go func() {
wg.Wait()
close(results)
}()
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
workerPool(3, jobs, results)
for j := 1; j <= 10; j++ {
jobs <- j
}
close(jobs)
for r := range results {
fmt.Println("Result:", r)
}
}
六、Race Detector
Go 内置了数据竞争检测器,在测试或开发环境中使用 -race 标志即可:
# 运行测试并检测数据竞争
go test -race ./...
# 运行程序并检测数据竞争
go run -race main.go
# 构建时启用竞争检测
go build -race -o myapp
总结
Go 的并发编程有两条路径:当需要共享状态时,使用 sync.Mutex 保护;当需要传递数据时,使用 Channel 通信。记住 Go 的并发格言:"Don't communicate by sharing memory; share memory by communicating." 在实际开发中,优先使用 Channel 和 Context 构建清晰的并发模式,只在必要时才使用 Mutex。最后,始终在测试中启用 -race 标志,数据竞争是并发编程中最隐蔽的 bug。
返回文章列表
标签:Go并发编程Goroutine