go sync.Cond 条件变量

目录

1、数据结构

2、底层实现

3、条件判断

4、等待(Wait())

[5、通知(Signal/ Broadcast)](#5、通知(Signal/ Broadcast))

6、注意点

7、生产消费样例


1、数据结构

Go 复制代码
type Cond struct {
    L  sync.Locker       // 关联的锁,通常是 *sync.Mutex 或 *sync.RWMutex
    ch chan struct{}     // 用于协调 goroutine 的等待和通知的 channel
}

2、底层实现

go的条件变量用于协程间的同步,通常与互斥锁(sync.Mutex)或读写锁(sync.RWMutex)一起使用。当不满足某些条件时,调用wait方法,协程会被阻塞进入等待队列,等待条件满足。通常由其他协程调用signal方法发送通知来唤醒等待的协程,协程被重新调度,判断条件是否满足。

sync.Cond 在底层通过操作系统的线程同步原语(如条件变量和信号量)来实现。这些原语通常是由操作系统的线程库(例如 POSIX 线程库)提供的。Go 的运行时包装了这些原语,以便在 Go 的并发模型中使用。

sync.Cond 的数据结构有一个锁和一个通道组成:

  • L 是一个同步锁,可以是任何实现了 sync.Locker 接口的锁。用于保护临界区的访问,临界资源包括:判断条件、执行程序需要的共享数据。

  • ch 是一个chan struct{}类型的channel,Go 调度器会使用这个channel来阻塞和唤醒协程。

  • 阻塞的goroutine将会进入等待队列,被唤醒的goroutine会重新被调度。

3、条件判断

  • 获取锁:用于保护判断条件

  • 条件判断:判断条件是否满足,满足则向下执行,不满足则执行wait方法,释放锁,并阻塞当前协程,等待条件满足。当被唤醒时重新获取锁,并做条件判断。

  • 释放锁:执行完成,释放锁。

4、等待(Wait())

当协程调用Wait方法时,释放与条件变量关联的互斥锁,并阻塞协程使其进入等待状态。当其他 goroutine 发出信号通知它时,重新获取锁。

  • 释放锁Wait() 会先释放与条件变量相关的锁(L),这使得其他 goroutines 可以进入临界区并修改共享数据。

  • 阻塞当前协程 :当前 goroutine 会被挂起,通常是通过一个内部的 channel 来实现阻塞,类似于 channel 的阻塞机制。具体地,Go 的调度器会将该 goroutine 加入到一个等待队列,并让其挂起,直到条件变量被通知。

    <-ch // 阻塞当前 goroutine

  • 重新获取锁:当条件变量被通知时,当前 goroutine 会从挂起的状态中恢复执行,并重新获取锁,继续执行剩下的代码。

5、通知(Signal/ Broadcast)

当协程调用Signal方法时,它会唤醒至少一个等待在条件变量上的协程,使其重新被调度;而Broadcast方法则唤醒所有等待的协程。协程被唤醒后,会重新去获取锁,然后最好重新判断条件,然后向下执行。

Signal() :调用 Cond.Signal() 时,调度器会从等待队列中唤醒一个 goroutine。这通常通过给等待在条件变量上的 goroutine 发送一个信号来实现(通过 channel 的发送操作)。被唤醒的 goroutine 会重新获取锁,并继续执行。

Go 复制代码
// 唤醒一个 goroutine 
ch <- struct{}{}

Broadcast() :调用 Cond.Broadcast() 时,调度器会唤醒所有等待在条件变量上的 goroutine。所有被挂起的 goroutine 会在获得锁之后恢复执行。

Go 复制代码
// 唤醒所有 goroutines 
for len(ch) > 0 { 
    ch<-struct{}{} 
}

6、注意点

  1. 释放锁和通知的原子性 :当调用 Wait() 时,sync.Cond 会首先释放与其关联的锁,并将 goroutine 阻塞。这样可以确保其他 goroutine 可以继续执行,并对共享资源进行修改。

  2. 调度与恢复 :当一个等待的 goroutine 被唤醒时,调度器会将它的状态恢复为可执行状态,并将其重新调度。由于锁是由 sync.Locker(如 sync.Mutex)控制的,因此在被唤醒后,goroutine 会重新获取锁,然后继续执行。

  3. 避免虚假唤醒 :Go 的 sync.Cond 设计与其他语言的条件变量类似,会避免虚假唤醒(spurious wakeups)。即使没有调用 Signal()Broadcast(),goroutine 也可能在某些情况下被唤醒(例如操作系统的内部调度机制)。为了防止因虚假唤醒导致的错误,Go 推荐在 Wait() 时结合 for 循环和条件判断进行等待。

7、生产消费样例

Go 复制代码
package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	var mu sync.Mutex             // 用于保护共享数据的互斥锁
	cond := sync.NewCond(&mu)     // 创建一个新的条件变量
	buffer := []int{}             // 模拟缓冲区
	maxBufferSize := 5            // 缓冲区的最大容量

	// 生产者 goroutine
	go func() {
		for i := 0; i < 10; i++ {
			mu.Lock() // 加锁,确保同步
			// 如果缓冲区已满,等待消费者消费
			for len(buffer) == maxBufferSize {
				fmt.Println("Buffer is full, producer is waiting...")
				cond.Wait() // 阻塞直到消费者消费了数据
			}
			// 生产数据并放入缓冲区
			buffer = append(buffer, i)
			fmt.Printf("Produced: %d\n", i)
			cond.Signal() // 通知消费者可以消费数据
			mu.Unlock()   // 解锁
			time.Sleep(500 * time.Millisecond) // 模拟生产时间
		}
	}()

	// 消费者 goroutine
	go func() {
		for {
			mu.Lock() // 加锁,确保同步
			// 如果缓冲区为空,等待生产者生产
			for len(buffer) == 0 {
				fmt.Println("Buffer is empty, consumer is waiting...")
				cond.Wait() // 阻塞直到生产者生产了数据
			}
			// 消费数据
			item := buffer[0]
			buffer = buffer[1:]
			fmt.Printf("Consumed: %d\n", item)
			cond.Signal() // 通知生产者可以继续生产
			mu.Unlock()   // 解锁
			time.Sleep(1 * time.Second) // 模拟消费时间
		}
	}()

	// 等待程序结束
	time.Sleep(10 * time.Second)
}
相关推荐
小红帽2.04 小时前
从零构建一款开源在线客服系统:我的Go语言实战之旅
开发语言·golang·开源
007php0076 小时前
Go语言面试:传值与传引用的区别及选择指南
java·开发语言·后端·算法·面试·golang·xcode
q567315238 小时前
手把手教你用Go打造带可视化的网络爬虫
开发语言·爬虫·信息可视化·golang
戎码江湖9 小时前
使用CI/CD部署后端项目(gin)
ci/cd·golang·gin·后端自动部署项目·自动化部署项目
二哈不在线10 小时前
代码随想录二刷之“贪心算法”~GO
算法·贪心算法·golang
君万12 小时前
【LeetCode每日一题】94. 二叉树的中序遍历 104. 二叉树的最大深度
算法·leetcode·golang
Craze_rd15 小时前
服务 HTTP 转 SRPC 技术方案
网络·网络协议·http·rpc·golang
尘鹄18 小时前
go 初始化组件最佳实践
后端·设计模式·golang
墩墩分墩18 小时前
【Go语言入门教程】 Go语言的起源与技术特点:从诞生到现代编程利器(一)
开发语言·后端·golang·go
ERP老兵-冷溪虎山21 小时前
Python/JS/Go/Java同步学习(第三篇)四语言“切片“对照表: 财务“小南“纸切片术切凭证到崩溃(附源码/截图/参数表/避坑指南/老板沉默术)
java·javascript·python·golang·中医编程·四语言同步学习·职场生存指南