前面刚讲到goroutine和channel,通过goroutine启动一个协程,通过channel的方式在多个goroutine中传递消息来保证并发安全。今天我们来学习sync包,这个包是Go提供的基础包,提供了锁的支持。但是Go官方给的建议是:不要以共享内存的方式来通信,而是要以通信的手段来共享内存。所以他们是提倡使用channel的方式来实现并发控制。
学过Java的同学对锁的概念肯定不陌生,在Java中提供Sychronized
关键字提供独占锁,Lock
类提供读写锁。在sync包中实现的功能也是与锁相关,包中主要包含的对象有:
- Locker:提供了加锁和解锁的接口
- Cond:条件等待通过 Wait 让例程等待,通过 Signal 让一个等待的例程继续,通过 Broadcast 让所有等待的例程继续。
- Map:线程安全的map ,同时被多个goroutines调用是安全的。
- Mutex:互斥锁,用来保证在任一时刻,只能有一个例程访问某对象。实现了Locker接口。Mutex 的初始值为解锁状态,Mutex 通常作为其它结构体的匿名字段使用,使该结构体具有 Lock 和 Unlock 方法
- Once:Once 是一个可以被多次调用但是只执行一次,若每次调用Do时传入参数f不同,但是只有第一个才会被执行。
- Pool:用于存储临时对象,它将使用完毕的对象存入对象池中,在需要的时候取出来重复使用,其中存放的临时对象随时可能被 GC 回收掉如果该对象不再被其它变量引用
- RWMutex:读写互斥锁,RWMutex 比 Mutex 多了一个“读锁定”和“读解锁”,可以让多个例程同时读取某对象。RWMutex 的初始值为解锁状态。RWMutex 通常作为其它结构体的匿名字段使用。
- WaitGroup :用于等待一组例程的结束。主例程在创建每个子例程的时候先调用 Add 增加等待计数,每个子例程在结束时调用 Done 减少例程计数。之后主例程通过 Wait 方法开始等待,直到计数器归零才继续执行。
1. Mutex 互斥锁使用
我们先用Go写一段经典的并发场景:
package main
import (
"fmt"
"time"
)
func main() {
var a = 0
for i := 0;i<1000;i++{
go func(i int) {
a += 1
fmt.Println(a)
}(i)
}
time.Sleep(time.Second)
}
运行这段程序,你会发现最后输出的不是1000。
这个时候你可以使用Mutex:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var a = 0
var lock sync.Mutex
for i := 0;i<1000;i++{
go func(i int) {
lock.Lock()
a += 1
fmt.Println(a)
lock.Unlock()
}(i)
}
time.Sleep(time.Second)
}
Mutex实现了Locker接口,所以他有Lock()方法和Unlock()方法。只需要在需要同步的代码块上下使用这两个方法就好。
Mutex等同于Java中的Synchronized关键字或者Lock。
2. 读写锁-RWMutex
类似于Java中的ReadWriteLock。读写锁有如下四个方法:
写操作的锁定和解锁
* func (*RWMutex) Lock
* func (*RWMutex) Unlock
读操作的锁定和解锁
* func (*RWMutex) Rlock
* func (*RWMutex) RUnlock
当有一个 goroutine 获得写锁定,其它无论是读锁定还是写锁定都将阻塞直到写解锁;
当有一个 goroutine 获得读锁定,其它读锁定仍然可以继续 ;
当有一个或任意多个读锁定,写锁定将等待所有读锁定解锁之后才能够进行写锁定 。
总结上面的三句话可以得出结论:
- 同时只能有一个 goroutine 能够获得写锁定;
- 同时可以有任意多个 goroutine 获得读锁定;
- 同时只能存在写锁定或读锁定(读和写互斥)。
看一个读写锁的例子:
package main
import (
"fmt"
"strconv"
"sync"
"time"
)
var (
rwLock sync.RWMutex
data = ""
)
func read(ran int) {
time.Sleep(time.Duration(ran) * time.Microsecond)
rwLock.RLock()
fmt.Printf("读操作开始:%s
",data)
data = ""
rwLock.RUnlock()
}
func write(subData string) {
rwLock.Lock()
data = subData
fmt.Printf("写操作开始:%s
",data)
rwLock.Unlock()
}
func deduce() {
for i:=0;i<10;i++ {
go write(strconv.Itoa(i))
}
for i:=0;i<10;i++ {
go read(i * 100)
}
}
func main() {
deduce()
time.Sleep(2*time.Second)
}
运行上面的程序,会发现写操作都执行了,但是读操作不是将所有写的数字都读出来了。这是因为读操作是可以同时有多个goroutine获取锁的,但是写操作只能同时有一个goroutine执行。
3. WaitGroup
WaitGroup 用于等待一组 goroutine 结束,它有三个方法:
func (wg *WaitGroup) Add(delta int)
func (wg *WaitGroup) Done()
func (wg *WaitGroup) Wait()
与Java中类比的话,相似与CountDownLatch。
package main
import (
"fmt"
"sync"
"time"
)
func goWithMountain(p int,wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("%d,我已经上来了
",p)
}
func main() {
var wg sync.WaitGroup
wg.Add(10)
for i:=0;i<10;i++ {
go goWithMountain(i,&wg)
}
wg.Wait()
time.Sleep(2*time.Second)
fmt.Printf("=登山结束
")
}
输出:
0,我已经上来了
9,我已经上来了
3,我已经上来了
7,我已经上来了
8,我已经上来了
6,我已经上来了
2,我已经上来了
4,我已经上来了
5,我已经上来了
1,我已经上来了
=登山结束
是不是有一样一样的呢。
4. Cond条件变量
与互斥量不同,条件变量的作用并不是保证在同一时刻仅有一个线程访问某一个共享数据,而是在对应的共享数据的状态发生变化时,通知其他因此而被阻塞的线程。条件变量总是与互斥量组合使用。互斥量为共享数据的访问提供互斥支持,而条件变量可以就共享数据的状态的变化向相关线程发出通知。 下面给出主要的几个函数:
func NewCond(l Locker) *Cond:用于创建条件,根据实际情况传入sync.Mutex或者sync.RWMutex的指针,一定要是指针,否则会发生复制导致锁的失效
func (c *Cond) Broadcast():唤醒条件上的所有goroutine
func (c *Cond) Signal():随机唤醒等待队列上的goroutine,随机的方式效率更高
func (c *Cond) Wait():挂起goroutine的操作
看一个读写操作的例子:
package main
import (
"bytes"
"fmt"
"io"
"sync"
"time"
)
type MyDataBucket struct {
br *bytes.Buffer
gmutex *sync.RWMutex
rcond *sync.Cond //读操作需要用到的条件变量
}
func NewDataBucket() *MyDataBucket {
buf := make([]byte, 0)
db := &MyDataBucket{
br: bytes.NewBuffer(buf),
gmutex: new(sync.RWMutex),
}
db.rcond = sync.NewCond(db.gmutex.RLocker())
return db
}
func (db *MyDataBucket) Read(i int) {
db.gmutex.RLock()
defer db.gmutex.RUnlock()
var data []byte
var d byte
var err error
for {
//读取一个字节
if d, err = db.br.ReadByte(); err != nil {
if err == io.EOF {
if string(data) != "" {
fmt.Printf("reader-%d: %s
", i, data)
}
db.rcond.Wait()
data = data[:0]
continue
}
}
data = append(data, d)
}
}
func (db *MyDataBucket) Put(d []byte) (int, error) {
db.gmutex.Lock()
defer db.gmutex.Unlock()
//写入一个数据块
n, err := db.br.Write(d)
db.rcond.Broadcast()
return n, err
}
func main() {
db := NewDataBucket()
go db.Read(1)
go db.Read(2)
for i := 0; i < 10; i++ {
go func(i int) {
d := fmt.Sprintf("data-%d", i)
db.Put([]byte(d))
}(i)
time.Sleep(100 * time.Millisecond)
}
}
上例中,读操作必依赖于写操作先写入数据才能开始读。当读取的数据为空的时候,会先调用wait()方法阻塞当前方法,在Put方法中写完数据之后会调用Broadcast()去广播,告诉阻塞者可以开始了。
5.Pool 临时对象池
Pool 用于存储临时对象,它将使用完毕的对象存入对象池中,在需要的时候取出来重复使用,目的是为了避免重复创建相同的对象造成 GC 负担过重。从 Pool 中取出对象时,如果 Pool 中没有对象,将返回 nil,但是如果给 Pool.New 字段指定了一个函数的话,Pool 将使用该函数创建一个新对象返回。
sync.Pool可以安全被多个线程同时使用,保证线程安全。这个Pool和我们一般意义上的Pool不太一样 ,Pool无法设置大小,所以理论上只受限于系统内存大小。Pool中的对象不支持自定义过期时间及策略,究其原因,Pool并不是一个Cache。
看一个小例子:
package main
import (
"fmt"
"sync"
)
func main() {
//我们创建一个Pool,并实现New()函数
sp := sync.Pool{
New: func() interface{} {
return make([]int, 16)
},
}
item := sp.Get()
fmt.Println("item : ", item)
//我们对item进行操作
//New()返回的是interface{},我们需要通过类型断言来转换
for i := 0; i < len(item.([]int)); i++ {
item.([]int)[i] = i
}
fmt.Println("item : ", item)
//使用完后,我们把item放回池中,让对象可以重用
sp.Put(item)
//再次从池中获取对象
item2 := sp.Get()
//注意这里获取的对象就是上面我们放回池中的对象
fmt.Println("item2 : ", item2)
//我们再次获取对象
item3 := sp.Get()
//因为池中的对象已经没有了,所以又重新通过New()创建一个新对象,放入池中,然后返回
//所以item3是大小为16的空[]int
fmt.Println("item3 : ", item3)
}
输出:
item : [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
item : [0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15]
item2 : [0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15]
item3 : [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
6. Once 执行一次
Once 的作用是多次调用但只执行一次,Once 只有一个方法,Once.Do(),向 Do 传入一个函数,这个函数在第一次执行 Once.Do() 的时候会被调用,以后再执行 Once.Do() 将没有任何动作,即使传入了其它的函数,也不会被执行,如果要执行其它函数,需要重新创建一个 Once 对象。
看一个很简单的例子:
package main
import (
"fmt"
"sync"
)
func main() {
var once sync.Once
onceBody := func() {
fmt.Println("我只会出现一次")
}
done := make(chan bool)
for i := 0; i < 3; i++ {
go func() {
once.Do(onceBody)
done <- true
}()
}
for i := 0; i < 3; i++ {
<-done
}
}