sync
包包含了对低级别内存访问同步最有用的并发原语。
1. WaitGroup
sync.WiteGroup
的应用场景是等待一组并发操作完成。sync.WaitGroup
拥有一个内部计数器。当计数器等于0时,Wait()
方法会立即返回。否则它将阻塞执行Wait()
方法的goroutine
直到计数器等于0时为止。
要增加计数器,我们必须使用Add(int)
方法。要减少它,我们可以使用Done()
。
我们可以将sync.WaitGroup
视为一个并发-安全的计数器:调用通过传入的整数执行Add
方法增加计数器的增量,并调用Done
方法对计数器进行递减。Wait
阻塞,直到计数器为零。
func main() {
var wg sync.WaitGroup
wg.Add(1) // 表示一个Goroutine开始了
go func() {
defer wg.Done()
fmt.Println("第一个Goroutine执行完成")
}()
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("第二个Goroutine执行完成")
}()
wg.Wait()
fmt.Println("所有Goroutine执行完成")
}
执行结果为:
第二个Goroutine执行完成
第一个Goroutine执行完成
所有Goroutine执行完成
2. Mutex
Mutex
是互斥的意思,是保护程序中需要独占访问共享资源的区域的一种方式,我们可以通过使用Mutex
对内存进行保护来协调对内存的访问。
package main
import (
"sync"
"fmt"
)
var count int
func main() {
var lock sync.Mutex
increment := func() {
lock.Lock() // 请求对全局变量的独占,使用互斥锁来解决
defer lock.Unlock() // 完成对全局变量的独占,释放互斥锁
count ++
fmt.Println("Increment: ", count)
}
decrement := func() {
lock.Lock()
defer lock.Unlock()
count --
fmt.Println("Decrement: ", count)
}
var wg sync.WaitGroup
for i := 0; i <= 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
increment()
}()
}
for i := 0; i <= 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
decrement()
}()
}
wg.Wait()
fmt.Println("全部Goroutine都完成")
}
输出结果为:
Increment: 1
Decrement: 0
Increment: 1
Increment: 2
Increment: 3
Decrement: 2
Decrement: 1
Decrement: 0
Decrement: -1
Decrement: -2
Increment: -1
Increment: 0
全部Goroutine都完成
我们经常在defer
语句中调用Unlock
,这是很常见的习惯,它可以确保即使出现了panic
,调用也总是执行。如果不这样做,程序可能会陷入死锁。
3. RWMutex
读写互斥锁在概念上和互斥是一样的:它保护着对内存的访问。读写锁适合于对数据结构的读次数比写次数多得多的情况。 因为, 读模式锁定时可以共享, 以写模式锁住时意味着独占。
在Go
语言中,读写锁由结构体类型sync.RWMutex
代表。在此类型的方法集合中包含了两对方法,即:
func (*RWMutex) Lock
func (*RWMutex) Unlock
和
func (*RWMutex) RLock
func (*RWMutex) RUnlock
前一对方法的名称和签名与互斥锁的那两个方法完全一致。它们分别代表了对写操作的锁定和解锁。以下简称它们为写锁定和写解锁。而后一对方法则分别表示了对读操作的锁定和解锁。以下简称它们为读锁定和读解锁。
对已被写锁定的读写锁进行写锁定,会造成当前Goroutine
的阻塞,直到该读写锁被写解锁。当然,如果有多个Goroutine
因此而被阻塞,那么当对应的写解锁被进行之时只会使其中一个Goroutine
的运行被恢复。类似的,对一个已被写锁定的读写锁进行读锁定,也会阻塞相应的Goroutine
。但不同的是,一旦该读写锁被写解锁,那么所有因欲进行读锁定而被阻塞的Goroutine
的运行都会被恢复。另一方面,如果在进行过程中发现当前的读写锁已被读锁定,那么这个写锁定操作将会等待直至所有施加于该读写锁之上的读锁定都被清除。同样的,在有多个写锁定操作为此而等待的情况下,相应的读锁定的全部清除只能让其中的某一个写锁定操作获得进行的机会。
package main
import (
"sync"
"math/rand"
"time"
"fmt"
)
var count int
var mutex sync.RWMutex
func write(n int) {
rand.Seed(time.Now().UnixNano()) // 保证每次的结果都是随机的
mutex.Lock()
fmt.Printf("写goroutine %d 正在写数据\n", n)
num := rand.Intn(500)
count = num
fmt.Printf("写goroutine %d 写数据结束,写入新值%d\n", n, num)
mutex.Unlock()
}
func read(n int) {
mutex.RLock()
fmt.Printf("读goroutine %d 正在读数据\n", n)
num := count
fmt.Printf("读goroutine %d 读取数据结束,读到数据%d\n", n, num)
mutex.RUnlock()
}
func main() {
for i := 0; i < 10; i++ {
go read(i + 1)
}
for i := 0; i < 10; i++ {
go write(i + 1)
}
time.Sleep(time.Second * 5)
}
运行结果为:
读goroutine 1 正在读数据
读goroutine 1 读取数据结束,读到数据0
读goroutine 2 正在读数据
读goroutine 2 读取数据结束,读到数据0
读goroutine 6 正在读数据
读goroutine 6 读取数据结束,读到数据0
写goroutine 10 正在写数据
写goroutine 10 写数据结束,写入新值358
读goroutine 10 正在读数据
读goroutine 5 正在读数据
读goroutine 5 读取数据结束,读到数据358
读goroutine 7 正在读数据
读goroutine 7 读取数据结束,读到数据358
读goroutine 9 正在读数据
读goroutine 3 正在读数据
读goroutine 3 读取数据结束,读到数据358
读goroutine 9 读取数据结束,读到数据358
读goroutine 4 正在读数据
读goroutine 4 读取数据结束,读到数据358
读goroutine 10 读取数据结束,读到数据358
写goroutine 5 正在写数据
写goroutine 5 写数据结束,写入新值358
读goroutine 8 正在读数据
读goroutine 8 读取数据结束,读到数据358
写goroutine 8 正在写数据
写goroutine 8 写数据结束,写入新值358
写goroutine 3 正在写数据
写goroutine 3 写数据结束,写入新值290
写goroutine 4 正在写数据
写goroutine 4 写数据结束,写入新值23
写goroutine 6 正在写数据
写goroutine 6 写数据结束,写入新值332
写goroutine 7 正在写数据
写goroutine 7 写数据结束,写入新值117
写goroutine 9 正在写数据
写goroutine 9 写数据结束,写入新值480
写goroutine 1 正在写数据
写goroutine 1 写数据结束,写入新值85
写goroutine 2 正在写数据
写goroutine 2 写数据结束,写入新值162
从上面的运行结果我们可以看到,读操作是可以同时进行的。但是写操作只能一个一个的进行。
4.Cond
Cond
实现了一种条件变量,主要用来解决多个读协程等待共享资源变成ready的场景。在使用Cond
的时候,需要特别注意下:每个Cond
都会关联一个Lock(*sync.Mutex or *sync.RWMutex
),当修改条件或者调用Wait
方法时,必须加锁。Cond
主要有三个函数构成,Broadcast()
, Signal()
, Wait()
。
4.1 broadcast
用来唤醒所有的处于等待状态的协程,如果没有等待的协程,该函数也不会报错。
// Broadcast wakes all goroutines waiting on c.
//
// It is allowed but not required for the caller to hold c.L
// during the call.
func (c *Cond) Broadcast() {
c.checker.check()
runtime_notifyListNotifyAll(&c.notify)
}
4.2 signal
通知单个等待状态的协程,让它继续执行,如果此时有多个协程处于等待状态,会从等待列表中取出最开始等待的那个协程,来接收消息。
// Signal wakes one goroutine waiting on c, if there is any.
//
// It is allowed but not required for the caller to hold c.L
// during the call.
func (c *Cond) Signal() {
c.checker.check()
runtime_notifyListNotifyOne(&c.notify)
}
4.3 wait
该函数在被调用之后,在没有收到Signal
或者Broadcast
的通知之前,协程处于阻塞状态。
// Wait atomically unlocks c.L and suspends execution
// of the calling goroutine. After later resuming execution,
// Wait locks c.L before returning. Unlike in other systems,
// Wait cannot return unless awoken by Broadcast or Signal.
//
// Because c.L is not locked when Wait first resumes, the caller
// typically cannot assume that the condition is true when
// Wait returns. Instead, the caller should Wait in a loop:
//
// c.L.Lock()
// for !condition() {
// c.Wait()
// }
// ... make use of condition ...
// c.L.Unlock()
//
func (c *Cond) Wait() {
c.checker.check()
t := runtime_notifyListAdd(&c.notify)
c.L.Unlock()
runtime_notifyListWait(&c.notify, t)
c.L.Lock()
}
4.4 例子
package main
import (
"sync"
"fmt"
"time"
)
var locker = new(sync.Mutex)
var cond = sync.NewCond(locker)
func Test(x int) {
cond.L.Lock()
fmt.Println("aaa:", x)
cond.Wait()
fmt.Println("bbb:", x)
time.Sleep(time.Second * 1)
cond.L.Unlock()
}
func main() {
for i := 0; i < 5; i++ {
go Test(i)
}
fmt.Println("start all")
time.Sleep(time.Second * 1) // 使所有Goroutine进入wait状态
cond.Signal() // 唤醒单个锁
time.Sleep(time.Second * 1)
cond.Signal()
time.Sleep(time.Second * 1)
fmt.Println("broadcast")
cond.Broadcast() // 唤醒所有锁
time.Sleep(time.Second * 3)
fmt.Println("finish all")
}
执行结果为:
aaa: 0
aaa: 2
aaa: 1
aaa: 3
aaa: 4
start all
bbb: 0
bbb: 2
broadcast
bbb: 4
bbb: 1
bbb: 3
finish all
5. Once
sync.Once.Do(f func())
是一个挺有趣的东西,能保证once
只执行一次,无论你是否更换once.Do(xx)
这里的方法,这个sync.Once
块只会执行一次。
5.1 实例
package main
import (
"sync"
"fmt"
)
var count = 0
var once sync.Once
var wg sync.WaitGroup
func main() {
increment := func() {
count ++
}
wg.Add(100)
for i := 0; i < 100; i++ {
go func() {
defer wg.Done()
once.Do(increment)
}()
}
wg.Wait()
fmt.Println("count is ", count)
}
输出结果为:
count is 1
第二个实例:
package main
import (
"sync"
"fmt"
)
var count int
func main() {
increment := func() { count ++ }
decrement := func() { count -- }
var once sync.Once
once.Do(increment)
once.Do(decrement)
fmt.Println("count is ", count)
}
输出结果:
count is 1
这说明sync.Once
只计算调用Do
方法的次数,而不是多少次唯一的调用Do
方法。