①开课前预习: GO实现生产者消费者模型 | 青训营笔记

215 阅读11分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第一篇笔记

在被卷的焦虑中我报名了字节青训营,在看到了大犇们在找队友的自我介绍中描绘的知识,我直接自惭形愧。于是决定奋发图强,不摸鱼的完成一套go技术栈的学习。

然而,我没有开发经验,肚子里只有一点底ACM铁牌的算法知识和后端开发的粗浅认知,我从何开始呢。

这时课程安排发下来了,咱第二节课就有讲并发编程,这不正巧学校开设操作系统课程,并且要求我们实现一个生产者消费者模型,这也是一个经典的并发场景,于是我打算利用Go实现这个实验课作业

语法基础?边写边看啦,又不是没学过C,看得懂就行,不会的递归百度啦

涉及的知识点

  • 操作系统:进/线程通信(信号量机制)

  • Go语法基础

Go为并发而生

问什么要讲这句话?为的是简要的阐述一下Go的优越性

如果你理解为什么要学Go而不是跟风学就不会问这个问题了(bushi)

我们为什么学习Go?是为了追求性能,因为:

摩尔定律已经被打破……我们没有办法在硬件上一直取得突破,我们需要提高软件的性能,或者说我们需要高性能的软件。

……Go于2009年发布,当时多核处理器已经上市。Go语言在多核并发上拥有原生的设计优势,Go语言从底层原生支持并发,无须第三方库、开发者的编程技巧和开发经验。

摘自李文周老师的博客

于是跟风学Go的我看完李老师的博客就知道为啥要学Go了

信号量(semaphore)

什么是信号量?信号量就是一个整型变量,其值就是空闲资源总数,再要复杂点就是带一个队列,用来存储被阻塞的进程,信号量同步机制中,一共有两个操作:P(wait)操作和V(signal)操作

(为啥是PV呢,因为发明这个的是个荷兰人所以用的荷兰语,有些学校上课用的PV,我们学校用的wait和signal)

简单说这俩也就是一个加一个减,为了形象的理解信号量机制,我来举个栗子:

社区的快递站因为疫情改变了存取方式,快递站设定了n个封闭的小隔间,不管是快递员还是收快递的客户都不能同时进入一个小隔间,快递的存储都要在小隔间内完成。快递员和取快递的客户进小隔间和出小隔间都要扫健康吗,若没有多余的小隔间,则得在快递站外排队完成。

如果要用一段伪代码来描述快递员和客户的行为,我们要怎么写呢?我会这么写:

func 快递员/客户:

    进入扫码
    
    存/取快递

    离开扫码

这个时候有的大聪明可能就会想问了,你扫个码就能进,小隔间里人挤人,这不等于没防疫么?

这位大聪明的想法确实有道理,但是如果我们站在快递员和客户的角度,他们会怎么想呢:

“这特么还小隔间呢,小黑屋还不错,我哪知道里面有没有人”

我们都知道,21世纪的年轻人都是高素质公民,一旦他们知道有人在里面,自然会自觉的排队,所以现在的当务之急就是要让外面的人知道,到底还剩多少个小隔间可以用来存取快递

收到了快递员和客户的抱怨之后,社区在快递站贴了一个新的二维码,扫码之后可以查看实时的隔间空闲情况和自己的排队情况。

这下就好办了,社区顺利的解决了快递存取的问题,那么,如何用伪代码描述二维码起的作用呢?我会这么写:

进入:

func 进入扫码:

    空闲隔间数--

    if 空闲隔间数<0:

        开始排队

离开:

func 离开扫码:

    空闲隔间数++

    if 空闲隔间数>=0:

        告诉排到最前面的同志去存/取快递            

在上面的例子中,每一个快递员和客户都是一个进程,其中快递员就是生产者,客户就是消费者,进入扫码就是wait操作,离开扫码就是signal操作,排的队就是一个阻塞队列,进程在进入时先扫码,有空间就进,没空间就排队,出来时释放一个空间,并且告诉下一个就绪的进程进入。这个例子就是一个记录型信号量的实现,那现在让我们来看看标准版的代码

信号量定义:

typedef struct{
    int value;
    QueueType qu;
}semaphore

main:

int main(){
    //init semaphore smph
    
    wait(smph);
    
    //do something
    
    signal(smph);
}

wait:

void wait(semaphore * s){
    s.value --;
    if(s.value < 0){
        block(s.qu);//add this process into s.qu
    }
}

signal:

void signal(semaphore * s){
    s.value ++;
    if(s.value >= 0){
        wakeup(s.qu);//remove this process from s.qu
    }
}

用我们老师的话总结一下,进/线程好比人,如果人与人之间相互了解熟悉,知晓状况,他们就不太可能发生争夺,而是会相互沟通解决问题,而这个相互沟通的过程,就叫进/线程通信

那么,你看明白了吗?

Go中的实现

温馨提示:以下内容属于现学现卖,如有错误请轻喷

回想一下我们上面的例子,我么需要一些什么样的数据结构/变量/功能/函数呢?

本文选取了一种相对简单友好的实现方式,使用通道 chan

Go 并发语法

协程 go

协程是 golang 并发的最小单元,类似于其他语言的线程,只不过线程的实现借助了操作系统的实现,每次线程的调度都是一次系统调用,需要从用户态切换到内核态,这是一项非常耗时的操作,因此一般的程序里面线程太多会导致大量的性能耗费在线程切换上。而在 golang 内部实现了这种调度,协程在这种调度下面的切换非常的轻量级,成百上千的协程跑在一个 golang 程序里面是很正常的事情

golang 为并发而生,启动一个协程的语法非常简单,使用 go 关键字即可

go func () {
    // do something
}()

通道 chan

通道可以理解为一个消息队列,生产者往队列里面放,消费者从队列里面取。通道可以使用 close 关闭

ic := make(chan int, 10)  // 申明一个通道
ic <- 10        // 往通道里面放
i := <- ic      // 从通道里面取

close(ic)       // 关闭通道

摘自golang 并发编程之生产者消费者 - SegmentFault

看完了代码的实例,让我们来实操一下罢!

其实蛮简单的,就按照伪码照着写就行

main.go

package main

import (
   "fmt"
   "math/rand"
   "time"
)

var smph chan int

func wait(smph chan int, id int) {
   smph <- id
   fmt.Printf("now %d get in\n", id)
}
func signal(smph chan int) {
   fmt.Printf("now %d get out\n", <-smph)
}
func Producer(id int, arr []int) { //快递小哥
   wait(smph, id)
   for i := 0; i < len(arr); i++ {
      fmt.Printf("producer %d store a package %d\n", id, arr[i])
   }
   signal(smph)
}
func Consumer(id int) { //客户
   wait(smph, id)
   fmt.Printf("consumer get a package %d\n", id)
   signal(smph)
}
func main() {
   rand.Seed(time.Now().Unix())
   //随机数种子
   smph = make(chan int, 5)
   //设定5个小隔间
   for i := 1; i <= 4; i++ {
      //fmt.Printf("Producer %d ", i)
      //四个快递员
      var custom []int
      tm := rand.Intn(20)
      //fmt.Printf("storing %d pakage(s)\n", tm)
      //每个快递员送tm个快递
      for j := 1; j <= tm; j++ {
         value := i*1000 + j
         //生成快递单号
         custom = append(custom, value)
      }
      //fmt.Printf("id=%v\n", custom)
      go Producer(i, custom)
      //快递员存放快递
      time.Sleep(time.Duration(1) * time.Second / 5)
      //存放过程等待
      for _, v := range custom {
         go Consumer(v)
         //给每个客户发短信来要他们取快递
      }
   }
   time.Sleep(time.Duration(5) * time.Second)
}

细心的大聪明们可能发现了,我的代码实现和之前的伪码比起来少了点东西

少了啥呢?阻塞队列不见了

但是这可不是我忘了写,原因在于:

Go实在是太厉害啦,当我使用通道chan来构建小隔间(缓冲区)的时候,Go已经帮我实现好了阻塞队列

当通道里的数据量已经达到设定的容量时,此时再往里发送数据会阻塞整个程序。

利用这个特性,可以用当他来当程序的锁。

程序输出

now 1 get in
producer 1 store a package 1001
producer 1 store a package 1002
producer 1 store a package 1003
producer 1 store a package 1004
producer 1 store a package 1005
producer 1 store a package 1006
producer 1 store a package 1007
producer 1 store a package 1008
producer 1 store a package 1009
now 1 get out
now 1002 get in
consumer get a package 1002
now 1001 get out
now 2 get in
producer 2 store a package 2001
producer 2 store a package 2002
now 1002 get out
now 1008 get in
consumer get a package 1008
now 1003 get out
now 1003 get in
consumer get a package 1003
now 2 get out
now 1001 get in
consumer get a package 1001
now 1007 get out
now 1007 get in
consumer get a package 1007
now 1008 get out
now 1009 get in
consumer get a package 1009
now 1009 get out
now 1005 get in
consumer get a package 1005
now 1005 get out
now 1004 get in
consumer get a package 1004
now 1006 get out
now 1006 get in
consumer get a package 1006
now 1004 get out
now 3 get in
producer 3 store a package 3001
producer 3 store a package 3002
producer 3 store a package 3003
producer 3 store a package 3004
producer 3 store a package 3005
producer 3 store a package 3006
producer 3 store a package 3007
producer 3 store a package 3008
producer 3 store a package 3009
producer 3 store a package 3010
producer 3 store a package 3011
now 3 get out
now 2002 get in
consumer get a package 2002
now 2001 get out
now 2001 get in
consumer get a package 2001
now 2002 get out
now 3001 get in
consumer get a package 3001
now 3001 get out
now 3004 get in
consumer get a package 3004
now 3002 get out
now 3003 get in
consumer get a package 3003
now 3003 get out
now 3002 get in
consumer get a package 3002
now 3004 get out
now 3006 get in
consumer get a package 3006
now 3006 get out
now 3005 get in
consumer get a package 3005
now 3005 get out
now 3007 get in
consumer get a package 3007
now 3007 get out
now 3008 get in
consumer get a package 3008
now 3008 get out
now 3009 get in
consumer get a package 3009
now 3009 get out
now 3010 get in
consumer get a package 3010
now 3010 get out
now 4 get in
producer 4 store a package 4001
producer 4 store a package 4002
producer 4 store a package 4003
producer 4 store a package 4004
producer 4 store a package 4005
producer 4 store a package 4006
producer 4 store a package 4007
producer 4 store a package 4008
producer 4 store a package 4009
producer 4 store a package 4010
now 3011 get out
now 3011 get in
consumer get a package 3011
now 4 get out
now 4001 get in
consumer get a package 4001
now 4010 get out
now 4003 get in
consumer get a package 4003
now 4001 get out
now 4002 get in
consumer get a package 4002
now 4002 get out
now 4010 get in
consumer get a package 4010
now 4003 get out
now 4004 get in
consumer get a package 4004
now 4004 get out
now 4008 get in
consumer get a package 4008
now 4008 get out
now 4009 get in
consumer get a package 4009
now 4009 get out
now 4005 get in
consumer get a package 4005
now 4005 get out
now 4007 get in
consumer get a package 4007
now 4006 get out
now 4006 get in
consumer get a package 4006
now 4007 get out

这么一看啊,代码执行效果还是不错滴

那这篇文章就写到这里,如有错误,欢迎批评!