记一次go协程读写锁 sync.RWMutex未释放导致其他协程阻塞bug
记一次go协程读写锁 sync.RWMutex未释放导致其他协程阻塞bug
通过一个简单示例模拟某协程结束,但是对共享变量的锁未释放,导致其他访问该共享变量的协程阻塞的程序运行结果
用到的监测工具
- http/pprof:一款用于golang程序运行时监测的工具,官网地址http/pprof
程序简要介绍
- 从main函数开始
- 初始化并运行监测程序:
go StartHTTPDebuger()
- 创建了一个共享变量:
students
,供多个协程进行读写操作 - 定义协程数量
N
,这里取200
- 启动
N
个读协程,对students
进行读操作QueryStudent
- 启动
N
个写协程,对students
进行写操作AddStudent
wg.Wait()
等待启动的读协程和写协程运行都运行结束,只有当所有的读协程和写协程运行结束才会运行wg.Wait()
后面的代码- 运行程序,并在浏览器中输入
http://localhost:8082/debug/pprof/goroutine?debug=1
进行程序运行监测
示例代码
package main
import (
"fmt"
"net/http"
"net/http/pprof"
"strconv"
"sync"
"time"
)
type Student struct {
id string
name string
}
func NewStudent(id, name string) *Student {
return &Student{
id: id,
name: name,
}
}
type Students struct {
mu sync.RWMutex // 对共享变量stus的访问需要加的锁
stus map[string]*Student
}
func (students *Students) AddStudent(student *Student) {
students.mu.Lock()
// AddStudent在协程中调用
// 由于读协程和写协程用的是相同的锁Students.mu来访问相同的数据stus,而且在golang中sync.RWMutex是写优先
// (读锁必须等待写锁释放),加上我们这里设置的条件,当满足这个条件时,直接返回,此时协程运行结束,此时写锁没有被释放(!!!!!)
// 由于其他的写协程和读协程必须等待锁释放才能被调度运行,由于上面写锁一致释放不了,所以,那些等待锁的协程会阻塞(一直没办法继续往下运行)
if student.id == "50" {
// 不释放写锁,直接返回
return
}
students.stus[student.id] = student
students.mu.Unlock()
}
func (students *Students) QueryStudent(id string) *Student {
students.mu.RLock()
stu := students.stus[id]
students.mu.RUnlock()
return stu
}
const (
pprofAddr string = ":8082"
)
func StartHTTPDebuger() {
pprofHandler := http.NewServeMux()
pprofHandler.Handle("/debug/pprof/", http.HandlerFunc(pprof.Index))
server := &http.Server{Addr: pprofAddr, Handler: pprofHandler}
go server.ListenAndServe()
}
func main() {
var wg sync.WaitGroup
go StartHTTPDebuger()
// 创建供写协程和读协程们访问的共享变量
students := &Students{
stus: make(map[string]*Student),
}
N := 200
for i := 0; i < N; i++ {
wg.Add(1)
go func(index int) {
defer wg.Done()
time.Sleep(time.Second * 2)
fmt.Println("query start:" + strconv.Itoa(index+1))
stu := students.QueryStudent(strconv.Itoa(index + 1))
fmt.Println("query end:" + strconv.Itoa(index+1))
if stu != nil {
fmt.Printf("id=%s, name=%s \n", stu.id, stu.name)
}
}(i)
}
for i := 0; i < N; i++ {
wg.Add(1)
go func(index int) {
defer wg.Done()
time.Sleep(time.Second * 1)
fmt.Println("add start:" + strconv.Itoa(index+1))
students.AddStudent(NewStudent(strconv.Itoa(index+1), "zhangsan_"+strconv.Itoa(index+1)))
fmt.Println("add end:" + strconv.Itoa(index+1))
}(i)
}
wg.Wait() // 等待wg的计数变为0(上面调用defer wg.Done()的所有协程运行结束)才会执行下面的代码
fmt.Println("all goroutine done") // 不输出说明:至少存在一个协程没法运行结束
for {
time.Sleep(time.Second * 2)
}
}
运行结果
wg.Wait()
后面的代码一直没有输出:说明读协程、写协程至少有一个没有结束- 访问
http://localhost:8082/debug/pprof/goroutine?debug=1
发现:有160个写协程和200个读协程在等待锁,如下图所示
- 以上两点正好验证了当协程结束时,写锁未释放会造成的后果
运行结果分析
详见示例代码中的注释说明