锁的基础
原子(atomic)操作
- 原子操作是一种硬件层面加锁的机制
- 保证操作一个变量的时候,其他协程/线程无法访问
- 只能适用于简单变量的简单操作 ```go
func do(p int32) { // p相加无法达到1000 p++ // 采用原子操作 atomic.AddInt32(p,1) }
func main() { p := int32(0) for i := 0; i < 1000; i++ { go do(&p) } time.Sleep(time.Second) fmt.Println(p) }
<a name="LhVmx"></a>
## sema锁
1. 也叫信号量锁/信号锁
2. 核心是uint32值,含义是同时可并发的数量
3. 每一个sema锁都对应一个semaRoot结构体
![image.png](https://cdn.nlark.com/yuque/0/2022/png/22399957/1663903555187-ff2531bd-a099-4a3f-be03-df5f2142b286.png#clientId=u5fab819e-4140-4&errorMessage=unknown%20error&from=paste&height=864&id=u7dbb9ff0&originHeight=1080&originWidth=1920&originalType=binary&ratio=1&rotation=0&showTitle=false&size=220024&status=error&style=none&taskId=u5bd7f7b5-35e2-4faf-8a85-25cd9b5b16b&title=&width=1536)
<a name="nP5Oc"></a>
### sema操作(sema>0)
1. 获取锁:uint32减一,获取成功
2. 释放锁:uint32加一,释放成功
<a name="FxKU2"></a>
### sema==0
1. 获取锁:协程休眠。进入堆树等待
2. 释放锁:从堆树中取出一个协程,唤醒
3. sema锁退化成一个专用休眠队列
<a name="UzB5q"></a>
## 总结
1. 原子操作是一种硬件层面加锁的机制
2. 数据类型和操作类型都有限制
3. sema锁是runtime的常用工具
4. sema锁经常被用作休眠队列
<a name="fHoC0"></a>
# 互斥锁
1. sync.Mutex
2. go 用于并发保护最常见的方案
<a name="O1w7u"></a>
## 结构体
```go
type Mutex struct {
state int32
sema uint32
}
正常模式
加锁
- 尝试CAS直接加锁
- 若无法直接获取,进行多次自旋尝试
- 多次尝试失败,进入sema队列休眠
解锁
- 将locked置为1的协程,会把locked置为0. 然后检查WaiterShift有没有协程在等待
- 有协程等待,会唤醒sema中的一个协程放入到调度器中
- 被唤醒的协程,继续去抢夺locked这把锁,有可能还会抢不到这把锁,会继续进入队列休眠.重复此操作会造成锁饥饿
总结
- mutex正常模式: 自旋加锁+sema休眠队列
-
锁饥饿
总结
锁竞争严重时,互斥锁进入饥饿模式
饥饿模式没有自旋等待,有利于公平(饥饿模式,新进的协程直接进入队里休眠)
经验
要减少锁的使用时间
-
读写锁
结构体
type RWMutex struct {
w Mutex // held if there are pending writers
writerSem uint32 // semaphore for writers to wait for completing readers
readerSem uint32 // semaphore for readers to wait for completing writers
readerCount int32 // 已经架设读锁协程的数量
readerWait int32 // number of departing readers
}
w:互斥锁作为写锁
- writerSem:写协程队列
- readerSem:读协程队列
- readerCount:正值:正在读的协程,负值:加了写锁的数量
- readerWait:写锁应该等待释放读协程的个数
等待组waiteGroup
结构体
type WaitGroup struct {
noCopy noCopy //告诉编译器,该结构体禁止拷贝
// 64-bit value: high 32 bits are counter, low 32 bits are waiter count.
// 64-bit atomic operations require 64-bit alignment, but 32-bit
// compilers do not ensure it. So we allocate 12 bytes and then use
// the aligned 8 bytes in them as state, and the other 4 as storage
// for the sema.
// 三个uint32, 一个为被等待协程计数器counter,一个为等待协程计数器waiter count,一个为seam等待队列
state1 [3]uint32
}
package main
import (
"fmt"
"sync"
)
type Person struct {
salary int
level int
}
func (p *Person) promote( w *sync.WaitGroup) {
p.salary+=1000
fmt.Println("salary",p.salary)
p.level++
fmt.Println("level",p.level)
w.Done()
}
func main() {
P:=Person{level: 1,salary: 10000}
wg := sync.WaitGroup{}
wg.Add(3)
go P.promote(&wg)
go P.promote(&wg)
go P.promote(&wg)
wg.Wait()
}
once锁
结构体
type Once struct {
// done indicates whether the action has been performed.
// It is first in the struct because it is used in the hot path.
// The hot path is inlined at every call site.
// Placing done first allows more compact instructions on some architectures (amd64/386),
// and fewer instructions (to calculate offset) on other architectures.
done uint32
m Mutex
}
排查锁异常情况
vet工具
package main
import (
"fmt"
"sync"
)
type Person struct {
mu sync.RWMutex
salary int
level int
}
func (p *Person) promote( ) {
//p.mu.Lock()
//defer p.mu.Unlock()
p.salary+=1000
fmt.Println("salary",p.salary)
p.level++
fmt.Println("level",p.level)
}
func main() {
// 拷贝场景一
P:=Person{level: 1,salary: 10000}
p:=P//拷贝的结构体中有互斥锁
// 拷贝场景二
m:=sync.Mutex{}
m.Lock()
//业务代码
n:=m//拷贝锁
//业务代码
m.Unlock()
n.Lock()
}
go vet main.go
数据竞争
go build -race main.go
./main.exe //执行编译后的文件,就可以查看到代码文件中的数据竞争或者bug
func main() {
n:=0
for i := 0; i < 200; i++ {
go func() {
n++
}()
}
fmt.Println(n)//n无法到达200
time.Sleep(time.Second * 10)
}
死锁检测
https://github.com/sasha-s/go-deadlock