在并发操作中为了防止多任务同时修改共享资源导致的不确定结果,我们可能会用到互斥锁和读写锁。
一:互斥锁
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.RWMutex的RLock和RUnlock性能要好?
如果假设成立的话,是有可能出现读写锁的性能不如互斥锁的情况的。
下面我们来验证一下:
package main
import (
"fmt"
"sync"
"time"
)
const LOCKNUM = 1e8 //加锁次数
var locks sync.Mutex //互斥锁
var rwlocks sync.RWMutex //读写锁
func main() {
time1 := time.Now()
for i := 0; i < LOCKNUM; i++ {
locks.Lock()
locks.Unlock()
}
time2 := time.Now()
for i := 0; i < LOCKNUM; i++ {
rwlocks.RLock()
rwlocks.RUnlock()
}
time3 := time.Now()
fmt.Println("lock time:", time2.Sub(time1).String())
fmt.Println("rwlock time:", time3.Sub(time2).String())
}
直接说结论,两者性能差不多,但是互斥锁的耗时会稍微少一些
今天又做了一下测试,发现其实defer会对程序性能影响产生比较大的影响,于是更改了一下test函数,不使用defer关键字。然后看一下效果:
这时两者的性能其实已经差不多了,至于为什么互斥锁还会用时还是会稍微少一些其实是因为即使在不做任何读取操作仅仅是获取锁和释放锁,互斥锁的用时就要少啦,见下图:
最后我们再做一个实验,在每一个goroutine获取map的值时,让他等待一段时间,增加goroutine的冲突可能,然后再看看效果:
func test1(ch chan int, i int, mymap map[int]int, w *sync.WaitGroup) int {
lock.Lock()
defer lock.Unlock()
w.Done()
time.Sleep(time.Nanosecond)
//ch <- i
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()
time.Sleep(time.Nanosecond)
//ch <- i
return mymap[i % MAXNUM]
}
lock time: 17.5834104s
rwlock time: 5.9241ms
可以看到在增强了goroutine的冲突可能之后,读写锁的优势一下子就体现出来了,而且非常明显。
总
结
:
\color{#FF0000}{总结:}
总结:
1.
在
单
纯
的
只
是
获
取
锁
和
释
放
锁
时
,
互
斥
锁
的
用
时
要
少
一
些
,
这
主
要
是
因
为
多
个
线
程
同
时
获
取
读
写
锁
的
情
况
比
较
少
出
现
。
\color{#FF0000}{1.在单纯的只是获取锁和释放锁时,互斥锁的用时要少一些,这主要是因为多个线程同时获取读写锁的情况比较少出现。 }
1.在单纯的只是获取锁和释放锁时,互斥锁的用时要少一些,这主要是因为多个线程同时获取读写锁的情况比较少出现。
2.
g
o
l
a
n
g
底
层
实
现
上
,
互
斥
锁
确
实
要
比
读
写
锁
的
性
能
要
好
一
些
,
这
主
要
是
因
为
读
写
锁
的
底
层
实
现
其
实
是
互
斥
锁
加
上
计
数
器
。
\color{#FF0000}{ 2.golang底层实现上,互斥锁确实要比读写锁的性能要好一些,这主要是因为读写锁的底层实现其实是互斥锁加上计数器。}
2.golang底层实现上,互斥锁确实要比读写锁的性能要好一些,这主要是因为读写锁的底层实现其实是互斥锁加上计数器。
3.
在
增
强
协
程
互
相
冲
突
的
效
果
后
,
读
写
锁
的
性
能
要
明
显
高
于
互
斥
锁
。
\color{#FF0000}{3.在增强协程互相冲突的效果后,读写锁的性能要明显高于互斥锁。}
3.在增强协程互相冲突的效果后,读写锁的性能要明显高于互斥锁。