跳转到内容

Go 条件变量

来自代码酷
Admin留言 | 贡献2025年4月29日 (二) 04:41的版本 (Page creation by admin bot)

(差异) ←上一版本 | 已核准修订 (差异) | 最后版本 (差异) | 下一版本→ (差异)

Go条件变量[编辑 | 编辑源代码]

条件变量(Condition Variable)是Go语言并发编程中用于协调多个goroutine之间同步的重要机制。它通常与互斥锁(Mutex)配合使用,允许goroutine在特定条件不满足时进入等待状态,并在条件可能满足时被唤醒。条件变量提供了一种高效的方式来实现复杂的线程间通信模式。

概述[编辑 | 编辑源代码]

条件变量的核心思想是:当某个共享数据的条件不满足时,goroutine可以主动释放锁并进入等待状态;当其他goroutine修改了共享数据并使得条件可能满足时,它可以通知等待的goroutine重新检查条件。

在Go中,条件变量通过sync.Cond类型实现,它包含三个主要方法:

  • Wait() - 释放锁并挂起goroutine
  • Signal() - 唤醒一个等待的goroutine
  • Broadcast() - 唤醒所有等待的goroutine

基本用法[编辑 | 编辑源代码]

以下是条件变量的基本使用模式:

package main

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

func main() {
	var m sync.Mutex
	c := sync.NewCond(&m)
	queue := make([]interface{}, 0, 10)

	removeFromQueue := func(delay time.Duration) {
		time.Sleep(delay)
		c.L.Lock()
		queue = queue[1:]
		fmt.Println("Removed from queue")
		c.L.Unlock()
		c.Signal() // 通知等待的goroutine条件可能已改变
	}

	for i := 0; i < 10; i++ {
		c.L.Lock()
		for len(queue) == 2 { // 条件检查
			c.Wait() // 等待条件满足
		}
		fmt.Println("Adding to queue")
		queue = append(queue, struct{}{})
		go removeFromQueue(1 * time.Second)
		c.L.Unlock()
	}
}

输出示例:

Adding to queue
Adding to queue
Removed from queue
Adding to queue
Removed from queue
Adding to queue
...

这个示例展示了典型的生产者-消费者模式,其中生产者(主goroutine)在队列满时等待,消费者(removeFromQueue goroutine)在移除元素后发出信号。

工作原理[编辑 | 编辑源代码]

条件变量的工作流程可以用以下状态图表示:

stateDiagram [*] --> 获取锁 获取锁 --> 检查条件 检查条件 --> 条件满足: 是 检查条件 --> 等待: 否 等待 --> 被唤醒 被唤醒 --> 检查条件 条件满足 --> 执行业务逻辑 执行业务逻辑 --> 释放锁 释放锁 --> [*]

关键点: 1. 条件检查必须在持有锁的情况下进行 2. Wait()会自动释放锁并在返回前重新获取锁 3. 条件检查应该使用循环而不是if语句,以防止虚假唤醒

实际应用案例[编辑 | 编辑源代码]

工作池模式[编辑 | 编辑源代码]

条件变量非常适合实现工作池,其中worker goroutine在没有任务时等待,在有新任务时被唤醒。

type WorkerPool struct {
	mu    sync.Mutex
	cond  *sync.Cond
	tasks []Task
}

func (p *WorkerPool) AddTask(task Task) {
	p.mu.Lock()
	p.tasks = append(p.tasks, task)
	p.mu.Unlock()
	p.cond.Broadcast() // 通知所有worker
}

func (p *WorkerPool) Worker() {
	for {
		p.mu.Lock()
		for len(p.tasks) ==  {
			p.cond.Wait()
		}
		task := p.tasks[0]
		p.tasks = p.tasks[1:]
		p.mu.Unlock()
		task.Execute()
	}
}

有限资源分配[编辑 | 编辑源代码]

当需要限制同时访问某种资源的goroutine数量时,条件变量也非常有用:

type ResourcePool struct {
	mu      sync.Mutex
	cond    *sync.Cond
	inUse   int
	maxSize int
}

func (p *ResourcePool) Acquire() {
	p.mu.Lock()
	defer p.mu.Unlock()
	
	for p.inUse >= p.maxSize {
		p.cond.Wait()
	}
	p.inUse++
}

func (p *ResourcePool) Release() {
	p.mu.Lock()
	p.inUse--
	p.mu.Unlock()
	p.cond.Signal()
}

最佳实践[编辑 | 编辑源代码]

1. 总是使用循环检查条件:防止虚假唤醒

   c.L.Lock()
   for !condition {
       c.Wait()
   }
   c.L.Unlock()

2. 选择合适的通知机制

  * 使用Signal()当只有一个goroutine需要被唤醒时
  * 使用Broadcast()当多个goroutine可能满足条件时

3. 避免锁竞争:在调用Signal()Broadcast()前释放锁

4. 考虑上下文:在长时间等待时,考虑使用context.Context实现超时

数学原理[编辑 | 编辑源代码]

条件变量的正确性可以部分通过Hoare逻辑来验证。对于条件变量操作,有以下不变式:

{PC}Wait(){P} {P}Signal(){C}

其中:

  • P是保护条件变量的不变式
  • C是等待的条件

常见错误[编辑 | 编辑源代码]

1. 不检查条件直接等待:这可能导致丢失唤醒或无限等待 2. 在持有锁时执行耗时操作:这会降低并发性能 3. 忘记调用Signal/Broadcast:导致goroutine永远等待 4. 错误的条件检查顺序:应该在持有锁的情况下检查条件

性能考虑[编辑 | 编辑源代码]

条件变量相比通道(channel)在某些场景下性能更好,特别是:

  • 需要频繁通知多个等待者时
  • 需要细粒度控制锁时
  • 实现复杂同步模式时

然而,对于简单场景,通道通常是更安全的选择。

总结[编辑 | 编辑源代码]

Go的条件变量提供了强大的goroutine同步机制,特别适合实现复杂的等待/通知模式。正确使用时,它可以构建高效且正确的并发程序。关键是要记住:

  • 总是使用循环检查条件
  • 确保在正确的时机获取和释放锁
  • 选择合适的通知方式
  • 考虑使用更高级的抽象(如channel)如果它们更适合你的场景

通过掌握条件变量,你可以解决Go并发编程中的许多复杂同步问题。