大家好,今天将梳理出的 Go语言并发知识内容,分享给大家。 请多多指教,谢谢。
本章节内容
WaitGroup
Mutex
Locker
RWMutex
Go在内存访问同步基元的基础上构建了一组新的并发基元,并为使用者提供扩展的内容。 Go sync标准库,主要包含对低级别内存访问同步最有用的并发原语。 如果你使用的是主要通过内存访问同步处理并发的语言,那么这些类型是不错的选择。
WaitGroup
WaitGroup 类型原型
type WaitGroup struct { // contains filtered or unexported fields}func (wg *WaitGroup) Add(delta int)func (wg *WaitGroup) Done()func (wg *WaitGroup) Wait()
可以把 WaitGroup 视作一个安全的并发计数器:调用 Add()
增加计数,调用 Done()
减少计数。调用 Wait()
会阻塞并等待至计数器归零。
请注意,Add()
的调用是在 goroutine 之外完成的。 如果没有这样做,我们会引入一个数据竞争条件,因为我们没有对 goroutine 做任何调度顺序上的保证; 我们可能在任何一个 goroutine 开始前触发 Wait()
调用。 如果 Add()
的调用被放置在 goroutine 的闭包中,对 Wait()
的调用可能完全没有阻塞地返回,因为 Add()
没有被执行。
WaitGroup 使用
package mainimport ( "fmt" "sync")var wg sync.WaitGroupfunc main() { hello := func(wg *sync.WaitGroup, id int) { defer wg.Done() fmt.Printf("id: %d\n", id) } wg.Add(5) for i := 0; i < 5; i++ { go hello(&wg, i+1) } wg.Wait()}
通常情况下,尽可能与要跟踪的 goroutine 就近且成对的调用 Add()
,但有时候会一次性调用 Add()
来跟踪一组 goroutine。
Mutex
之前文章中已经简单介绍了 Mutex
类型,可以参考 Golang 基础之并发知识 (三) 文章。
Mutex 很容易理解,代表 “mutual exclusion(互斥)”。互斥提供了一种并发安全的方式来表示对共享资源访问的独占。
可以理解为在代码块设置临界区,在同一时刻只能由一个 goroutine 去操作。
Mutex 类型原型
type Mutex struct { // contains filtered or unexported fields}func (m *Mutex) Lock()func (m *Mutex) TryLock() boolfunc (m *Mutex) Unlock()
Lock()
方法: 锁定
TryLock()
方法: 尝试锁定并报告 (很少使用)
Unlock()
方法: 解锁
Mutex 使用
举例:两个 goroutine,它们试图增加和减少一个公共值,并使用 Mutex 来同步访问。
// 并发修改一个公共值package mainimport ( "fmt" "sync")var Count intvar Lock sync.Mutexvar wg sync.WaitGroupfunc main() { // 增加 for i := 0; i <= 5; i++ { wg.Add(1) go func() { defer wg.Done() increment() }() } // 减少 for i := 0; i <= 5; i++ { wg.Add(1) go func() { defer wg.Done() decrement() }() } wg.Wait()}func increment() { Lock.Lock() defer Lock.Unlock() Count++ fmt.Printf("Incrementing: %d\n", Count)}func decrement() { Lock.Lock() defer Lock.Unlock() Count-- fmt.Printf("Decrementing: %d\n", Count)}
这里,count变量由互斥锁保护
输出
Decrementing: -1Incrementing: 0Incrementing: 1Incrementing: 2Incrementing: 3Incrementing: 4Incrementing: 5Decrementing: 4Decrementing: 3Decrementing: 2Decrementing: 1Decrementing: 0
这里因为goroutine调度机制原因,在大家各自设备编码后结果会发生变化。
注意,被锁定部分是程序的性能瓶颈,进入和退出锁定的成本有点高,因此通常尽量减少锁定涉及的范围。
Locker
Locker 接口原型
type Locker interface { Lock() Unlock()}
Locker接口中定义了锁定和解锁的方法。
RWMutex
RWMutex
是读写互斥锁,锁可以由任意数量的读或单个写持有。RWMutex
的零值是一个未锁定的mutex。
RWMutex
与 Mutex
在概念上是一样的:它保护对内存的访问;不过,RWMutex可以给你更多地控制方式。 你可以请求锁定进行读取,在这种情况下,你将被授予读取权限,除非锁定正在进行写入操作。 这意味着,只要没有别的东西占用写操作,任意数量的读取者就可以进行读取操作。
常见的服务对资源的读写比列会非常高,如果大多数的请求都是读请求,它们之间不会互相影响,那么就可以将资源的操作进行读和写分离,出于这样的考虑,可以使用RWMutex。
读写锁控制下的多个写操作之间都是互斥的,并且写操作与读操作之间也都是互斥的。但多个读操作之间不存在互斥关系。读写锁可以在大大降低因使用锁而造成的性能损耗,完成对共享资源的访问控制。
RWMutex 类型原型
type RWMutex struct { // contains filtered or unexported fields}func (rw *RWMutex) Lock()func (rw *RWMutex) RLock()func (rw *RWMutex) RLocker() Lockerfunc (rw *RWMutex) RUnlock()func (rw *RWMutex) TryLock() boolfunc (rw *RWMutex) TryRLock() boolfunc (rw *RWMutex) Unlock()
Lock()
方法: 用于写入的锁定;如果锁已被锁定用于读取或写入,则锁定会一直锁定,直到锁可用。
RLock()
方法: 用于读取的锁定;它不应用于递归读取锁定;被阻止的锁调用会阻止新读取获取锁。
RLocker()
方法: RLocker返回一个Locker接口,该接口通过调用rw来实现Lock和Unlock方法。
RUnlock()
方法: RUnlock撤销一个RLock调用;它不会影响其他同时读取的goroutine。如果rw在进入RUnlock时未被锁定读取,则为运行时错误。
TryLock()
方法: TryLock试图锁定rw进行写入,并报告是否成功。 (很少使用)
TryRLock()
方法: TryRLock尝试锁定rw进行读取,并报告是否成功。 (很少使用)
Unlock()
方法: 解锁用于写入的rw。如果rw未被锁定以写入要解锁的条目,则这是一个运行时错误。
与 Mutex 一样,RWMutex的互斥体与特定的 goroutine 没有关联。一个 goroutine 可以重新锁定(锁定),然后安排另一个 goroutine 运行锁定(解锁)。
RWMutex 使用
举例:读写锁的使用
package mainimport ( "fmt" "sync" "time")func main() { var rwm sync.RWMutex for i := 0; i < 3; i++ { go func(i int) { fmt.Printf("执行读锁: %d\n", i) rwm.RLock() fmt.Printf("读锁: %d\n", i) time.Sleep(time.Second * 2) fmt.Printf("执行取消读锁: %d\n", i) rwm.RUnlock() fmt.Printf("取消读锁: %d\n", i) }(i) } time.Sleep(time.Millisecond * 100) fmt.Println("执行写锁...") rwm.Lock() fmt.Println("写锁")}
输出
执行读锁: 0读锁: 0执行读锁: 1读锁: 1执行读锁: 2读锁: 2执行写锁...执行取消读锁: 1取消读锁: 1执行取消读锁: 0取消读锁: 0执行取消读锁: 2取消读锁: 2写锁
启用了 3 个 goroutine 用于读写锁 rwm 的读锁定和读解锁操作
读解锁操作会延迟 2s 进行模拟真是的情况
先让主 goroutine 睡眠 100ms,让 3个 goroutine先有足够时间执行
之后 rwm 的写锁定操作让主 goroutine 阻塞,因为此时 3个 goroutine读锁定还未进行读解锁操作
当 3个 goroutine读解锁完成后,main函数写锁定才会完成
可以通过这个例子看到 RWMutex
在大量级上相对于 Mutex
是有性能优势。 建议在逻辑上合理的情况下使用 RWMutex
而不是 Mutex
。
package mainimport ( "os" "fmt" "sync" "time" "math" "text/tabwriter")var wg sync.WaitGroupfunc main() { tw := tabwriter.NewWriter(os.Stdout, 0, 1, 2, ' ', 0) defer tw.Flush() var m sync.RWMutex fmt.Fprintf(tw, "Readers\tRWMutex\tMutex\n") for i := 0; i < 20; i++ { count := int(math.Pow(2, float64(i))) fmt.Fprintf( tw, "%d\t%v\t%v\n", count, test(count, &m, m.RLocker()), test(count, &m, &m), ) }}func test(count int, mutex, rwMutex sync.Locker) time.Duration { wg.Add(count + 1) beginTime := time.Now() go producer(&wg, mutex) for i := count; i > 0; i-- { go observer(&wg, rwMutex) } wg.Wait() return time.Since(beginTime)}func producer(wg *sync.WaitGroup, l sync.Locker) { // 1 defer wg.Done() for i := 5; i > 0; i-- { l.Lock() l.Unlock() time.Sleep(1) // 2 }}func observer(wg *sync.WaitGroup, l sync.Locker) { defer wg.Done() l.Lock() defer l.Unlock()}
输出
package mainimport ( "fmt" "sync")var wg sync.WaitGroupfunc main() { hello := func(wg *sync.WaitGroup, id int) { defer wg.Done() fmt.Printf("id: %d\n", id) } wg.Add(5) for i := 0; i < 5; i++ { go hello(&wg, i+1) } wg.Wait()}0
producer 函数的第二个参数是 sync.Locker
类型。 该接口有两种方法,锁定和解锁,Mutex
和 RWMutex
类型都适用。
让 producer 休眠1秒
技术文章持续更新,请大家多多关注呀~~
搜索微信公众号,关注我【 帽儿山的枪手 】
参考材料
[1] 《Go并发编程实战》书籍
[2] 《Concurrency in Go》书籍
[3] https://pkg.go.dev/sync sync标准库
原文:https://juejin.cn/post/7097902611017236488