大家好,今天将梳理出的 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 main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func 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() bool
func (m *Mutex) Unlock()
Lock()方法: 锁定
TryLock()方法: 尝试锁定并报告 (很少使用)
Unlock()方法: 解锁
Mutex 使用
举例:两个 goroutine,它们试图增加和减少一个公共值,并使用 Mutex 来同步访问。
// 并发修改一个公共值
package main
import (
"fmt"
"sync"
)
var Count int
var Lock sync.Mutex
var wg sync.WaitGroup
func 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: -1
Incrementing: 0
Incrementing: 1
Incrementing: 2
Incrementing: 3
Incrementing: 4
Incrementing: 5
Decrementing: 4
Decrementing: 3
Decrementing: 2
Decrementing: 1
Decrementing: 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() Locker
func (rw *RWMutex) RUnlock()
func (rw *RWMutex) TryLock() bool
func (rw *RWMutex) TryRLock() bool
func (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 main
import (
"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 main
import (
"os"
"fmt"
"sync"
"time"
"math"
"text/tabwriter"
)
var wg sync.WaitGroup
func 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()
}
输出
Readers RWMutex Mutex
1 68.662µs 10.743µs
2 55.434µs 30.323µs
4 7.104µs 6.948µs
8 51.248µs 28.52µs
16 14.832µs 20.174µs
32 81.398µs 82.892µs
64 138.191µs 76.251µs
128 130.363µs 82.062µs
256 87.907µs 58.945µs
512 173.526µs 159.093µs
1024 273.202µs 253.273µs
2048 540.341µs 505.23µs
4096 1.190994ms 963.283µs
8192 3.125891ms 2.015721ms
16384 4.364017ms 4.279084ms
32768 15.637706ms 12.776055ms
65536 17.786311ms 16.694232ms
131072 35.540288ms 39.605993ms
262144 61.371264ms 64.062957ms
524288 119.099709ms 131.583856ms
- producer 函数的第二个参数是
sync.Locker类型。 该接口有两种方法,锁定和解锁,Mutex和RWMutex类型都适用。 - 让 producer 休眠1秒
技术文章持续更新,请大家多多关注呀~~
搜索微信公众号,关注我【 帽儿山的枪手 】
参考材料
[1] 《Go并发编程实战》书籍
[2] 《Concurrency in Go》书籍
[3] pkg.go.dev/sync sync标准库