go锁
如果一个公共变量同时被多个go程操控, 会带来一些不可预知的错误。就类似去看电影选座位, 同一个座位在同时只能有一个人使用, 一旦被人提前买走后就无法再次购买。
var (
x int64
等待组 sync.WaitGroup
)
func 加() {
for i := 0; i < 10000; i++ {
x += 1
}
等待组.Done()
}
func main() {
并发数量 := 3
等待组.Add(并发数量)
for i := 0; i < 并发数量; i++ {
go 加()
}
等待组.Wait()
fmt.Printf("当前数量: %v, (正常数量:%v )", x, 并发数量*10000)
}
多运行几次上述代码会发现当前数量进程会和正常数量对不上!
产生这种情况的原因很简单, 因为在处理全局变量的时候是需要时间的, 比如第一个go程拿到全局变量正在处理时, 第二个go程序去读取全局变量这时候读取到的值还是之前未经过第一个处理的值, 同样的问题也出现在保存的时候, 后保存的全局变量会覆盖掉前一个。
互斥锁与读写互斥锁
互斥锁
解决上述问题很简单, 只需要让全局变量同时只能交给一个Go程执行, 剩下的go程需要等待第一个执行结束后在执行即可。也就是保证同一时刻只有一个Go程能拿到全局变量。
互斥锁的实现理解起来较容易, 当锁定一个对象后, 其他Go程无论是想要读取还是写入, 都需要等待对象被解锁后才能执行。
var (
x int64
等待组 sync.WaitGroup
互斥锁 sync.Mutex
)
func 加() {
for i := 0; i < 10000; i++ {
互斥锁.Lock()
x += 1
互斥锁.Unlock()
}
等待组.Done()
}
func main() {
并发数量 := 3
等待组.Add(并发数量)
for i := 0; i < 并发数量; i++ {
go 加()
}
等待组.Wait()
fmt.Printf("当前数量: %v, (正常数量:%v )", x, 并发数量*10000)
}
使用互斥锁后无论运行多少次, 当前数量都会和正常数量相同
读写互斥锁
读写互斥锁的逻辑并不是读锁会锁定读操作, 写锁会锁定写操作。这样理解是错误的!
var (
x int64
等待组 sync.WaitGroup
读写互斥锁 sync.RWMutex
)
func 加() {
for i := 0; i < 10000; i++ {
读写互斥锁.RLock()
time.Sleep(time.Second)
fmt.Println(x)
x += 1
读写互斥锁.RUnlock()
}
等待组.Done()
}
func main() {
并发数量 := 3
等待组.Add(并发数量)
for i := 0; i < 并发数量; i++ {
go 加()
}
等待组.Wait()
fmt.Printf("当前数量: %v, (正常数量:%v )", x, 并发数量*10000)
}
上述代码中, 我使用了读锁, 并且在其中让其停滞了一秒, 但实际运行效果是下图这样
每次间隔一秒都会同时被读取三次(因为我的并发数是三), 按效果就可以看出, 读锁并没有对数据读取产生任何影响, 实际上读锁锁的是写入操作。
var (
x int64
等待组 sync.WaitGroup
互斥锁 sync.Mutex
读写互斥锁 sync.RWMutex
)
func 加() {
for i := 0; i < 10000; i++ {
读写互斥锁.RLock()
x += 1
fmt.Println("+++++开始等待+++++", x)
time.Sleep(time.Second)
fmt.Println("-----结束等待-----", x)
读写互斥锁.RUnlock()
}
等待组.Done()
}
func 加_写锁() {
time.Sleep(time.Second * 2)
for i := 0; i < 10; i++ {
读写互斥锁.Lock()
x = 1
fmt.Println("加_写锁", x)
读写互斥锁.Unlock()
}
}
func 加_读锁() {
time.Sleep(time.Second * 2)
for i := 0; i < 10; i++ {
读写互斥锁.RLock()
x = 1
fmt.Println("加_读锁", x)
读写互斥锁.RUnlock()
}
}
func main() {
并发数量 := 3
等待组.Add(并发数量)
for i := 0; i < 并发数量; i++ {
go 加()
加_读锁()
}
等待组.Wait()
fmt.Printf("当前数量: %v, (正常数量:%v )", x, 并发数量*10000)
}
根据上面的代码的输出结果可以看出使用写锁的函数只有在读锁解除后(结束等待后)才会开始执行写入, 而使用读锁的函数并不会被读锁阻塞在读锁还没有被解除的时候就执行了。同样的方法,我们只需把上述加()
函数中的读锁替换成写锁就可以测试写锁了。最后我们可以获得如下结论:
读锁会阻塞写锁, 但不会对读锁产生影响; 写锁既会阻塞写如, 还会阻塞读取。并且写操作的优先级是高于读的, 如果在多个读取被阻塞的时候出现一个写操作, 写操作会被优先唤醒!
注: 读写互斥锁.RLocker().Lock()
与读写互斥锁.RLocker().Unlock()
相当于读写互斥锁.Lock()
和读写互斥锁.Unlock()
的别名。
想要知道Go是如何实现这样的读写互斥锁可以看一下这个大佬的博客 GO 读写锁实现原理剖析
性能对比
在读多写少的情况下, 读写锁性能明显优于互斥锁, 写入约接近读取时性能差距越小, 当写入多于读取的时候性能会逐渐持平。具体性能对比方法可以看李文周大佬的博客。
首次执行锁
在项目使用并发的时候, 往往出现某些初始化只需要执行一次即可, 这时候就可以利用到sync.Once()
类中的Do(函数)
方法, 这个方法能确保当前函数只被执行了一次!
var 等待组 sync.WaitGroup
var 首次执行 = sync.Once{}
func 初始化() {
fmt.Println("--------------初始化--------------")
开始初始化 := func() {
fmt.Println("初始化成功")
}
首次执行.Do(开始初始化)
defer 等待组.Done()
}
func main(){
for i := 0; i < 3; i++ {
等待组.Add(1)
go 初始化()
}
等待组.Wait()
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)