简介
Go语言使用go
语句开启新的goroutine
,由于goroutine
非常轻量除了对其分配栈空间外,所占的空间也是微乎其微的。但当多个goroutine
同时处理时会遇到比如同时抢占一个资源,某个goroutine
会等待等一个goroutine
处理完毕某才能继续执行的问题。对于这种情况,官方并不希望依靠共享内存的方式来实现进程的协同操作,而是希望通过channel
信道的方式来处理。但在某些特殊情况下,依然需要使用到锁,为此sync
包提供了锁。
sync
包围绕着Locker
锁接口展开,Locker
接口中提供了两个方法Lock()
和Unlock()
。
type Locker interface {
Lock()
Unlock()
}
Go语言标准库sync
中提供了两种锁分别是互斥锁sync.Mutex
和读写互斥锁sync.RWMutex
sync.Mutex
sync.Mutex
可能是sync
包中使用最广泛的原语。它允许在共享资源上互斥访问(不能同时访问),sync.Mutex
是Golang
标准库提供的一个互斥锁,当一个goroutine
获得互斥锁权限后,其他请求锁的goroutine
会阻塞在Lock()
方法的调用上,直到调用Unlock()
方法被释放。
它由两个字段 state
和 sema
组成,state
表示当前互斥锁的状态,而 sema
真正用于控制锁状态的信号量,这两个加起来只占 8 个字节空间的结构体就表示了 Go 语言中的互斥锁。
type Mutex struct {
state int32 //状态标识
sema uint32 //信号量
}
sync.Mutex
不区分读写锁,只有Lock()
和Lock()
之间才会导致阻塞的情况。若在一个地方Lock()
,在另一个地方不Lock()
而是直接修改或访问共享数据,对于sync.Mutext
类型是允许的,因为mutex
不会和goroutine
进行关联。若要区分读锁和写锁,可使用sync.RWMutex
类型。可以通过在代码前调用Lock
方法,在代码后调用Unlock
方法来保证一段代码的互斥执行,也可以使用defer
语句来保证互斥锁一定会被解锁。当一个goroutine
调用Lock
方法获得锁后,其它请求的goroutine
都会阻塞在Lock
方法直到锁被释放。
mutex := &sync.Mutex{}
mutex.Lock()
// Update共享变量 (比如切片,结构体指针等)
mutex.Unlock()
示例代码:大家可以想一下如果不加锁,会发生什么呢?num
最终会输出100吗?
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var num = 0
var locker sync.Mutex
for i := 0; i < 100; i++ {
go func(i int) {
locker.Lock()
defer locker.Unlock()
num++
fmt.Printf("goroutine %d: num = %d\n", i, num)
}(i)
}
time.Sleep(time.Second)
}
Go语言中的Mutex类型的互斥锁Lock()
锁定与其它语言不同的是,Lock()
锁定的是互斥锁而非一段代码。其他语言比如Java中使用同步锁锁定的是一段代码,以确保多线程并发誓只有一个线程可以控制运行此代码块直到释放同步锁。Go语言是在goroutine
中锁定互斥锁,其它goroutine
执行到有锁的位置时,由于获取不到互斥锁的锁定,因此会发生阻塞而等待,从而达到控制同步的目的。
sync.RWMutex
读写互斥锁在 Go 语言中的实现是 RWMutex
,其中不仅包含一个互斥锁,还持有两个信号量,分别用于写等待读和读等待写:
type RWMutex struct {
w Mutex
writerSem uint32
readerSem uint32
readerCount int32
readerWait int32
}
readerCount
存储了当前正在执行的读操作的数量,最后的 readerWait
表示当写操作被阻塞时等待的读操作个数。
sync.RWMutex
读写锁是基于sync.Mutex
实现的,读写锁的特点是针对读写操作的互斥锁,读写锁与互斥锁最大不同之处在于分别对读、写进行了锁定。一般用在大量读操作少量写操作中。
- 同时只能具有一个
goroutine
能够获得写锁定 - 同时可以具有任意多个
goroutine
获得读锁定 - 同时只能存在写锁定或读锁定,即读和写互斥。
Go标准库sync.RWMutex
读写互斥锁提供了四个方法
读写互斥锁 | 描述 |
---|---|
Lock |
添加写锁 |
Unlock |
释放写锁 |
RLock |
添加读锁 |
RUnlock |
释放读锁 |
加锁同Mutex
mutex := &sync.RWMutex{}
mutex.Lock()
// Update 共享变量
mutex.Unlock()
mutex.RLock()
// Read 共享变量
mutex.RUnlock()
sync.WaitGroup
sync.WaitGroup
也是一个经常会用到的同步原语,它的使用场景是在一个goroutine
等待一组goroutine
执行完成。
sync.WaitGroup
拥有一个内部计数器。当计数器等于0
时,则Wait()
方法会立即返回。否则它将阻塞执行Wait()
方法的goroutine
直到计数器等于0
时为止。
要增加计数器,我们必须使用Add(int)
方法。要减少它,我们可以使用Done()
(将计数器减1
),也可以传递负数给Add
方法把计数器减少指定大小,Done()
方法底层就是通过Add(-1)
实现的。
WaitGroup
结构体中的成员变量非常简单,其中的 noCopy
的主要作用就是保证 WaitGroup
不会被开发者通过再赋值的方式进行拷贝,进而导致一些诡异的行为:
type WaitGroup struct {
noCopy noCopy
state1 [3]uint32
}
copylock
包就是一个用于检查类似错误的分析器,它的原理就是在 编译期间 检查被拷贝的变量中是否包含 noCopy
或者 sync
关键字,如果包含当前关键字就会报出以下的错误,需要注意的是只有在**go vet**
命令下,才会提示出其中的**nocopy**
问题,整个sync下的常用锁都是不可复制
package main
import (
"fmt"
"sync"
)
func main() {
wg := sync.WaitGroup{}
yawg := wg
fmt.Println(wg, yawg)
}
/**
输出:
.\syncTest.go:10:13: variable declaration copies lock value to yawg: sync.WaitGroup contains sync.noCopy
.\syncTest.go:11:14: call of fmt.Println copies lock value: sync.WaitGroup contains sync.noCopy
.\syncTest.go:11:18: call of fmt.Println copies lock value: sync.WaitGroup contains sync.noCopy
*/
在以下示例中,我们将启动八个goroutine
,并等待他们完成
func main() {
wg := &sync.WaitGroup{}
for i := 0; i < 8; i++ {
wg.Add(1)
go func(i int) {
fmt.Printf("goroutine %d running\n", i)
// Do something
wg.Done()
}(i)
}
wg.Wait()
// 继续往下执行...
fmt.Println("main goroutine running")
}
每次创建goroutine
时,我们都会使用wg.Add(1)
来增加wg
的内部计数器。我们也可以在for
循环之前调用wg.Add(8)
。与此同时,每个goroutine
完成时,都会使用wg.Done()
减少wg
的内部计数器。main goroutine
会在八个goroutine
都执行wg.Done()
将计数器变为0
后才能继续执行。
Once
Go 语言在标准库的 sync
同步包中还提供了 Once
语义,它的主要功能其实也很好理解,保证在 Go 程序运行期间 Once
对应的某段代码只会执行一次。
作为 sync
包中的结构体,Once
有着非常简单的数据结构,每一个 Once
结构体中都只包含一个用于标识代码块是否被执行过的 done
以及一个互斥锁 Mutex
type Once struct {
done uint32
m Mutex
}
在如下所示的代码中,Do
方法中传入的函数只会被执行一次,也就是我们在运行如下所示的代码时只会看见一次 only once
的输出结果:
func main() {
o := &sync.Once{}
for i := 0; i < 10; i++ {
o.Do(func() {
fmt.Println("only once")
})
}
}
/**
*输出:
*only once
*/
Cond
Go 语言在标准库中提供的 Cond
其实是一个条件变量,通过 Cond
我们可以让一系列的 Goroutine 都在触发某个事件或者条件时才被唤醒,每一个 Cond
结构体都包含一个互斥锁 L
Cond
的结构体中包含 noCopy
和 copyChecker
两个字段,前者用于保证 Cond
不会再编译期间拷贝,后者保证在运行期间发生拷贝会直接 panic
,持有的另一个锁 L
其实是一个接口 Locker
,任意实现 Lock
和 Unlock
方法的结构体都可以作为 NewCond
方法的参数
type Cond struct {
noCopy noCopy
L Locker
notify notifyList
checker copyChecker
}
结构体中最后的变量 notifyList
其实也就是为了实现 Cond
同步机制,该结构体其实就是一个 Goroutine
的链表:
type notifyList struct {
wait uint32
notify uint32
lock mutex
head *sudog
tail *sudog
}
在这个结构体中,head
和 tail
分别指向的就是整个链表的头和尾,而 wait
和 notify
分别表示当前正在等待的 Goroutine 和已经通知到的 Goroutine,我们通过这两个变量就能确认当前待通知和已通知的 Goroutine。
通过如下示例来展示Cond
如何使用
func main() {
c := sync.NewCond(&sync.Mutex{})
for i := 0; i < 10; i++ {
go listen(c)
}
go broadcast(c)
ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt)
<-ch
}
func broadcast(c *sync.Cond) {
c.L.Lock()
c.Broadcast()
c.L.Unlock()
}
func listen(c *sync.Cond) {
c.L.Lock()
c.Wait()
fmt.Println("listen")
c.L.Unlock()
}
在上述代码中我们同时运行了 11 个 Goroutine,其中的 10 个 Goroutine 会通过 Wait
等待期望的信号或者事件,而剩下的一个 Goroutine 会调用 Broadcast
方法通知所有陷入等待的 Goroutine,当调用 Boardcast
方法之后,就会打印出 10 次 "listen"
并结束调用。