golang是一门支持并发编程的语言,它提供了goroutine和channel等机制来实现多个任务的并行执行。但是,并发编程也会带来一些挑战,比如数据竞争、死锁、内存泄漏等。为了解决这些问题,golang提供了一个标准库sync,它包含了一些高性能的同步原语,可以帮助我们更好地管理并发状态和资源。
一、sync.Mutex
相信大多数同学都有线上抢购东西的经历,在开始抢购的一瞬间,有大量的用户都发起了请求,形成了不同的线程,对同一个商品进行抢购。现在我们来模拟一下这个场景,假设待抢购的商品是一款网红电视机,库存为1000台,在开始抢购的一瞬间,有刚好1000人点击了购买按钮,按照预期,抢购完成后,库存为0,代码如下:
func main() {
stock := 1000
group := sync.WaitGroup{
}
group.Add(1000)
for i := 0; i < 1000; i++ {
go func() {
stock -= 1
group.Done()
}()
}
group.Wait()
fmt.Println(stock)
}
输出如下:
76
可能不熟悉并发编程的同学可能会想:咦?为啥不是0呢?归根到底,-=1
这个操作并不是原子性的,为了解决这个问题,go引入了sync.Mutex{},这是一个互斥锁,它可以保证在任意时刻,只有一个goroutine可以访问某个共享变量或临界区。我们可以使用Lock()和Unlock()方法来加锁和解锁。我们对上面的代码做如下的改造例如:
func main() {
stock := 1000
mutex := sync.Mutex{
} //1.声明互斥锁
group := sync.WaitGroup{
}
group.Add(1000)
for i := 0; i < 1000; i++ {
go func() {
mutex.Lock() //2.加锁
stock -= 1
mutex.Unlock() //3.解锁
group.Done()
}()
}
group.Wait()
fmt.Println(stock)
}
输出如下:
0
为了保证stock的正确性,我们使用了sync.Mutex{}来加锁和解锁,这样同时只会有一个协程对stock变量进行操作,这样就可以避免数据竞争的问题,最终输出结果也符合我们最终的预期。
二、sync.RWMutex
sync.Mutext解决了并发问题,但是在实际使用场景中,有很多时候读的次数是远大于写的次数的,读取数据并不会对数据造成影响,只需要限制其他协程不能对数据同时进行修改即可,不需要限制其他的协程对该数据的读取操作。sync.RWMutex{}是一个读写锁,它可以保证在任意时刻,只有一个