go快速上手:并发编程之sync.Cond

234 阅读4分钟

Go语言并发编程中的sync.Cond:条件变量的优雅使用

在Go语言的并发编程世界里,sync包提供了丰富的同步原语来帮助开发者处理并发任务中的同步与通信问题。其中,sync.Cond(条件变量)是一个相对复杂但功能强大的工具,它允许一个或多个goroutine在某个条件成立时继续执行。本文将深入探讨sync.Cond的工作原理、使用方法以及它在Go语言并发编程中的应用场景。

一、sync.Cond是什么?

sync.Cond是Go标准库sync包中的一个结构体,它封装了对条件变量的操作。条件变量通常与互斥锁(如sync.Mutex)一起使用,以确保在检查条件或修改条件时数据的同步访问。sync.Cond提供了WaitSignalBroadcast等方法,用于在条件不满足时阻塞goroutine,以及在条件满足时唤醒一个或多个等待的goroutine

二、sync.Cond的工作原理

sync.Cond的工作原理基于以下几个关键点:

  1. 互斥锁sync.Cond总是与一个互斥锁(通常是*sync.Mutex*sync.RWMutex)关联,以确保在检查或修改条件时的数据一致性。
  2. 等待队列sync.Cond内部维护了一个等待队列,用于存放因条件不满足而阻塞的goroutine
  3. 条件检查:在调用Wait方法之前,调用者通常会在互斥锁的保护下检查某个条件是否满足。
  4. 阻塞与唤醒:如果条件不满足,调用者会调用Wait方法释放互斥锁并进入等待状态。当条件变为满足时,其他goroutine可以通过调用SignalBroadcast方法来唤醒一个或所有等待的goroutine。被唤醒的goroutine会重新尝试获取互斥锁并检查条件。

三、sync.Cond的使用方法

要使用sync.Cond,你需要先创建一个与互斥锁关联的sync.Cond实例,并在需要等待条件满足的goroutine中调用其Wait方法。同时,在修改条件并希望唤醒等待的goroutine时,应调用SignalBroadcast方法。

3.1

以下是一个简单的使用示例:

package main

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

func main() {
    var mu sync.Mutex
    cond := sync.NewCond(&mu)
    var ready bool = false

    go func() {
        mu.Lock()
        defer mu.Unlock()
        for !ready {
            cond.Wait() // 等待条件满足
        }
        fmt.Println("Condition is true, proceeding...")
    }()

    time.Sleep(1 * time.Second) // 模拟条件准备时间

    mu.Lock()
    ready = true
    cond.Signal() // 唤醒一个等待的goroutine
    mu.Unlock()

    // 注意:在实际应用中,应确保在main函数结束前所有goroutine都已完成,否则程序可能会异常退出。
    // 这里为了简化示例,我们省略了等待goroutine完成的逻辑。
}

3.2 跟切片结合

使用channel

package main

import (
	"fmt"
	"sync"
)

type Item = int

type waiter struct {
	n int
	c chan []Item
}

type state struct {
	items []Item
	wait  []waiter
}

type Queue struct {
	s chan state
}

func NewQueue() *Queue {
	s := make(chan state, 1)
	s <- state{}
	return &Queue{s}
}

func (q *Queue) Put(item Item) {
	s := <-q.s
	s.items = append(s.items, item)
	for len(s.wait) > 0 {
		w := s.wait[0]
		if len(s.items) < w.n {
			break
		}
		w.c <- s.items[:w.n:w.n]
		s.items = s.items[w.n:]
		s.wait = s.wait[1:]
	}
	q.s <- s
}

func (q *Queue) GetMany(n int) []Item {
	s := <-q.s
	if len(s.wait) == 0 && len(s.items) >= n {
		items := s.items[:n:n]
		s.items = s.items[n:]
		q.s <- s
		return items
	}
	c := make(chan []Item)
	s.wait = append(s.wait, waiter{n, c})
	q.s <- s
	return <-c
}

func main() {
	q := NewQueue()
	var wg sync.WaitGroup
	for n := 10; n > 0; n-- {
		wg.Add(1)
		go func(n int) {
			items := q.GetMany(n)
			fmt.Printf("%2d: %2d\n", n, items)
			wg.Done()
		}(n)
	}
	for i := 0; i < 100; i++ {
		q.Put(i)
	}

	wg.Wait()
}

使用sync.Cond

package main

import (
	"fmt"
	"sync"
)

type Item = int

type Queue struct {
	items     []Item
	itemAdded sync.Cond
}

func NewQueue() *Queue {
	q := new(Queue)
	q.itemAdded.L = &sync.Mutex{} // 为Cond绑定锁
	return q
}

func (q *Queue) Put(item Item) {
	q.itemAdded.L.Lock()
	defer q.itemAdded.L.Unlock()
	q.items = append(q.items, item)
	q.itemAdded.Signal() // 当Queue中加入数据成功,调用Signal 发送通知
}

func (q *Queue) GetMany(n int) []Item {
	q.itemAdded.L.Lock()
	defer q.itemAdded.L.Unlock()
	for len(q.items) < n { // 等待Queue中有n个数据
		q.itemAdded.Wait() // 阻塞等待Signal 发送通知
	}
	items := q.items[:n:n]
	q.items = q.items[n:]
	return items
}

func main() {
	q := NewQueue()
	var wg sync.WaitGroup
	for n := 10; n > 0; n-- {
		wg.Add(1)
		go func(n int) {
			items := q.GetMany(n)
			fmt.Printf("%2d: %2d\n", n, items)
			wg.Done()
		}(n)
	}
	for i := 0; i < 100; i++ {
		q.Put(i)
	}

	wg.Wait()
}

3.3 与sync.WaitGroup结合

3.3.1

package main

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

func main() {
	var (
		locker sync.Mutex
		cond   = sync.NewCond(&locker)
		wg     sync.WaitGroup
	)
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(number int) {
			cond.L.Lock()
			defer cond.L.Unlock()
			cond.Wait()
			fmt.Printf("g %v ok~ \n", number)
			wg.Done()
		}(i)
	}
	for i := 0; i < 5; i++ {
		cond.Signal()
		//fmt.Println("Signal...", i)
		time.Sleep(time.Millisecond * 50)
	}
	time.Sleep(time.Millisecond * 50)
	cond.Broadcast()
	fmt.Println("Broadcast...")
	wg.Wait()
}

3.3.2

package main

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

var queue []struct{}

func main() {
	var wg sync.WaitGroup
	wg.Add(2)
	c := sync.NewCond(&sync.Mutex{})
	for i := 0; i < 2; i++ {
		go func(i int) {
			c.L.Lock()
			for len(queue) <= 0 {
				fmt.Println("goroutine" + strconv.Itoa(i) + "wait")
				c.Wait()
			}
			fmt.Println("goroutine"+strconv.Itoa(i), "pop data")
			queue = queue[1:]
			c.L.Unlock()
			wg.Done()
		}(i)
	}

	for i := 0; i < 2; i++ {
		time.Sleep(2 * time.Second)
		c.L.Lock()
		fmt.Println("main goroutine push data")
		queue = append(queue, struct{}{})
		c.Broadcast()
		fmt.Println("main goroutine broadcast")
		c.L.Unlock()
	}
	wg.Wait()

}

四、sync.Cond的注意事项

  1. 避免死锁:在调用Wait方法之前,必须确保已经加锁。Wait方法会释放锁并进入等待状态,但在被唤醒后,它会重新尝试获取锁。因此,在Wait方法之后,应立即检查条件是否满足,并在必要时重新加锁。
  2. 正确使用SignalBroadcastSignal方法仅唤醒等待队列中的一个goroutine(如果有的话),而Broadcast方法会唤醒所有等待的goroutine。根据实际需求选择合适的唤醒方式。
  3. 避免在Wait方法外部释放锁Wait方法内部会负责释放锁并在被唤醒后重新尝试获取锁。因此,在调用Wait之前加锁后,不要在Wait调用和条件检查之间释放锁。

五、sync.Cond的应用场景

sync.Cond适用于需要等待某个条件成立才能继续执行的场景,如生产者-消费者模型中的消费者等待队列中有元素可消费、线程池中的工作线程等待新的任务等。通过合理使用sync.Cond,可以编写出既高效又易于维护的并发代码。

六、结语

以上就是sync.Cond的基本用法。