在开始深入探讨Go语言的并发模式之前,我们先快速回顾一下Go语言并发编程的基础。Go语言的并发模型基于CSP(Communicating Sequential Processes)理论,其核心是goroutine和channel。
goroutine是Go语言中的轻量级线程,由Go运行时管理。创建一个goroutine非常简单,只需要在函数调用前加上go关键字:
go复制go func() {
// 并发执行的代码
}()
channel则是goroutine之间通信的主要方式,它提供了类型安全的消息传递机制:
go复制ch := make(chan int) // 创建一个int类型的channel
go func() { ch <- 42 }() // 在一个goroutine中发送数据
value := <-ch // 在主goroutine中接收数据
这种基于消息传递而非共享内存的并发模型,使得Go程序更容易编写正确的并发代码。但要想真正发挥Go并发的威力,我们需要掌握一些高级的并发模式。
生产者-消费者模式是最基础的并发模式之一,它解耦了数据的生产和消费过程。在Go中,我们可以用channel优雅地实现这一模式:
go复制func producer(ch chan<- int) {
for i := 0; i < 10; i++ {
ch <- i
time.Sleep(time.Millisecond * 100)
}
close(ch)
}
func consumer(ch <-chan int) {
for num := range ch {
fmt.Println("Received:", num)
}
}
func main() {
ch := make(chan int)
go producer(ch)
consumer(ch)
}
默认情况下,channel是无缓冲的,这意味着发送和接收操作会阻塞,直到另一方准备好。我们可以使用带缓冲的channel来提高性能:
go复制ch := make(chan int, 100) // 缓冲大小为100
带缓冲的channel允许发送者在channel满之前不阻塞地发送多个值,这在生产者比消费者快的情况下特别有用。
注意:缓冲大小需要根据实际情况调整。过大的缓冲可能掩盖性能问题,过小的缓冲可能导致不必要的阻塞。
在实际应用中,我们经常需要处理多个生产者和多个消费者的情况:
go复制func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Println("worker", id, "processing job", j)
time.Sleep(time.Second)
results <- j * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
// 启动3个worker
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// 发送9个任务
for j := 1; j <= 9; j++ {
jobs <- j
}
close(jobs)
// 收集结果
for a := 1; a <= 9; a++ {
<-results
}
}
Worker Pool(工作池)模式是一种常见的并发模式,它维护一组固定数量的worker goroutine来处理任务。这种模式特别适合需要限制并发量的场景,比如数据库连接池。
下面是一个完整的Worker Pool实现:
go复制type Job struct {
ID int
Data interface{}
}
type Result struct {
Job Job
Err error
Output interface{}
}
func worker(id int, jobs <-chan Job, results chan<- Result) {
for job := range jobs {
// 模拟处理任务
time.Sleep(time.Second)
results <- Result{
Job: job,
Err: nil,
Output: fmt.Sprintf("worker %d processed job %d", id, job.ID),
}
}
}
func createWorkerPool(numWorkers int, jobs <-chan Job, results chan<- Result) {
for i := 0; i < numWorkers; i++ {
go worker(i, jobs, results)
}
}
func main() {
jobs := make(chan Job, 100)
results := make(chan Result, 100)
// 创建worker pool
createWorkerPool(5, jobs, results)
// 发送任务
go func() {
for i := 0; i < 20; i++ {
jobs <- Job{ID: i, Data: fmt.Sprintf("data-%d", i)}
}
close(jobs)
}()
// 收集结果
for i := 0; i < 20; i++ {
result := <-results
fmt.Println(result.Output)
}
}
在实际使用Worker Pool时,有几个关键参数需要考虑:
提示:可以使用runtime.NumCPU()获取机器的CPU核心数,作为worker数量的参考。
Fan-out/Fan-in是一种强大的并发模式,它包含两个阶段:
go复制func producer(nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
out <- n
}
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
out <- n * n
}
}()
return out
}
func merge(cs ...<-chan int) <-chan int {
var wg sync.WaitGroup
out := make(chan int)
// 为每个输入channel启动一个goroutine
output := func(c <-chan int) {
defer wg.Done()
for n := range c {
out <- n
}
}
wg.Add(len(cs))
for _, c := range cs {
go output(c)
}
// 等待所有goroutine完成
go func() {
wg.Wait()
close(out)
}()
return out
}
func main() {
in := producer(1, 2, 3, 4, 5)
// Fan-out
c1 := square(in)
c2 := square(in)
// Fan-in
for n := range merge(c1, c2) {
fmt.Println(n) // 1, 4, 9, 16, 25
}
}
Fan-out/Fan-in模式特别适合以下场景:
sync.WaitGroup是Go中常用的同步机制,它可以等待一组goroutine完成:
go复制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 <= 5; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait()
}
context包提供了更强大的goroutine控制能力,特别是取消和超时:
go复制func worker(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Println(name, "stopped")
return
default:
fmt.Println(name, "working")
time.Sleep(time.Second)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go worker(ctx, "worker1")
go worker(ctx, "worker2")
time.Sleep(3 * time.Second)
cancel()
time.Sleep(time.Second)
}
有时我们需要限制同时运行的goroutine数量,可以使用带缓冲的channel作为信号量:
go复制func worker(id int, sem chan struct{}) {
defer func() { <-sem }()
fmt.Println("worker", id, "started")
time.Sleep(time.Second)
fmt.Println("worker", id, "finished")
}
func main() {
const maxConcurrent = 3
sem := make(chan struct{}, maxConcurrent)
for i := 0; i < 10; i++ {
sem <- struct{}{}
go worker(i, sem)
}
// 等待所有goroutine完成
for i := 0; i < maxConcurrent; i++ {
sem <- struct{}{}
}
}
在并发程序中,错误处理尤为重要。一种常见的模式是将错误与结果一起返回:
go复制type Result struct {
Value int
Err error
}
func compute(value int) Result {
if value < 0 {
return Result{Err: fmt.Errorf("invalid value: %d", value)}
}
return Result{Value: value * 2}
}
func main() {
values := []int{1, 2, -3, 4}
results := make(chan Result, len(values))
for _, v := range values {
go func(v int) {
results <- compute(v)
}(v)
}
for range values {
r := <-results
if r.Err != nil {
fmt.Println("Error:", r.Err)
continue
}
fmt.Println("Result:", r.Value)
}
}
当有多个goroutine可能产生错误时,我们可以聚合这些错误:
go复制func worker(id int, errChan chan<- error) {
defer close(errChan)
if id%2 == 0 {
errChan <- fmt.Errorf("worker %d failed", id)
return
}
fmt.Printf("worker %d succeeded\n", id)
}
func main() {
const numWorkers = 5
errChans := make([]chan error, numWorkers)
for i := 0; i < numWorkers; i++ {
errChans[i] = make(chan error, 1)
go worker(i, errChans[i])
}
var errors []error
for _, ch := range errChans {
if err := <-ch; err != nil {
errors = append(errors, err)
}
}
if len(errors) > 0 {
fmt.Println("Errors occurred:")
for _, err := range errors {
fmt.Println("-", err)
}
}
}
Pipeline模式将复杂的处理过程分解为多个阶段,每个阶段由独立的goroutine处理:
go复制func gen(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
func sq(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
func print(in <-chan int) {
for n := range in {
fmt.Println(n)
}
}
func main() {
// 设置pipeline: gen -> sq -> print
print(sq(gen(1, 2, 3, 4)))
}
在并发编程中,处理超时是非常重要的:
go复制func slowOperation() (int, error) {
time.Sleep(2 * time.Second)
return 42, nil
}
func main() {
result := make(chan int, 1)
errChan := make(chan error, 1)
go func() {
res, err := slowOperation()
if err != nil {
errChan <- err
return
}
result <- res
}()
select {
case res := <-result:
fmt.Println("Result:", res)
case err := <-errChan:
fmt.Println("Error:", err)
case <-time.After(1 * time.Second):
fmt.Println("Timeout!")
}
}
有时我们需要限制处理速率,可以使用time.Ticker来实现:
go复制func process(item int) {
fmt.Println("Processing", item)
time.Sleep(100 * time.Millisecond)
}
func main() {
requests := make(chan int, 100)
for i := 0; i < 100; i++ {
requests <- i
}
close(requests)
limiter := time.Tick(200 * time.Millisecond)
for req := range requests {
<-limiter
go process(req)
}
time.Sleep(5 * time.Second)
}
goroutine泄漏是常见的并发编程问题。确保每个启动的goroutine都有明确的退出条件:
go复制func worker(stop <-chan struct{}) {
for {
select {
case <-stop:
fmt.Println("worker stopped")
return
default:
fmt.Println("working...")
time.Sleep(time.Second)
}
}
}
func main() {
stop := make(chan struct{})
go worker(stop)
time.Sleep(3 * time.Second)
close(stop) // 发送停止信号
time.Sleep(time.Second)
}
调试并发程序的一些实用技巧:
go复制func worker(id int) {
log.Printf("[worker %d] starting\n", id)
time.Sleep(time.Second)
log.Printf("[worker %d] done\n", id)
}
func main() {
for i := 0; i < 3; i++ {
go worker(i)
}
time.Sleep(2 * time.Second)
}
在实际项目中,如何选择合适的并发模式?这里有一些指导原则:
选择模式时需要考虑:
Go的race detector可以帮助识别数据竞争:
bash复制go run -race main.go
Go提供了多种同步原语:
go复制type SafeCounter struct {
mu sync.Mutex
v map[string]int
}
func (c *SafeCounter) Inc(key string) {
c.mu.Lock()
defer c.mu.Unlock()
c.v[key]++
}
func (c *SafeCounter) Value(key string) int {
c.mu.Lock()
defer c.mu.Unlock()
return c.v[key]
}
func main() {
c := SafeCounter{v: make(map[string]int)}
for i := 0; i < 100; i++ {
go c.Inc("somekey")
}
time.Sleep(time.Second)
fmt.Println(c.Value("somekey"))
}
在实际开发中,我发现理解这些并发模式只是第一步,真正的挑战在于根据具体场景选择合适的模式组合。比如在一个网络服务中,可能会同时使用Worker Pool处理请求、Pipeline处理数据流、Fan-out/Fan-in聚合结果。关键是要理解每种模式的适用场景和限制,而不是生搬硬套。