目录
Day 6: 高并发掌控 - WaitGroup/Mutex高级同步模式
Go语言的并发编程不仅仅局限于基础的Goroutine和Channel,随着并发任务复杂度的增加,开发者常常需要处理更为复杂的同步与协调机制。本篇博客将深入探讨Go中的并发高级模式,重点介绍WaitGroup
与Mutex
同步、Context
机制(超时控制与取消)以及原子操作(sync/atomic
)。最后,我们将通过一个实际的例子——并发爬虫框架的雏形,来展示如何应用这些高级并发模式。
1. WaitGroup与Mutex同步
1.1 WaitGroup的使用
sync.WaitGroup
用于等待一组Goroutine完成,它提供了一个简单的机制来协调多个Goroutine的同步。在并发编程中,常常需要等待多个并发任务执行完毕,WaitGroup
正是为了解决这一问题。
示例代码:
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // 表示任务完成时,调用Done
fmt.Printf("Worker %d starting\n", id)
time.Sleep(2 * time.Second) // 模拟任务执行
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
// 启动3个工作Goroutine
for i := 1; i <= 3; i++ {
wg.Add(1) // 为每个Goroutine增加一个计数
go worker(i, &wg)
}
// 等待所有工作Goroutine完成
wg.Wait()
fmt.Println("All workers are done")
}
结果:
Worker 1 starting
Worker 2 starting
Worker 3 starting
Worker 1 done
Worker 2 done
Worker 3 done
All workers are done
在这个例子中,WaitGroup
的Add(1)
方法在每个Goroutine启动时调用,而Done()
方法在每个Goroutine完成时调用。Wait()
方法会阻塞主Goroutine,直到所有Goroutine完成。
1.2 Mutex的使用
sync.Mutex
是一种用于保护共享资源的同步原语。它允许多个Goroutine并发访问共享资源,但在同一时刻只有一个Goroutine能够持有锁。
示例代码:
package main
import (
"fmt"
"sync"
)
var counter int
var mutex sync.Mutex
func increment(wg *sync.WaitGroup) {
defer wg.Done()
mutex.Lock() // 获取锁
counter++
mutex.Unlock() // 释放锁
}
func main() {
var wg sync.WaitGroup
// 启动100个并发Goroutine
for i := 0; i < 100; i++ {
wg.Add(1)
go increment(&wg)
}
wg.Wait()
fmt.Println("Final counter:", counter)
}
结果:
Final counter: 100
在这个例子中,Mutex
确保了只有一个Goroutine能够同时修改counter
变量。即使有100个并发任务,Mutex
也保证了对counter
的访问是安全的。
2. Context机制(超时控制与取消)
2.1 Context的概述
Context
是Go语言中用来管理并发任务的超时、取消信号以及其他请求范围内的共享值的机制。在复杂的并发程序中,特别是在网络请求和数据库查询等操作中,Context
可以帮助我们控制任务的超时和取消。
示例代码:
package main
import (
"context"
"fmt"
"time"
)
func longRunningTask(ctx context.Context) {
select {
case <-time.After(5 * time.Second):
fmt.Println("Task completed")
case <-ctx.Done():
fmt.Println("Task cancelled:", ctx.Err())
}
}
func main() {
// 创建一个带超时的Context
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
// 启动一个长时间运行的任务
go longRunningTask(ctx)
// 等待任务完成或超时
time.Sleep(4 * time.Second)
}
结果:
Task cancelled: context deadline exceeded
在这个例子中,WithTimeout
方法为Context
设置了一个3秒的超时。由于longRunningTask
需要5秒钟才能完成,因此会被Context
的超时机制取消,并输出“Task cancelled: context deadline exceeded”。
2.2 使用Context
取消任务
Context
还可以通过调用cancel()
来显式取消任务。例如:
package main
import (
"context"
"fmt"
"time"
)
func taskWithCancel(ctx context.Context) {
select {
case <-time.After(5 * time.Second):
fmt.Println("Task completed")
case <-ctx.Done():
fmt.Println("Task cancelled:", ctx.Err())
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
// 启动任务
go taskWithCancel(ctx)
// 模拟取消操作
time.Sleep(2 * time.Second)
cancel()
// 等待任务取消
time.Sleep(1 * time.Second)
}
结果:
Task cancelled: context canceled
在这个例子中,cancel()
被调用后,taskWithCancel
任务会被取消,输出Task cancelled: context canceled
。
3. 原子操作(sync/atomic)
sync/atomic
提供了一些原子操作,这些操作在并发环境下是安全的,能够避免竞态条件。sync/atomic
主要用于对简单类型(如int32
、int64
)进行原子加法、比较和交换等操作。
3.1 原子加法操作
package main
import (
"fmt"
"sync/atomic"
"time"
)
func main() {
var counter int32
// 启动100个并发Goroutine,进行原子加法操作
for i := 0; i < 100; i++ {
go func() {
atomic.AddInt32(&counter, 1)
}()
}
// 等待足够的时间,让所有Goroutine执行完
time.Sleep(1 * time.Second)
// 打印结果
fmt.Println("Final counter:", counter)
}
结果:
Final counter: 100
atomic.AddInt32
确保了对counter
的每次加法操作是原子的,避免了竞态条件。
4. 实战代码:并发爬虫框架雏形
在本节中,我们将应用上述并发高级模式来实现一个简单的并发爬虫框架。爬虫会从多个网站并发地获取页面内容,并输出结果。
4.1 爬虫框架设计
爬虫的任务是并发获取多个URL的内容。我们会使用WaitGroup
来等待所有任务完成,Mutex
来同步输出结果,Context
来控制超时或取消操作。
示例代码:
package main
import (
"context"
"fmt"
"net/http"
"sync"
"time"
)
var mutex sync.Mutex
func fetchURL(ctx context.Context, url string, wg *sync.WaitGroup) {
defer wg.Done()
// 设置超时
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
// 发送请求
resp, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Println("Error fetching URL:", err)
return
}
defer resp.Body.Close()
// 同步输出
mutex.Lock()
fmt.Printf("Fetched URL: %s, Status: %s\n", url, resp.Status)
mutex.Unlock()
}
func main() {
urls := []string{
"https://www.example.com",
"https://www.google.com",
"https://www.github.com",
}
// 创建带超时的Context
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
var wg sync.WaitGroup
// 启动爬虫任务
for _, url := range urls {
wg.Add(1)
go fetchURL(ctx, url, &wg)
}
// 等待所有任务完成
wg.Wait()
}
结果:
Fetched URL: https://www.example.com, Status: 200 OK
Fetched URL: https://www.google.com, Status: 200 OK
Fetched URL: https://www.github.com, Status: 200 OK
在这个爬虫框架中,WaitGroup
确保我们等待所有URL的请求完成,Mutex
保证多个Goroutine对fmt
的并发访问不会发生竞态,Context
实现了超时控制。
结语
通过本篇博客,我们学习了Go语言中的并发高级模式,包括WaitGroup
、Mutex
、Context
和原子操作的使用。通过结合这些高级并发模式,我们实现了一个简单的并发爬虫框架,展示了如何在实际应用中处理并发任务的同步、超时控制和取消。掌握这些技巧后,你可以在实际项目中更高效地处理并发问题,编写出更稳定、高效的并发代码。