在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
什么时候需要用到锁?当程序中就一个线程的时候,是不需要加锁的,但是通常实际的代码不会只是单线程,所以这个时候就需要用到锁了,那么关于锁的使用场景主要涉及到哪些呢?
互斥锁(sync.Mutex)互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个 goroutine 可以访问到共享资源(同一个时刻只有一个线程能够拿到锁) 先通过一个并发读写的例子演示一下,当多线程同时访问全局变量时,结果会怎样? package main import ("fmt") var count int func main() { for i := 0; i < 2; i++ { go func() { for i := 1000000; i > 0; i-- { count ++ } fmt.Println(count) }() } fmt.Scanf("\n") //等待子线程全部结束 } 运行结果: 980117 1011352 //最后的结果基本不可能是我们想看到的:200000
修改代码,在累加的地方添加互斥锁,就能保证我们每次得到的结果都是想要的值 package main import ("fmt" "sync" ) var ( count int lock sync.Mutex ) func main() { for i := 0; i < 2; i++ { go func() { for i := 1000000; i > 0; i-- { lock.Lock() count ++ lock.Unlock() } fmt.Println(count) }() } fmt.Scanf("\n") //等待子线程全部结束 } 运行结果: 1952533 2000000 //最后的线程打印输出
读写锁(sync.RWMutex)在读多写少的环境中,可以优先使用读写互斥锁(sync.RWMutex),它比互斥锁更加高效。sync 包中的 RWMutex 提供了读写互斥锁的封装 读写锁分为:读锁和写锁
通过设置写锁,同样可以实现数据的一致性: package main import ("fmt" "sync" ) var ( count int rwLock sync.RWMutex ) func main() { for i := 0; i < 2; i++ { go func() { for i := 1000000; i > 0; i-- { rwLock.Lock() count ++ rwLock.Unlock() } fmt.Println(count) }() } fmt.Scanf("\n") //等待子线程全部结束 } 运行结果: 1968637 2000000
互斥锁和读写锁的性能对比demo:制作一个读多写少的例子,分别开启 3 个 goroutine 进行读和写,输出最终的读写次数 1)使用互斥锁: package main import ( "fmt" "sync" "time" ) var ( count int //互斥锁 countGuard sync.Mutex ) func read(mapA map[string]string){ for { countGuard.Lock() var _ string = mapA["name"] count += 1 countGuard.Unlock() } } func write(mapA map[string]string) { for { countGuard.Lock() mapA["name"] = "johny" count += 1 time.Sleep(time.Millisecond * 3) countGuard.Unlock() } } func main() { var num int = 3 var mapA map[string]string = map[string]string{"nema": ""} for i := 0; i < num; i++ { go read(mapA) } for i := 0; i < num; i++ { go write(mapA) } time.Sleep(time.Second * 3) fmt.Printf("最终读写次数:%d\n", count) } 运行结果: 最终读写次数:3766
2)使用读写锁 package main import ( "fmt" "sync" "time" ) var ( count int //读写锁 countGuard sync.RWMutex ) func read(mapA map[string]string){ for { countGuard.RLock() //这里定义了一个读锁 var _ string = mapA["name"] count += 1 countGuard.RUnlock() } } func write(mapA map[string]string) { for { countGuard.Lock() //这里定义了一个写锁 mapA["name"] = "johny" count += 1 time.Sleep(time.Millisecond * 3) countGuard.Unlock() } } func main() { var num int = 3 var mapA map[string]string = map[string]string{"nema": ""} for i := 0; i < num; i++ { go read(mapA) } for i := 0; i < num; i++ { go write(mapA) } time.Sleep(time.Second * 3) fmt.Printf("最终读写次数:%d\n", count) } 运行结果: 最终读写次数:8165 结果差距大概在 2 倍左右,读锁的效率要快很多!
关于互斥锁的补充互斥锁需要注意的问题:
死锁: 当前程序中的主 goroutine 以及我们启用的那些 goroutine 都已经被阻塞,这些 goroutine 可以被称为用户级的 goroutine 这就相当于整个程序已经停滞不前了,并且这个时候 go 程序会抛出如下的 panic: fatal error: all goroutines are asleep - deadlock! 并且go语言运行时系统抛出自行抛出的panic都属于致命性错误,都是无法被恢复的,调用recover函数对他们起不到任何作用 Go语言中的互斥锁是开箱即用的,也就是我们声明一个sync.Mutex 类型的变量,就可以直接使用它了,需要注意:该类型是一个结构体类型,属于值类型的一种,将它当做参数传给一个函数,将它从函数中返回,把它赋值给其他变量,让它进入某个管道,都会导致他的副本的产生。并且原值和副本以及多个副本之间是完全独立的,他们都是不同的互斥锁,所以不应该将锁通过函数的参数进行传递
关于读写锁的补充1、在写锁已被锁定的情况下再次试图锁定写锁,会阻塞当前的goroutine 2、在写锁已被锁定的情况下再次试图锁定读锁,也会阻塞当前的goroutine 3、在读锁已被锁定的情况下试图锁定写锁,同样会阻塞当前的goroutine 4、在读锁已被锁定的情况下再试图锁定读锁,并不会阻塞当前的goroutine
对于某个受到读写锁保护的共享资源,多个写操作不能同时进行,写操作和读操作也不能同时进行,但多个读操作却可以同时进行 对写锁进行解锁,会唤醒“所有因试图锁定读锁,而被阻塞的goroutine”, 并且这个通常会使他们都成功完成对读锁的锁定(这个还不理解) 对读锁进行解锁,只会在没有其他读锁锁定的前提下,唤醒“因试图锁定写锁,而被阻塞的 goroutine” 并且只会有一个被唤醒的 goroutine 能够成功完成对写锁的锁定,其他的 goroutine 还要在原处继续等待,至于哪一个goroutine,那么就要看谁等待的事件最长 解锁读写锁中未被锁定的写锁, 会立即引发panic ,对其中的读锁也是如此,并且同样是不可恢复的
参考链接:https://www.cnblogs.com/zhaof/p/8636384.html
ending ~
|
请发表评论