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)
}
相关推荐
Lewiis6 小时前
Go语言的错误处理机制
开发语言·后端·golang
benzun_yinzi8 小时前
go升级之后找不到goroot解决办法
golang
lars_lhuan9 小时前
Go Once
开发语言·golang
hongtianzai9 小时前
Go vs Java:终极性能对决
java·开发语言·golang
贺小涛9 小时前
Golang Gin框架核心原理与架构解析
架构·golang·gin
古城小栈1 天前
Go 底层代码的完整分类
开发语言·后端·golang
耳冉鹅1 天前
Go无锁共享内存环形缓冲区设计
开发语言·golang
fy121632 天前
GO 快速升级Go版本
开发语言·redis·golang
童话ing2 天前
【Golang】Golang Map数据结构底层原理
数据结构·golang·哈希算法
GDAL2 天前
go.mod 文件讲解
golang·go.mod