boxmoe_header_banner_img

Hello! 欢迎来到我的博客!

加载中

文章导读

Go 并发编程 – 互斥锁,读写锁,管道


avatar
xiaoifei 2026年6月3日 49

相关文章:
Go Goroutine-协程大揭秘
Go Channel-协程通信机制

前言

说到并发编程,不得不提到并发与并行

并发 (Concurrency)并行 (Parallelism)
含义逻辑上多个任务”同时进行”(可交错)物理上多个任务真的同时跑(多核)
硬件单核也能并发(靠切换)必须有多于一个 CPU 核(物理同时执行的能力)
类比一个人同时烧水+扫地(来回切换)两个人同时烧水和扫地
Go 的衡量goroutine 数实际用的 CPU 核数

对于Go而言,用户不能直接操纵操作系统的线程,只能通过创建Goroutine在线程上工作,实现并发。而并行,取决于CPU有几个核、OS如何分配线程到核。也就是说并发(结构)在 Go 层,并行(物理同时)在硬件/OS 层

对于广义的“并发”,实际上包含了上述的两个概念。

在runtime下存在runtime.NumCPU()方法,他能查看CPU逻辑核心数,并通过GOMAXPROCS(允许并行度)指定Goroutine最大并行数量

并发问题

单机环境中,由于线程/协程共享进程内存,因此在对同一变量(临界资源)进行修改会引发并发问题

var ticket = 10

func main() {
	var wg sync.WaitGroup
	wg.Go(func() { saleTicket("sale1:") })
	wg.Go(func() { saleTicket("sale2:") })
	wg.Go(func() { saleTicket("sale3:") })

	wg.Wait()
	fmt.Println("final ticket:", ticket) // -2
}

func saleTicket(s string) {
	for {
		if ticket > 0 {
			fmt.Println(s, ticket)
			ticket--
		} else {
			fmt.Println(s + "库存不足")
			return
		}
	}
}

为了解决这一问题我们引入锁

互斥锁

方法签名说明
Lockfunc (m *Mutex) Lock()加锁。若已有人持有则阻塞等待
Unlockfunc (m *Mutex) Unlock()释放锁。未加锁时调用会 panic
TryLockfunc (m *Mutex) TryLock() bool非阻塞尝试加锁。拿到返回 true,否则 false

对于TryLock,他不像Lock会一直等待抢占的资源被释放,而是当检测到资源被占用时放弃上锁。用于跳过任务/防止重复执行任务/测锁状态执行其他路径

利用互斥锁可以解决库存超卖问题:

var ticket = 10

var mu sync.Mutex

func main() {
	var wg sync.WaitGroup
	wg.Go(func() { saleTicket("sale1:") })
	wg.Go(func() { saleTicket("sale2:") })
	wg.Go(func() { saleTicket("sale3:") })

	wg.Wait()
	fmt.Println("final ticket:", ticket)
}

func saleTicket(s string) {
	for {
		mu.Lock()
		if ticket > 0 {
			time.Sleep(time.Duration(rand.N(1000)) * time.Microsecond) // 休眠期间被其他写成抢占修改,导致超卖
			runtime.Gosched()
			fmt.Println(s, ticket)
			ticket--
		} else {
			fmt.Println(s + "库存不足")
			mu.Unlock()
			return
		}
		mu.Unlock()
	}
}

读写锁

Mutex与RWMutex对比

特性MutexRWMutex
分组只有一种锁(互斥)读锁 + 写锁
读操作也要互斥(读也排队)读锁共享,多读者同时进
写操作互斥写锁独占,挡所有读+写
方法Lock/Unlock/TryLockRLock/RUnlock/Lock/Unlock/TryRLock/TryLock
适用读写频次相近/临界区短读远多于写
性能(读多)较差(读也排队)更好(读并发)
混用/漏解锁死锁漏 RUnlock、写饥饿、读写锁混用

注意:

  • 当A执行Lock时,B无法执行RLock,Lock
  • 当A执行RLock时,B可以RLock,不能Lock

由于读写锁中只有写锁能保证互斥,所以针对上述的超卖问题只有写锁有用,读锁没用
并且读写锁更适合运用在单独的业务查询,业务更新,读多写少的情况

channel

channel也可以实现类似于锁的效果

func main() {
	var wg sync.WaitGroup

	ch := make(chan struct{}, 1)
	ch <- struct{}{}
	wg.Go(func() { saleTicket("sale1:", ch) })
	wg.Go(func() { saleTicket("sale2:", ch) })
	wg.Go(func() { saleTicket("sale3:", ch) })

	wg.Wait()
	fmt.Println("final ticket:", ticket)
}

func saleTicket(s string, ch chan struct{}) {
	for {
		<-ch
		if ticket > 0 {
			fmt.Println(s, ticket)
			ticket--
			ch <- struct{}{}
		} else {
			fmt.Println(s + "库存不足")
			ch <- struct{}{}
			return
		}
	}
}


评论(0)

查看评论列表

暂无评论


发表评论

表情 颜文字

插入代码