生产者&消费者模型

概述
生产者消费者模型是多线程设计的经典模型,该模型被广泛的应用到各个系统的多线程/进程模型设计中。本文介绍了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包的使用时再进一步分析。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值