Go语言的并发编程,包括Mutex、RWMutex、WaitGroup和Channel等机制
在传统的编程语言中,如C++、Java、Python等,并发逻辑多建立在操作系统线程之上。线程间的通信通常依赖于操作系统提供的基础原语,包括共享内存、信号、管道、消息队列及套接字等,其中共享内存是最为普遍的通信方式。但这种基于共享内存的并发模型在复杂或大规模业务场景下往往显得复杂且易于出错。
Go语言在设计时即以解决传统并发问题为目标,融入了CSP(Communicating Sequential Processes,通信顺序进程)模型的理念。CSP模型致力于简化并发编程,目标是让编写并发程序的难度与顺序程序相当。
在CSP模型中,通信和同步通过一种特定的流程实现:生产者产生数据,然后通过输出数据到输入/输出原语,最终到达消费者。Go语言为实现CSP模型,特别引入了Channel机制。Goroutine可以通过Channel进行数据的读写操作,Channel作为连接多个Goroutine的通信桥梁,简化了并发编程的复杂性。
虽然CSP模型在Go语言中占据主流地位,但Go同样支持基于共享内存的并发模型。Go的sync
包中,提供了包括互斥锁、读写锁、条件变量和原子操作等多种同步机制,以满足不同并发场景下的需求。
互斥锁(Mutex)
基本概念
互斥锁(Mutex)用于在并发环境中安全访问共享资源。当一个协程获取到锁时,它将拥有临界区的访问权,其他请求该锁的协程将会阻塞,直到锁被释放。
应用场景
并发访问共享资源的情形非常普遍,例如:
- 秒杀系统
- 多个Goroutine并发修改某个变量
- 同时更新用户信息
如果没有互斥锁的控制,将会导致商品超卖、变量数值不正确、用户信息更新错误等问题。
基本用法
Mutex
实现了Locker
接口,提供了两个方法:Lock
和Unlock
。Lock
用于对临界区上锁,Unlock
用于释放锁。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var mu sync.Mutex
var count int
increment := func() {
mu.Lock()
defer mu.Unlock()
count++
fmt.Println("Count:", count)
}
for i := 0; i < 5; i++ {
go increment()
}
time.Sleep(time.Second)
}
易错场景
- 不可重入锁:Go的
Mutex
是不可重入锁,持有锁的协程不能再次获取锁,否则会发生死锁。 - Lock和Unlock不配对:未正确配对的调用会导致死锁。
- 复制已使用的锁:复制已使用的锁会导致意外行为。
Mutex的状态和模式
Mutex
有四种状态:mutexLocked
(锁定)、mutexWoken
(唤醒)、mutexStarving
(饥饿模式)、waiterCount
(等待者数量)。在正常模式下,新请求的Goroutine与被唤醒的Goroutine竞争锁,而饥饿模式则优先唤醒等待队列中的Goroutine。
读写锁(RWMutex)
基本概念
RWMutex
是一种读写锁,同一时间只能被一个写操作持有,或者被多个读操作持有。在读多写少的场景下,它比Mutex
更高效。
基本用法
RWMutex
提供了Lock
(写锁)、Unlock
(释放写锁)、RLock
(读锁)和RUnlock
(释放读锁)等方法。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var rw sync.RWMutex
var count int
write := func() {
rw.Lock()
defer rw.Unlock()
count++
fmt.Println("Write:", count)
}
read := func() {
rw.RLock()
defer rw.RUnlock()
fmt.Println("Read:", count)
}
for i := 0; i < 5; i++ {
go read()
}
go write()
time.Sleep(time.Second)
}
易错场景
与Mutex
类似,RWMutex
也是不可重入锁,未配对的Lock
和Unlock
调用也会导致死锁。
WaitGroup
基本概念
WaitGroup
用于等待一组Goroutine完成。通过Add
方法增加计数,Done
方法减少计数,Wait
方法阻塞等待计数归零。
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()
fmt.Println("All workers done")
}
Channel
基本概念
Go通过Channel
实现Goroutine之间的通信。Channel分为无缓冲和有缓冲两种。close
关闭Channel,发送和接收都可以作为select
语句的case
。
应用场景
Channel常用于数据传递、并发编排和实现信号通知等。
package main
import (
"fmt"
"time"
)
func producer(ch chan<- int, count int) {
for i := 0; i < count; i++ {
ch <- i
fmt.Println("Produced:", i)
time.Sleep(time.Millisecond * 500)
}
close(ch)
}
func consumer(ch <-chan int) {
for data := range ch {
fmt.Println("Consumed:", data)
time.Sleep(time.Millisecond * 1000)
}
}
func main() {
ch := make(chan int, 5)
go producer(ch, 10)
consumer(ch)
}
小结
Go语言通过Mutex、RWMutex、WaitGroup和Channel等工具,为并发编程提供了强大的支持。每种机制都有其独特的应用场景和注意事项,合理使用这些工具可以帮助开发者编写更高效、安全的并发程序。