Golang 推荐通过 channel 进行通信和同步,但在实际开发中 sync 包用得也非常的多,在以太坊的源码中也有很多这类应用的体现。
Go sync包提供了:sync.Mutex,sync.RMutex,sync.Once,sync.Cond,sync.Waitgroup,sync.atomic等,文本主要对sync.Mutex,sync.RMutex和sync.Waitgroup的使用进行了说明,后续会推出其它方法的使用说明。
sync包含一个 Locker interface:
type Locker interface {
Lock()
Unlock()
}
该接口只有两个方法,Lock() 和 Unlock()。整个sync包都是围绕该接口实现。
互斥锁Mutex
互斥锁Mutex是Locker的一种具体实现,有两个方法:
func (m *Mutex) Lock()
func (m *Mutex) Unlock()
一个互斥锁只能同时被一个 goroutine 锁定,其它 goroutine 将阻塞直到互斥锁被解锁,之后再重新争抢对互斥锁的锁定。
对一个未锁定的互斥锁解锁将会产生运行时错误。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
ch := make(chan struct{}, 2)
var l sync.Mutex
go func() {
l.Lock()
defer l.Unlock()
fmt.Println("goroutine1: 锁定大概 2s")
time.Sleep(time.Second * 2)
fmt.Println("goroutine1: 解锁了,可以开抢了!")
ch <- struct{}{}
}()
go func() {
fmt.Println("groutine2: 等待解锁")
l.Lock()
defer l.Unlock()
fmt.Println("goroutine2: 我锁定了")
ch <- struct{}{}
}()
// 等待 goroutine 执行结束
for i := 0; i < 2; i++ {
<-ch
}
}
读写锁 RWMutex
读写锁是针对读写操作的互斥锁,读写锁与互斥锁最大的不同就是可以分别对 读、写 进行锁定。一般用在大量读操作、少量写操作的情况:
func (rw *RWMutex) Lock()
func (rw *RWMutex) Unlock()
func (rw *RWMutex) RLock()
func (rw *RWMutex) RUnlock()
由于这里需要区分读写锁定,读写锁这样定义:
读锁定(RLock),对读操作进行锁定
读解锁(RUnlock),对读锁定进行解锁
写锁定(Lock),对写操作进行锁定
写解锁(Unlock),对写锁定进行解锁
不要混用锁定和解锁,如:Lock 和 RUnlock、RLock 和 Unlock。因为对未读锁定的读写锁进行读解锁或对未写锁定的读写锁进行写解锁将会引起运行时错误。
如何理解读写锁呢?
同时只能有一个 goroutine 能够获得写锁定。
同时可以有任意多个 gorouinte 获得读锁定。
同时只能存在写锁定或读锁定(读和写互斥)。
也就是说,当有一个 goroutine 获得写锁定,其它无论是读锁定还是写锁定都将阻塞直到写解锁;当有一个 goroutine 获得读锁定,其它读锁定仍然可以继续;当有一个或任意多个读锁定,写锁定将等待所有读锁定解锁之后才能够进行写锁定。所以说这里的读锁定(RLock)目的其实是告诉写锁定:有很多人正在读取数据,需等它们读(读解锁)完再来写(写锁定)。
package main
import (
"fmt"
"math/rand"
"sync"
)
var count int
var rw sync.RWMutex
func main() {
ch := make(chan struct{}, 10)
for i := 0; i < 5; i++ {
go read(i, ch)
}
for i := 0; i < 5; i++ {
go write(i, ch)
}
for i := 0; i < 10; i++ {
<-ch
}
}
func read(n int, ch chan struct{}) {
rw.RLock()
fmt.Printf("goroutine %d 进入读操作...\n", n)
v := count
fmt.Printf("goroutine %d 读取结束,值为:%d\n", n, v)
rw.RUnlock()
ch <- struct{}{}
}
func write(n int, ch chan struct{}) {
rw.Lock()
fmt.Printf("goroutine %d 进入写操作...\n", n)
v := rand.Intn(1000)
count = v
fmt.Printf("goroutine %d 写入结束,新值为:%d\n", n, v)
rw.Unlock()
ch <- struct{}{}
}
WaitGroup
WaitGroup 用于等待一组 goroutine 结束,用法比较简单。它有三个方法:
func (wg *WaitGroup) Add(delta int)
func (wg *WaitGroup) Done()
func (wg *WaitGroup) Wait()
Add 用来添加 goroutine 的个数。Done 执行一次数量减 1。Wait 用来等待结束:
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
for i, s := range seconds {
// 计数加 1
wg.Add(1)
go func(i, s int) {
// 计数减 1
defer wg.Done()
fmt.Printf("goroutine%d 结束\n", i)
}(i, s)
}
// 等待执行结束
wg.Wait()
fmt.Println("所有 goroutine 执行结束")
}
注意,wg.Add() 方法一定要在 goroutine 开始前执行。