? 放在前面说的话
大家好,我是北 ??
本科在读,此为日常捣鼓.
如有不对,请多指教,也欢迎大家来跟我讨论鸭 ???
今天是我们「Go并发」系列的第三篇:「sync包并发同步原语(1)」;
Let’s get it!
sync包
再并发编程中同步原语也就是我们日常说的锁
保证多线程或多goroutine在访问同一内存时,不出现混乱问题
Go语言提供的sync包提供了常见的并发编程同步原语
sync.Mutex
sync.RWMutex
sync.WaitGroup
sync.Map
sync.Pool
sync.Once
sync.Cond
一、sync.Mutex
在Go并发任务中,容易出现多个goroutine同时操作一个资源,这就会产生竞态问题。这时互斥锁就可以体现出它真正的作用了
1.sync.Mutex概念
Mutex 也称为互斥锁,互斥锁就是互相排斥的锁,它可以用作保护临界区的共享资源,保证同一时刻只有一个 goroutine 操作临界区中的共享资源。
控制共享资源访问方法
保证同一时间有且仅有一个goroutine操作资源(临界区),其他goroutine只能等待锁,直到前面的互斥锁释放,下一个goroutine才能获取锁去操作资源,往后同理
多线程,随机唤醒一个
2. sync.Mutex有以下方法
Mutex 的 Lock 方法和 Unlock 方法要成对使用,不要忘记将锁定的互斥锁解锁,一般做法是使用 defer。
3.sync.Mutex 栗子
不加锁栗子
func main() { var count = 0
var wg sync.WaitGroup
// 开启十个协程
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// 一万叠加
for j := 0; j < 10000; j++ {
count++
}
}()
}
wg.Wait()
fmt.Println(count)
}
打印:73662正确打印:100000原因:多个goroutine在同一片资源出现竟态问题,叠加错误(有时候可能能够正常执行,打印正确,但并不代表后面不会出现错误)### 加锁优化栗子```gofunc main() { var count = 0 var wg sync.WaitGroup var m sync.Mutex // 开启十个协程 for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() // 一万叠加 for j := 0; j < 10000; j++ { m.Lock() count++ m.Unlock() } }() } wg.Wait() fmt.Println(count)}打印:100000
4. sync.Mutex和sync.RWMutex比较
RWMutex是基于Mutex的,在Mutex的基础之上增加了读、写的信号量,并使用了类似引用计数的读锁数量,RWMutex 将对临界区的共享资源的读写操作做了区分,RWMutex 可以针对读写操作做不同级别的锁保护。
Mutex在大并发环境下,容易造成锁等待,对性能的影响较大。若某个读/写操作协程加了锁,其他协程就没必要处于等待状态了,也应该可以并发地访问共享变量,这时候,应使用RWMutex,让读/写操作并行,提高性能
二、sync.RWMutex
适用于并发读读,不能进行并发读写
1. sync.RWMutex概念
读锁与读锁兼容,读锁与写锁互斥,写锁与写锁互斥,只有在锁释放后才可以继续申请互斥的锁
可以同时申请多个读锁
有读锁时申请写锁将阻塞,有写锁时申请读锁将阻塞
只要有写锁,后续申请读锁和写锁都将阻塞
2.sync.RWMutex有以下方法
RWMutex 也称为读写互斥锁,读写互斥锁就是读取/写入互相排斥的锁。它可以由任意数量的读取操作的 goroutine 或单个写入操作的 goroutine 持有。 |方法|功能| | --- | --- | |Lock()|申请写锁| |Unlock()|申请释放写锁| |RLock()|申请读锁| |RUnlock()|申请释放读锁| |RLocker()|返回一个实现了Lock()和Unlock()方法的Locker接口|
3.sync.RWMutex栗子
func main() { var rm sync.RWMutex for i := 0; i < 3; i++ { go read(&rm, i) } time.Sleep(time.Second * 2)}func read(rm *sync.RWMutex, i int) { fmt.Println(i, "reader start") rm.RLock() fmt.Println(i, "reading") time.Sleep(time.Second * 1) rm.RUnlock() fmt.Println(i, "reader over")}打印:
打印结果看得出来,2开始,还没有读完,1和0就相继跟上开始读了
三、sync.WaitGroup
1. time.sleep()和sync.WaitGroup比较
理论上wait和sleep是完全没有可比性的,一个用于线程间的通信,另一个用于线程阻塞一段时间。唯一相同的就是,sleep方法和wait方法都是用来使线程进入休眠状态的,对于中断信号,都可以进行响应和中断 不同:
语法使用:wait方法必须配合synchronized一起使用,sleep可以单独使用
唤醒方式:sleep需要传递一个超时时间,因此,sleep具有主动唤醒功能,而不需要传递任何参数的wait只能被动唤醒
释放锁资源:wait方法主动释放锁,sleep不释放锁
线程进入状态:sleep有时限等待状态,wait无时限等待状态
2.sync.WaitGroup有以下方法
在并发操作中,生硬使用time.Sleep并不合适,反观前面的比较,sync.WaitGroup更适用于并发任务的同步实现
sync.WaitGroup内部维护着一个计数器,可增可减。当我们要启动Z个并发任务时,计时器总数要增加到Z,可以通过for循环Add()单个增加,也可以一下子增加到Z;每当一个任务结束时,Done()就要在计数器里-1,直到计数器为0时,调用Wait()表示等待并发任务执行完成。
3. sync.WaitGroup栗子
func main() { wg := sync.WaitGroup{} n := 10 wg.Add(n) // 计数器累加至n for i := 0; i <= n; i++ { go f(i, &wg) } wg.Wait() // 等待计数器值为0,告知main函数的主协程,其他协程执行完毕}func f(i int, wg *sync.WaitGroup) { fmt.Println(i) wg.Done() // 完成该协程后,计数器-1}sync.WaitGroup一定要通过指针传值,不然进程会进入死锁状态
? 放在后面的话
本文我们介绍了 Go 语言中的基本同步原语sync.Mutex、sync.RWMutex、sync.WaitGroup的概念和简单应用,并分别对sync.Mutex和sync.RWMutex,time.sleep和sync.WaitGroup进行了比较。读写互斥锁可以对临界区的共享资源做更加细粒度的访问控制,不限制对临界区的共享资源的并发读,所以在读多写少的场景,我们可以使用读写互斥锁替代互斥锁,提升应用程序的性能。在并发中,我们并不知道完成这一应用程序,我们需要多长时间,time.sleep时有时限的且功能等在并发中,并不算优雅,故sync.WaitGroup更适用于任务编排,等待多个 goroutine 全部完成。
sync包中还有三个比较常用的锁,我们将会在下一篇细说。
原文:https://juejin.cn/post/7102779550815223816