记一次go协程读写锁 sync.RWMutex未释放导致其他协程阻塞bug

记一次go协程读写锁 sync.RWMutex未释放导致其他协程阻塞bug

  • 记一次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个读协程在等待锁,如下图所示
    记一次go协程读写锁 sync.RWMutex未释放导致其他协程阻塞bug_第1张图片
  • 以上两点正好验证了当协程结束时,写锁未释放会造成的后果

运行结果分析

详见示例代码中的注释说明

你可能感兴趣的:(go,golang,并发,读写锁)