生产者消费者模型分析

操作系统中的经典模型,由若干个消费者和生产者,消费者消耗系统资源,生产者创造系统资源,资源的数量要保持在一个合理范围(小于数量上限,大约0)。而消费者和生产者是通过并发或并行方式访问系统资源的,需要保持资源的原子操作。
其实就是生产者线程增加资源数,如果资源数大于最大值则生产者线程挂起等待,当收到消费者线程的通知后继续生产。
消费者线程减少资源数,如果资源数为0,则消费者线程挂起,等待生产者通知后继续生产。
将该模型提炼成伪代码如下:

func consume(){
Lock()
if count <= 0
挂起等待(解锁,并等待资源数大于0)
收到系统通知资源数大约0,抢占加锁
count--
如果当前资源数由最大值变少则通知生产者生产
ULock()
} func produce(){
Lock()
if count >= 最大值
挂起等待(解锁,并等待资源数小于最大值)
收到系统通知资源小于最大值,抢占加锁
count++
如果当前资源数由最小值0增加则通知消费者可以消耗
ULock()
}

  

consume()消耗资源,produce()生产资源,之前实现过C版本的该模型

http://www.limerence2017.com/2017/08/08/pthreadwait/
C方式实现的是抢占式的,线程切换开销较大。下面给出golang协程方式的实现。

先实现资源的互斥访问

对于资源的互斥访问,其他语言提供了线程锁,golang也有线程锁,当然可以通过channel实现,这里我给出加锁访问资源的方式,因为channel内部也是通过加锁实现的,而且我习惯用channel做协程通信,对于共享资源的控制习惯用锁来控制,也比较高效。

先定义几个全局变量

const (
PRODUCER_MAX = 5
CONSUMER_MAX = 2
PRODUCT_MAX = 20
) var productcount = 0
var lock sync.Mutex
var wgrp sync.WaitGroup

  

productcount为资源的数量,需要互斥处理。

wgrp主要是主协程用来等待其他协程退出。
PRODUCT_MAX 表示资源的上限,达到该值,生产者停止生产。
PRODUCER_MAX 表示生产者协程数量
CONSUMER_MAX 表示消费者协程数量
我们实现生产者代码

//生产者
func Produce(index int, wgrp *sync.WaitGroup) {
defer func() {
if err := recover(); err != nil {
fmt.Println("Producer ", index, " panic")
}
wgrp.Done()
}() for {
time.Sleep(time.Second)
lock.Lock()
fmt.Println("Producer ", index, " begin produce")
if productcount >= PRODUCT_MAX {
fmt.Println("Products are full")
lock.Unlock()
return
}
productcount++
fmt.Println("Products count is ", productcount)
lock.Unlock()
}
}

defer 的匿名函数主要是用来回收资源,不是重点

for循环内部生产者循环增加资源,为保证productcount的互斥访问,我们加了锁。
当productcount达到上限后解锁并返回,否则就增加数量,然后释放锁。
同样的道理我们实现了消费者

func Consume(index int, wgrp *sync.WaitGroup) {
defer func() {
if err := recover(); err != nil {
fmt.Println("Consumer ", index, " panic")
}
wgrp.Done()
}() for {
time.Sleep(time.Second)
lock.Lock()
fmt.Println("Consumer ", index, " begin consume")
if productcount <= 0 {
fmt.Println("Products are empty")
lock.Unlock()
return
}
productcount--
fmt.Println("Products count is ", productcount)
lock.Unlock()
}
}

  

消费者加锁减少productcount数量,当productcount为0,则解锁并返回。

然后我们实现主函数

func main() {
wgrp.Add(PRODUCER_MAX + CONSUMER_MAX)
for i := 0; i < PRODUCER_MAX; i++ {
go Produce(i, &wgrp)
} for i := 0; i < CONSUMER_MAX; i++ {
go Consume(i, &wgrp)
}
wgrp.Wait()
}

我们创建了若干生产者和消费者,主协程通过wgrp等待其他协程退出。

我们看下效果

可以看出并发的访问实现了,但是并没有实现条件等待和控制,比如当数量上限后其他生产者也可以访问。
接下来我们实现的是当数量上限是生产者挂起等待,直到消费者通知其生产。数量为0时消费者挂起,
等待生产者激活。也就是条件等待和异步协同。

实现条件等待和异步协同

协程之间的同步和等待可以使用channel,我们增加了两个全局非缓冲channel

var produce_wait chan struct{}
var consume_wait chan struct{}

produce_wait 用来控制生产者阻塞等待

consume_wait 用来控制消费者阻塞等待

我们修改下生产者

//生产者
func Produce(index int, wgrp *sync.WaitGroup) {
defer func() {
if err := recover(); err != nil {
fmt.Println("Producer ", index, " panic")
}
wgrp.Done()
}() for {
time.Sleep(time.Second)
lock.Lock()
fmt.Println("Producer ", index, " begin produce")
if productcount >= PRODUCT_MAX {
fmt.Println("Products are full")
lock.Unlock()
//产品满了,生产者wait
<-produce_wait
continue
}
lastcount := productcount
productcount++
fmt.Println("Products count is ", productcount)
lock.Unlock()
//产品数由0到1,激活消费者
if lastcount == 0 {
var consumActive struct{}
consume_wait <- consumActive
} }
}

在18行增加了<-produce_wait,这样生产者会挂起,等待消费者向produce_wait写入,从而得到激活。

另外26行增加了判断,当资源数由0到1时,激活消费者。
同样消费者实现类似

//消费者
func Consume(index int, wgrp *sync.WaitGroup) {
defer func() {
if err := recover(); err != nil {
fmt.Println("Consumer ", index, " panic")
}
wgrp.Done()
}() for {
time.Sleep(time.Second)
lock.Lock()
fmt.Println("Consumer ", index, " begin consume")
if productcount <= 0 {
fmt.Println("Products are empty")
lock.Unlock()
//产品空了,消费者等待
<-consume_wait
continue
}
lastcount := productcount
productcount--
fmt.Println("Products count is ", productcount)
lock.Unlock()
//产品数由PRODUCT_MAX变少,激活生产者
if lastcount == PRODUCT_MAX {
var productActive struct{}
produce_wait <- productActive
} }
}

这里我们要有并发的思想,考虑这样一个场景,当前产品数达到上限,Produce运行完16行,刚刚解锁,还没来得及运行18行挂起,

Consume抢占到锁正常运行消耗资源,运行到28行,优先对produce_wait写入,此时该消费者挂起,生产者收到信号后,
他们都会继续执行。
我们完善下main函数

func main() {
wgrp.Add(PRODUCER_MAX + CONSUMER_MAX)
produce_wait = make(chan struct{})
consume_wait = make(chan struct{})
for i := 0; i < CONSUMER_MAX; i++ {
go Consume(i, &wgrp)
}
for i := 0; i < PRODUCER_MAX; i++ {
go Produce(i, &wgrp)
} wgrp.Wait()
}

执行golang的锁检测并运行

go run -race main.go
可以看到是可以正常运行的

我们继续用并发思想分析,我们实现了基本功能,但是有个瑕疵,我们的生产者协程较多,比如生产者协程1判断生产上限在18行挂起,其他生产者如果抢占锁后进入生产判断数量上限,也会在18行挂起,由于我们的produce_wait是非缓冲的,那么当消费者来激活时,只有一个生产者被激活,另一个一直挂着,等到消费者激活才能继续生产。这么做在一定程度限制了生产者,我们可以通过引入两个bool变量通知其他协程睡眠,避免此问题。

增加bool变量实现休眠

我们可以引入两个bool变量

var stopProduce = false
var stopConsume = false

当资源达到上限或下限时,挂起单个协程,通过这两个变量休眠同类协程。

由于golang没有提供给我们休眠的api,我们就让同类型的协程sleep一会,这样也是可以提高模型并发的。
改进的生产者

//生产者
func Produce(index int, wgrp *sync.WaitGroup) {
defer func() {
if err := recover(); err != nil {
fmt.Println("Producer ", index, " panic")
}
wgrp.Done()
}() for {
time.Sleep(time.Second)
lock.Lock()
if stopProduce {
fmt.Println("Producer ", index, " stop produce, sleep 5 seconds")
lock.Unlock()
time.Sleep(time.Second * 5)
continue
}
fmt.Println("Producer ", index, " begin produce")
if productcount >= PRODUCT_MAX {
fmt.Println("Products are full")
stopProduce = true
lock.Unlock()
//产品满了,生产者wait
<-produce_wait
lock.Lock()
stopProduce = false
lock.Unlock()
continue
}
productcount++
fmt.Println("Products count is ", productcount)
if stopConsume {
var consumActive struct{}
consume_wait <- consumActive
}
lock.Unlock()
}
}

我们在22行设置了stopProduce为true,然后在25行挂起了该协程,其他生产者协程发现stopProduce为true,则睡眠5秒。

此办法保证了资源数临界值后仅有单个协程挂起,不会影响到其他同类协程。
同样实现消费者,这里不做赘述。
考虑这样一个场景,如果在生产者设置bool解锁后,其他消费者抢占锁后为了激活生产者,优先写入信道produce_wait,
此时生产者还没有从produce_wait读取,也不会有问题,毕竟生产者迟早要读取。
接下来我们测试下

可以看到当生产者1生产数上限后,其他生产者会进入休眠。当消费者激活后,生产者继续生产,其他生产者休眠后同样可以生产。
提高了并发效率。

源码下载

完整版源码地址
https://github.com/secondtonone1/golang-/tree/master/producerconsumer

感谢关注公众号

golang实现生产者消费者模型的更多相关文章

  1. golang的生产者消费者模型示例

    package main import "fmt" func Producer(ch chan int) { for i := 1; i <= 10; i++ { ch &l ...

  2. Golang 入门系列(十七)几个常见的并发模型——生产者消费者模型

    前面已经讲过很多Golang系列知识,包括并发,锁等内容,感兴趣的可以看看以前的文章,https://www.cnblogs.com/zhangweizhong/category/1275863.ht ...

  3. 生产者消费者模型及Golang简单实现

    简介:介绍生产者消费者模型,及go简单实现的demo. 一.生产者消费者模型 生产者消费者模型:某个模块(函数等〉负责产生数据,这些数据由另一个模块来负责处理(此处的模块是广义的,可以是类.函数.协程 ...

  4. 多道技术 进程 线程 协程 GIL锁 同步异步 高并发的解决方案 生产者消费者模型

    本文基本内容 多道技术 进程 线程 协程 并发 多线程 多进程 线程池 进程池 GIL锁 互斥锁 网络IO 同步 异步等 实现高并发的几种方式 协程:单线程实现并发 一 多道技术 产生背景 所有程序串 ...

  5. 【Windows】用信号量实现生产者-消费者模型

    线程并发的生产者-消费者模型: 1.两个进程对同一个内存资源进行操作,一个是生产者,一个是消费者. 2.生产者往共享内存资源填充数据,如果区域满,则等待消费者消费数据. 3.消费者从共享内存资源取数据 ...

  6. 第23章 java线程通信——生产者/消费者模型案例

    第23章 java线程通信--生产者/消费者模型案例 1.案例: package com.rocco; /** * 生产者消费者问题,涉及到几个类 * 第一,这个问题本身就是一个类,即主类 * 第二, ...

  7. Java里的生产者-消费者模型(Producer and Consumer Pattern in Java)

    生产者-消费者模型是多线程问题里面的经典问题,也是面试的常见问题.有如下几个常见的实现方法: 1. wait()/notify() 2. lock & condition 3. Blockin ...

  8. Java多线程15:Queue、BlockingQueue以及利用BlockingQueue实现生产者/消费者模型

    Queue是什么 队列,是一种数据结构.除了优先级队列和LIFO队列外,队列都是以FIFO(先进先出)的方式对各个元素进行排序的.无论使用哪种排序方式,队列的头都是调用remove()或poll()移 ...

  9. Java多线程14:生产者/消费者模型

    什么是生产者/消费者模型 一种重要的模型,基于等待/通知机制.生产者/消费者模型描述的是有一块缓冲区作为仓库,生产者可将产品放入仓库,消费者可以从仓库中取出产品,生产者/消费者模型关注的是以下几个点: ...

随机推荐

  1. Training #2 cell battle (BFS)

    Constraints: 1 <= R, C <= 500 1 <= T <= 5 Sample Input: 5 3 5 ##### a...b ##### 3 4 #### ...

  2. 编译teamtalk遇到的问题

    一.编译log4cxx遇到的问题 1.error: narrowing conversion 这是在gcc-6下面一个官方的错误 解决方法 https://issues.apache.org/jira ...

  3. CSP-S2019 退役记/赛后总结

    真就退役了呗. 作为一名非常失败的OIer,开了一个非常失败的blog,一直想在赛后写点什么,做点什么,总结些什么.自csp结束以来,徘徊了半个月,今夜里终于还是起笔了. 因为从来没写过这种玩意,不妨 ...

  4. hdfs冷热数据分层存储

    hdfs如何让某些数据查询快,某些数据查询慢? hdfs冷热数据分层存储 本质: 不同路径制定不同的存储策略. hdfs存储策略 hdfs的存储策略 依赖于底层的存储介质. hdfs支持的存储介质: ...

  5. C# ado.net 操作(一)

    简单的增删改查 class Program { private static string constr = "server=.;database=northwnd;integrated s ...

  6. CodeForces 839C - Journey | Codeforces Round #428 (Div. 2)

    起初误以为到每个叶子的概率一样于是.... /* CodeForces 839C - Journey [ DFS,期望 ] | Codeforces Round #428 (Div. 2) */ #i ...

  7. 【原创】洛谷 LUOGU P3373 【模板】线段树2

    P3373 [模板]线段树 2 题目描述 如题,已知一个数列,你需要进行下面两种操作: 1.将某区间每一个数加上x 2.将某区间每一个数乘上x 3.求出某区间每一个数的和 输入输出格式 输入格式: 第 ...

  8. 【线性代数】2-7:转置与变换(Transposes and Permutation)

    title: [线性代数]2-7:转置与变换(Transposes and Permutation) toc: true categories: Mathematic Linear Algebra d ...

  9. SSH 中文乱码解决

    在终端执行命令:export LC_ALL=zh_CN.GB2312;export LANG=zh_CN.GB2312是最有效的. 这种方法是临时的,只对当前SSH客户端有效,重启后依然乱码. 1.不 ...

  10. Django基础之render()

    结合一个给定的模板和一个给定的上下文字典, 并返回一个渲染后的HttpResponse对象. 参数: request: 用于生成响应的请求对象 template_name: 要使用的模板的完整名称, ...