一文看懂golang的sync包
sync包里包含的struct以及其功能
- sync.Mutex: 互斥量(锁),主要是处理多个goroutine竞争同一个资源时候的同步问题。
- sync.RWMutex: 读写互斥量(锁),相对Mutex而言能进行更细腻的控制,主要用在读多写少的情景下。
- sync.WaitGroup: WaitGroup用于等待一组goroutine结束。
- sync.Cond:实现一个条件变量,即等待或宣布事件发生的goroutines的会合点。
- sync.Pool:临时对象池,作为临时对象的保存和复用的集合。
- sync.Once:顾名思义,Once可以使得函数的调用只执行一次。
具体解释和示例
- Mutex 主要处理多个goroutine竞争同一个资源时的同步问题,能保证同时只有一个goroutine在使用该资源,而其他的goroutine则在等待,直到占用资源的goroutine释放了Mutex,即是调用了mutex.Unlock()
示例:
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: lock 2 seconds")
time.Sleep(time.Second * 2)
fmt.Println("goroutine1: unlocked")
ch <- struct{}{}
}()
go func() {
fmt.Println("goroutine2: wait for unlock")
l.Lock()
defer l.Unlock()
fmt.Println("goroutine2: lock 2 seconds")
ch <- struct{}{}
}()
for i:=0;i<2;i++ {
<- ch
}
}
执行结果
$ go run main.go
goroutine2: wait for unlock
goroutine1: lock 2 seconds
goroutine1: unlocked
goroutine2: lock 2 seconds
从结果可以看到goroutine2在调用l.Lock()时阻塞了,一直等待goroutine1,直到在goroutine1里调用了l.Unlock()。
- RWMutex 则可以对锁进行更精细的控制,主要的规则如下:
- 读锁之间是不互斥的
- 读锁和写锁互斥
- 写锁和写锁互斥
也就意味着同时只有一个goroutine能获得写锁定,当它获得了写锁定了,其他的goroutine,无论是写锁定还是读锁定,都无法获得。而可以同时有一个或多个goroutine获得读锁定,这时其他的goroutine无法获得写锁定。
示例如下:
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
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++ {
<- ch
}
}
func read(i int, ch chan struct{}) {
rw.RLock()
fmt.Printf("goroutine %d 进入读操作\n", i)
v := count
time.Sleep(time.Second * 1)
fmt.Printf("goroutine %d 读取结束,值为:%d\n", i, v)
rw.RUnlock()
ch <- struct{}{}
}
示例输出为:
goroutine 4 进入读操作
goroutine 1 进入读操作
goroutine 0 进入读操作
goroutine 2 进入读操作
goroutine 3 进入读操作
goroutine 1 读取结束,值为:0
goroutine 4 读取结束,值为:0
goroutine 3 读取结束,值为:0
goroutine 2 读取结束,值为:0
goroutine 0 读取结束,值为:0
可以看到5个goroutine可以同时进入读操作,而不会有任何的goroutine会阻塞。
对上面的例子稍作调整:
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
var count int
var rw sync.RWMutex
func main() {
ch := make(chan struct{}, 10)
for i:=0;i<5;i++ {
go read(i, ch)
}
go write(ch)
for i:=0;i<6;i++ {
<- ch
}
}
func read(i int, ch chan struct{}) {
rw.RLock()
fmt.Printf("goroutine %d 进入读操作\n", i)
v := count
time.Sleep(time.Second * 1)
fmt.Printf("goroutine %d 读取结束,值为:%d\n", i, v)
rw.RUnlock()
ch <- struct{}{}
}
func write(ch chan struct{}) {
rw.Lock()
fmt.Printf("goroutine write 进入写操作\n")
v := rand.Intn(1000)
count = v
fmt.Printf("goroutine write 写入结束,新值为:%d\n", count)
rw.Unlock()
ch <- struct{}{}
}
加入写锁,输出如下(因为多线程的原因,结果不唯一):
goroutine 1 进入读操作
goroutine 0 进入读操作
goroutine 1 读取结束,值为:0
goroutine 0 读取结束,值为:0
goroutine write 进入写操作
goroutine write 写入结束,新值为:81
goroutine 2 进入读操作
goroutine 3 进入读操作
goroutine 4 进入读操作
goroutine 2 读取结束,值为:81
goroutine 4 读取结束,值为:81
goroutine 3 读取结束,值为:81
可以看到加入了写锁之后,2,3,4这三个goroutine都必须等到goroutine write写入结束才能进入读操作。而0,1这两个goroutine这两个读操作完成之后,才能进入写goroutine。也就说明读写之间是互斥的,而多个读之间是不互斥的。
- sync.WaitGroup 主要用于等待一组goroutine结束。一般用于控制主线程等待所有的goroutine都结束之后再结束。
示例如下:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
wg := sync.WaitGroup{}
for i:=0;i<5;i++{
wg.Add(1)
go func(i int) {
defer wg.Done()
fmt.Printf("goroutine %d starts sleep\n", i)
time.Sleep(time.Second * time.Duration(i))
fmt.Printf("goroutine %d finished sleep\n", i)
}(i)
}
fmt.Println("wait group waiting...")
wg.Wait()
fmt.Println("wait group waitint finished")
}
输出如下:
wait group waiting...
goroutine 1 starts sleep
goroutine 3 starts sleep
goroutine 4 starts sleep
goroutine 0 starts sleep
goroutine 0 finished sleep
goroutine 2 starts sleep
goroutine 1 finished sleep
goroutine 2 finished sleep
goroutine 3 finished sleep
goroutine 4 finished sleep
wait group waitint finished
可以看到wg.Wait()这一句阻塞住了主线程,直到所有的goroutine结束之后,这里才不再阻塞。
- Once 相对而言是比较简单的,就是控制让某个函数仅仅执行一次,后面无论怎么调用,都不会再执行了。
示例如下:
package main
import (
"fmt"
"sync"
)
func onceFunc(i int) {
fmt.Printf("goroutine %d run", i)
}
func main() {
ch := make(chan struct{}, 10)
var once sync.Once
for i:=0;i<10;i++ {
go func() {
once.Do(func() {
onceFunc(i)
})
ch <- struct{}{}
}()
}
for i:=0;i<10;i++ {
<- ch
}
}
输出如下(多线程的原因,输出结果不唯一):
goroutine 3 run
可以看到尽管我们在代码里调用了10次,但是实际上只会执行1次。
- Cond 实现一个条件变量。代码示例如下:
package main
import (
"fmt"
"sync"
"time"
)
var count int = 4
func main() {
ch := make(chan struct{}, 5)
var l sync.Mutex
cond := sync.NewCond(&l)
for i:=0;i<5;i++ {
go func(i int) {
cond.L.Lock()
defer func() {
cond.L.Unlock()
ch <- struct{}{}
}()
for count > i {
cond.Wait()
fmt.Printf("收到一个通知 goroutine%d\n", i)
}
fmt.Printf("goroutine%d 执行结束\n", i)
}(i)
}
time.Sleep(time.Millisecond * 20)
fmt.Println("broadcast...")
cond.L.Lock()
count -= 1
cond.Broadcast()
cond.L.Unlock()
time.Sleep(time.Second * 1)
fmt.Println("signal...")
cond.L.Lock()
count -= 2
cond.Signal()
cond.L.Unlock()
time.Sleep(time.Second)
fmt.Println("broadcast...")
cond.L.Lock()
count -= 1
cond.Broadcast()
cond.L.Unlock()
for i:=0; i<5;i++{
<- ch
}
}
输出:
goroutine4 执行结束
broadcast...
收到一个通知 goroutine0
收到一个通知 goroutine3
goroutine3 执行结束
收到一个通知 goroutine2
收到一个通知 goroutine1
signal...
收到一个通知 goroutine0
broadcast...
收到一个通知 goroutine0
goroutine0 执行结束
收到一个通知 goroutine2
goroutine2 执行结束
收到一个通知 goroutine1
goroutine1 执行结束
可以看到cond主要是通过条件(count > i)和cond.Wait()函数来阻塞goroutine,而在外部修改了count之后,通过外部的Signal和Broadcast函数,通知到Lock了这个cond的goroutine,当条件满足时就不再阻塞了,而如果条件不满足,继续阻塞。
- Pool主要用于存临时对象,具体细节,且听下回分解。