在并发操作中为了防止多任务同时修改共享资源导致的不确定结果,我们可能会用到互斥锁和读写锁。
一:互斥锁
1.互斥锁有两种操作,获取锁和释放锁
2.当有一个goroutine获取了互斥锁后,任何goroutine都不可以获取互斥锁,只能等待这个goroutine将互斥锁释放
3.互斥锁适用于读写操作数量差不多的情况
二:读写锁
1.读写锁有四种操作 读上锁 读解锁 写上锁 写解锁
2.写锁最多有一个,读锁可以有多个(最大个数据说和CPU个数有关)
3.写锁的优先级高于读锁,这是因为为了防止读锁过多,写锁一直堵塞的情况发生
4.当有一个goroutine获得写锁时,其他goroutine不可以获得读锁或者写锁,知道这个写锁释放
5.当有一个goroutine获得读锁时,其他goroutine可以获得读锁,但是不能获得写锁。所以由此也可得知,如果当一个goroutine希望获取写锁时,不断地有其他goroutine在获得读锁和释放读锁会导致这个写锁一直处于堵塞状态,所以让写锁的优先级高于读锁可以避免这种情况,
6.读写锁适用于读多写少的情景。
从上文我们可以得知,互斥锁是非常霸道地,因为一旦有任何一个goroutine获取了互斥锁,其他goroutine都不能获取了,即使这个goroutine可能仅仅只是读取数据而不是修改数据。
而我们想想一个情景,假设现在有三个goroutine:G1,G2,G3都想要读取一段数据A,我们如果用互斥锁的话,就是以下的情形:
G1先加锁,然后读取A,然后释放;然后G2加锁,读取A,释放;G3加锁,读取A,然后释放…这个操作是串行的,由于每个goroutine都需要排队等待前一个goroutine释放锁,所以效率显然不高。
但是如果这个时候我们用读写锁就可以让G1,G2,G3同时读A,就可以大大的提升效率。
三:互斥锁和读写锁性能对比
但是读写锁的效率就一定比互斥锁高吗?这个问题还有待商榷,之前看到一个博主做了实验,认为互斥锁的效率更高,详情:https://www.cnblogs.com/shuiyuejiangnan/p/9457089.html
之后本人把这个博主的代码copy到本地跑了一下发现确实互斥锁不如读写锁优,不过他的代码中的对比有一些问题,在互斥锁的get操作中返回map然后再获取值,在读写锁的get函数中返回的就是int,我将两者都改为获取map的value值发现还是读写锁的性能要好。
原版代码:
更改后代码:
不过我想按照自己的思路来进行一下对比。
对比代码如下:
package main
import (
"fmt"
"sync"
"time"
)
const MAXNUM = 1000 //map的大小
const LOCKNUM = 1e7 //加锁次数
var lock sync.Mutex //互斥锁
var rwlock sync.RWMutex //读写锁
var lock_map map[int]int //互斥锁map
var rwlock_map map[int]int //读写锁map
func main() {
var lock_w = &sync.WaitGroup{}
var rwlock_w = &sync.WaitGroup{}
lock_w.Add(LOCKNUM)
rwlock_w.Add(LOCKNUM)
lock_ch := make(chan int, 10000)
rwlock_ch := make(chan int, 10000)
lock_map = make(map[int]int, MAXNUM)
rwlock_map = make(map[int]int, MAXNUM)
time1 := time.Now()
for i := 0; i < LOCKNUM; i++ {
go test1(lock_ch, i, lock_map, lock_w)
}
lock_w.Wait()
time2 := time.Now()
for i := 0; i < LOCKNUM; i++ {
go test2(rwlock_ch, i, rwlock_map, rwlock_w)
}
rwlock_w.Wait()
time3 := time.Now()
fmt.Println("lock time:", time2.Sub(time1).String())
fmt.Println("rwlock time:", time3.Sub(time2).String())
}
func init_map(a map[int]int, b map[int]int) { //初始化map
for i := 0; i < MAXNUM; i++ {
a[i] = i
b[i] = i
}
}
func test1(ch chan int, i int, mymap map[int]int, w *sync.WaitGroup) int {
lock.Lock()
defer lock.Unlock()
w.Done()
return mymap[i % MAXNUM]
}
func test2(ch chan int, i int, mymap map[int]int, w *sync.WaitGroup) int {
rwlock.RLock()
defer rwlock.RUnlock()
w.Done()
return mymap[i % MAXNUM]
}
这里列出来加锁次数从1e1到1e7互斥锁和读写锁的耗时对比,由于本人电脑比较渣,做到1e7次加锁就比较慢了,就不想上继续做了,对比表格如下:
加锁次数 | 互斥锁耗时 | 读写锁耗时 | 互斥锁性能好 | 读写锁性能好 |
---|---|---|---|---|
1e1 | 0s | 0s | √ | √ |
1e2 | 996.8µs | 0s | √ | |
1e3 | 978.7µs | 996.7µs | √ | |
1e4 | 3.9493ms | 2.992ms | √ | |
1e5 | 23.9094ms | 29.9204ms | √ | |
1e6 | 223.3684ms | 298.2022ms | √ | |
1e7 | 2.3785913s | 3.0448529s | √ |
其实1e1-1e3次加锁时,互斥锁和读写锁的耗时是很不稳定的,有时互斥锁耗时多,有时读写锁耗时高,在这里我们主要看1e4以上的加锁对比就可以了
到这里我也是很疑惑的,为什么互斥锁的性能竟然比读写锁要好?这不科学啊!!!
在这里我有一点怀疑:是否golang中sync.Mutex的Lock和Unlock在底层实现的时候要比sync.RW