概述
生产者消费者模型是多线程设计的经典模型,该模型被广泛的应用到各个系统的多线程/进程模型设计中。本文介绍了Go语言中channel的特性,并通过Go语言实现了两个生产者消费者模型。
channel的一些特性
在Go中channel是非常重要的协程通信的手段,channel是双向的通道,通过channel可以实现协程间数据的传递,通过channel也可以实现协程间的同步(后面会有介绍)。本文介绍的生产者消费者模型主要用到了channel的以下特性:任意时刻只能有一个协程能够对channel中某一个item进行访问。
单生产者单消费者模型
把生产者和消费者都放到一个无线循环中,这个和我们的服务器端的任务处理非常相似。生产者不断的向channel中放入数据,而消费者不断的从channel中取出数据,并对数据进行处理(打印)。由于生产者的协程不会退出,所以channel的写入会永久存在,这样当channel中没有放入数据时,消费者端将会阻塞,等待生产者端放入数据。
代码的实现如下:
package main
import (
"fmt"
"time"
)
var ch1 chan int = make(chan int)
var bufChan chan int = make(chan int, 1000)
var msgChan chan int = make(chan int)
func sum(a int, b int) {
ch1 <- a + b
}
// write data to channel
func writer(max int) {
for {
for i := 0; i < max; i++ { // 简单的向channel中放入一个整数
bufChan <- i
time.Sleep(1 * time.Millisecond) //控制放入的频率
}
}
}
// read data fro m channel
func reader(max int) {
for {
r := <-bufChan
fmt.Printf("read value: %d\n", r)
}
// 通知主线程,工作结束了,这一步可以省略
msgChan <- 1
}
func testWriterAndReader(max int) {
go writer(max)
go reader(max)
// writer 和reader的任务结束了,主线程会得到通知
res := <-msgChan
fmt.Printf("task is done: value=%d\n", res)
}
func main() {
testWriterAndReader(100)
}
多生产者消费者模型
我们可以利用channel在某个时间点只能有一个协程能够访问其中的某一个数据,的特性来实现生产者消费者模型。由于channel具有这样的特性,我们在放数据和消费数据时可以不需要加锁。
```go
package main
import (
"time"
"fmt"
"os"
)
var ch1 chan int = make(chan int)
var bufChan chan int = make(chan int, 1000)
var msgChan chan string = make(chan string)
func sum(a int, b int) {
ch1 <- a + b
}
// write data to channel
func writer(max int) {
for {
for i := 0; i < max; i++ {
bufChan <- i
fmt.Fprintf(os.Stderr, "%v write: %d\n", os.Getpid(), i)
time.Sleep(10 * time.Millisecond)
}
}
}
// read data fro m channel
func reader(name string) {
for {
r := <-bufChan
fmt.Printf("%s read value: %d\n", name, r)
}
msgChan <- name
}
func testWriterAndReader(max int) {
// 开启多个writer的goroutine,不断地向channel中写入数据
go writer(max)
go writer(max)
// 开启多个reader的goroutine,不断的从channel中读取数据,并处理数据
go reader("read1")
go reader("read2")
go reader("read3")
// 获取三个reader的任务完成状态
name1 := <-msgChan
name2 := <-msgChan
name3 := <-msgChan
fmt.Println("%s,%s,%s: All is done!!", name1, name2, name3)
}
func main() {
testWriterAndReader(100)
}
//输出结果
read3 read value: 0
80731 write: 0
80731 write: 0
read1 read value: 0
80731 write: 1
read2 read value: 1
80731 write: 1
read3 read value: 1
80731 write: 2
read2 read value: 2
80731 write: 2
... ...
总结
本文通过channel实现了经典的生产者和消费者模型,利用了channel的特性。但要注意,当消费者的速度小于生产者时,channel就有可能产生拥塞,导致占用内存增加,所以,在实际场景中需要考虑channel的缓冲区的大小。设置了channel的大小,当生产的数据大于channel的容量时,生产者将会阻塞,这些问题都是要在实际场景中需要考虑的。
一个解决办法就是使用一个固定的数组或切片作为环形缓冲区,而非channel,通过Sync包的机制来进行同步,实现生产者消费者模型,这样可以避免由于channel满而导致消费者端阻塞。但,对于环形缓冲区而言,可能会覆盖老的数据,同样需要考虑具体的使用场景。关于环形缓冲区的原理和实现,在分析Sync包的使用时再进一步分析。