当我们的程序就一个线程的时候是不需要用到锁的,但是通常我们实际的代码不会是单个线程的,所有这个时候就需要用到锁了,那么关于锁的使用场景主要涉及到哪些呢?
- 当我们多个线程在读相同的数据的时候则是需要加锁的
- 当我们的程序既有读又有写的时候更是需要加锁的
- 当我们有多个线程在写的时候同样也是需要加锁
互斥锁
互斥锁:同一个时刻只有一个线程能够拿到锁
我们先通过一个例子来演示,如果当多个线程同时更改一个变量,结果会是怎么样
不加锁版本
package main import ( "sync" "fmt" ) var ( //lock sync.Mutex count int w sync.WaitGroup //用于等待子线程执行完之后退出 ) func main() { w.Add(1) // 在调用线程前执行w.add go func(){ for i:=0;i<100000;i++{ count++ } w.Done() //执行完 执行w.Done }() for i :=0;i<100000;i++{ count++ } w.Wait() // 最后执行w.wait等待所有的线程执行完毕 fmt.Println(count) }
当我们运行多次就可以发现,最后的结果基本不可能是我们先看到的:200000
我们修改代码代码需要加锁保护的地方加上锁,并且这里加的是互斥锁,修改后的代码为:
package main import ( "sync" "fmt" ) var ( lock sync.Mutex count int w sync.WaitGroup //用于等待子线程执行完之后退出 ) func main() { w.Add(1) // 在调用线程前执行w.add go func(){ for i:=0;i<100000;i++{ lock.Lock() count++ lock.Unlock() } w.Done() //执行完 执行w.Done }() for i :=0;i<100000;i++{ lock.Lock() count++ lock.Unlock() } w.Wait() // 最后执行w.wait等待所有的线程执行完毕 fmt.Println(count) }
这次当我们多次运行的时候,就能保证我们每次都能看到我们想要的值:200000
接下来看读写锁
读写锁
读写锁主要用到读多写少的场景
读写锁分为:读锁和写锁
如果自己设置了一个写锁,那么其他读的线程以及写的线程都拿不到锁,这个时候和互斥锁的功能相同
如果自己设置了一个读锁,那么其他写的线程是拿不到锁的,但是其他读的线程都是可以拿到这个锁
我们把上面的例子代码进行更改:
package main import ( "sync" "fmt" ) var ( rwlock sync.RWMutex w sync.WaitGroup count int ) func main() { w.Add(1) go func(){ for i:=0;i<1000000;i++{ rwlock.Lock() // 这里定义了一个写锁 count++ rwlock.Unlock() } w.Done() }() for i:=0;i<1000000;i++{ rwlock.Lock() // 这里定义了一个写锁 count++ rwlock.Unlock() } w.Wait() fmt.Println(count) }
通过设置写锁,我们同样可以实现数据的一致性
下面是一个读锁的使用例子:
package main import ( "sync" "fmt" ) var ( rwlock sync.RWMutex w sync.WaitGroup count int ) func main() { w.Add(1) go func(){ for i:=0;i<1000000;i++{ rwlock.Lock() // 这里定义了一个写锁 count++ rwlock.Unlock() } w.Done() }() for i:=0;i<16;i++{ w.Add(1) go func(){ rwlock.RLock() //这里定义了一个读锁 fmt.Println(count) rwlock.RUnlock() //释放读锁 w.Done() }() } w.Wait() fmt.Println(count) }
Go中的原子操作
原子操作,我们则不需加锁,也能保证数据的一致性
并且如果只是计算,那么原子操作则是最快的
实例代码:
package main import ( "sync" //"time" "sync/atomic" "fmt" ) var ( w sync.WaitGroup count int32 ) func main() { w.Add(1) //start := time.Now().UnixNano() go func() { for i:=0;i<1000000;i++{ atomic.AddInt32(&count,1) } w.Done() }() for i:=0;i<1000000;i++{ atomic.AddInt32(&count,1) } w.Wait() //end := time.Now().UnixNano() //fmt.Println((end- start)/1000/1000) fmt.Println(count) }
关于互斥锁的补充
互斥锁需要注意的问题:
1、不要重复锁定互斥锁
2、不要忘记解锁互斥锁, 必要时使用defer语句
3、不要对尚未锁定或者已解锁的互斥锁解锁
4、不要对在多个函数之间直接传递互斥锁
对已经锁定的互斥锁进行锁定,会立即阻塞当前的goroutine 这个goroutine所执行的流程会一直停滞在该调用互斥锁的Lock方法的那行代码
所谓死锁: 当前程序中的主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 ,对其中的读锁也是如此,并且同样是不可恢复的