Go语言同步与锁

在并发条件下,信息的同步要求用户必须用锁的机制来保证准确性。Go语言标准库中提供了互斥锁和读写锁供开发者使用。

Go语言包中的sync包提供了两种锁类型:sync.Mutex和sync.RwMutex,前者是互斥锁,后者是读写锁。

互斥锁

互斥锁是传统的并发程序对资源共享进行访问控制的主要手段,在Go语言中,更推崇使用通道来实现资源共享和通信,互斥锁具有两个公开方法:调用Lock()获得锁和调用Unlock()释放锁。

  • 同一个协程中同步调用使用Lock()枷锁后,不能再对其加锁,只能在Unlock()之后再次Lock(),多个协程在Unlock()之后会产生锁竞争,来竞争下一次加锁的机会,因此互斥锁只允许有一个读或者写的场景,所以该锁也叫全局锁。
  • Unlock()只用于解锁,Unlock()之前必须要有Lock()。已经锁定的Mutex并不与特定的协程相关联,这样可以利用一个协程对其加锁,再利用其它协程对其解锁。

接下来看一段代码,通过三个协程体现互斥锁对资源的访问控制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package main

import (
"fmt"
"sync"
"time"
)

func main(){
wg := sync.WaitGroup{}
mutex := sync.Mutex{}

fmt.Println("Locking G0")
mutex.Lock()
fmt.Println("Locked G0")
wg.Add(3)

for i:=1;i<4;i++{
go func(i int) {
fmt.Printf("Locking G%d\n",i)
mutex.Lock()
fmt.Printf("Locked G%d\n",i)

time.Sleep(time.Second*2)
mutex.Unlock()
fmt.Printf("unlocked G%d\n",i)
wg.Done()
}(i)
}

time.Sleep(time.Second*2)
fmt.Println("ready unlocked G0")
mutex.Unlock()
fmt.Println("unlocked G0")
wg.Wait()
}

/*输出:
Locking G0
Locked G0
Locking G3
Locking G2
Locking G1
ready unlocked G0
unlocked G0
Locked G3
unlocked G3
Locked G2
unlocked G2
Locked G1
unlocked G1
*/

通过这段程序可以看到,只有当锁释放时,才能进行加锁动作,当G0的锁释放时,等待加锁的G1,G2,G3都会竞争加锁的机会。

读写锁

读写锁实质上是多读单写的互斥锁,分别针对读操作和写操作进行锁定和解锁操作,用于读次数远大于写次数的场合。读写锁具有四个公开的方法:写锁定Lock()、写解锁Unlock()、读锁定RLock()、读解锁RUnlock()。

  • 写锁定时,对读写锁进行读锁定或写锁定时,都将阻塞。
  • 读锁定时,对读写锁进行写锁定,将阻塞。但可多读。
  • 写解锁之前必须要写锁定。读解锁之间必须要读锁定。

下面看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import (
"fmt"
"sync"
"time"
)

var m *sync.RWMutex

func main() {
wg := sync.WaitGroup{}

wg.Add(20)
var rwMutex sync.RWMutex
Data := 0

for i:=0;i<10;i++{
go func(i int) {
rwMutex.RLock()
defer rwMutex.RUnlock()
fmt.Printf("读数据: %v , %d\n",Data,i )
wg.Done()
time.Sleep(time.Second)
}(i)

go func(t int) {
rwMutex.Lock()
defer rwMutex.Unlock()
Data += t
fmt.Printf("写数据: %v , %d\n", Data,i)
wg.Done()
time.Sleep(10*time.Second)
}(i)
}
wg.Wait()
}

运行这段代码可以看到,在写锁定的情况下,10s内其他协程都无法进行数据的读取,而当写解锁后,所有的读数据的协程都哦完成了对数据的读取。说直白一点就是:写的时候不可读,读的时候不可写,读的时候大家一起读,写的时候只有我能写。

sync.WaitGroup

WaitGroup用于线程总同步。它等待一组线程集合完成,才会继续向下执行。主线程调用Add()方法来设置等待的协程数量,然后每个协程运行,并在完成后调用Done()方法,同时Wait()方法用来阻塞主线程直到所有协程完成。

下面是一段代码实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
//wg.Add(5)
wg.Add(10)
for i:=0;i<10;i++{
go func(t int) {
defer wg.Done()
fmt.Println(t)
}(i)
}
wg.Wait()
}

当调用Wg.Add(5)是,程序只会输出5个结果,这是因为5个协程执行了Wg.Done()后,满足了主线程的Wg.Wait()的要求,程序解除阻塞,自动退出。