Go 中的“锁” | Go 主题月

702 阅读5分钟

前言

之前文章里面,我聊到了一个在Go中最主要的思想,就是通过通讯来共享变量,而不是通过共享变量来完成通讯,但是有些时候,通过通道的方式并发处理的时候,会徒增它的复杂度,为了解决这个问题,Go中有一一个“锁包”,用来完成类似于其他多线程语言加锁的一个过程。

竞态条件

我们知道,当只有一个goroutine的时候(也就是只有一个main函数),这个goroutine下面的代码,都会按照这从上到下的一个线性的顺序去执行,但是如果存在多个goroutine的时候,就没有办法保证这些goroutine之间的执行顺序了,而因为这个机制,就会造成一些bug,也就是我们常说的在并发条件下,没有互相同步的情况下,访问某个共享资源的时候,并且试图对这个资源进行读和写的时候,就会出现竞争的状态

对一个共享资源进行读写操作的时候,必须保持一个原子性,特别是涉及到写操作的时候更是如此,必须保证同一时刻只能有一个goroutine对资源进行一个读写操作。

接下来我们看一个最经典的例子——银行存钱的例子

小明和小红同时往一个账号里面去存钱,他们首先会先查看这个账户里面的额度有多少,然后在往里面存入不同的数目的钱,然后最后再查看一下当前的金额数目

var w sync.WaitGroup
var money int // 这是一个模拟的数据库
func main() {
	money = 200
	w.Add(2)
	go work(100,"hong")
	go work(100,"min")
	w.Wait()
	log.Printf("money is: %v",money)
}

func work(a int, n string)  {
	defer w.Done()
	value := money // 模拟从数据库里面拉取数据
	value -= a
	log.Printf("%v buy",n)
	money = value // 模拟把数据重新push回数据库
}

我们来看一下它打印的结果是什么

2021/04/23 01:06:43 hong buy
2021/04/23 01:06:43 min buy
2021/04/23 01:06:43 money is: 100

这种时候我们可以看到,当小红和小明同样花了100块钱的时候,钱包里本来应该是一块钱都没有的,但是这里确实还剩下100块,这个问题就是再并发情况下的读写问题了。

而要想解决这种问题,一共有两种方式可以解决,一种是通过chan的通道来完成数据的同步,但今天不讨论这一个方式,而是讨论另外的一种传统的方式——加锁

互斥锁

互斥锁顾名思义就是两个不能同时存在的锁,也就是有ab两条线程,如果a线程持有了这个资源的锁,那么b线程就必须等待a线程释放锁,才可以去获得锁来操作这个资源。

互斥锁的本质实在代码中创建一个临界区,保证再同一时间只会有一个goroutine去访问这个变量,我们用互斥锁的概念,重新修改一下上面的那一份代码

var w sync.WaitGroup
var money int
var m sync.Mutex //初始化一个锁
func main() {
	money = 200
	w.Add(2)
	go work(100,"hong")
	go work(100,"min")
	w.Wait()
	log.Printf("money is: %v",money)
}

func work(a int, n string)  {
	m.Lock() //给这个goroutine上锁
	defer w.Done()
	defer m.Unlock() // 释放锁
	value := money // 模拟从数据库里面拉取数据
	value -= a
	log.Printf("%v buy",n)
	money = value
}

通过简单的几步修改,这次我们打出来的结果就正确了

2021/04/23 01:14:58 min buy
2021/04/23 01:14:58 hong buy
2021/04/23 01:14:58 money is: 0

通过将goroutine枷锁的方式,成功的解决了之前所遗留下来的并发上的问题,其中再lock和unlock之间创建的那片空间,被叫做是临界区。

读写锁

很多时候,我们并不能像上面的情况那样,给每一个goroutine都加上互斥锁,那么这样Go语言的并发的设计也就没什么意义了,当我们面对一个读多写少的情景的时候,就不能像上面那样,给每一个Goroutine都加上互斥锁。

这里就要提到另外一种概念——读写锁

例如,有一个人,中了一百万的彩票,他们一家人都非常的高兴,这个幸运儿再晚上的时候,一家人都会时不时的打开手机看一下这个银行卡的存款,这时候,如果是使用了互斥锁的情况下,再网络条件不是特别好的时候,当有一个人再查看数据的时候,其他人就会被卡住。

为了解决这种问题,就需要用到我们的读写锁。

这里知道,读锁是可以相互间兼容的,但是读锁和写锁之间,当有一个在操作这个资源的锁的时候,另一个也是不能包括读在内的所有情况。

var mu sync.RWMutex
var balance int
func Balance() int {
    mu.RLock() // readers lock
    defer mu.RUnlock()
    return balance
}

最后

关于Go中锁的概念其实还有一些,例如初始化的sync.one这类的,但是这些再实际运用中并不是特别普遍,这里我就不做过多的介绍了。

最后的最后!!!如果看官大老爷已经看到了这里了,那就点个赞呀!!!