• Golang中的channel分析


    一、channel

    1、实现

    使用ch := make(chan int, 5)创建一个有缓冲的channel之后,ch变成函数栈帧上的一个指针,指向堆上的实际hcann数据结构。

    channel往往用于协程间的并发访问,所以要有一把锁锁住整个数据结构。

    对于上述有缓冲channel需要知道的信息有:
    1、已经存储的元素
    2、最多存储的元素
    3、每个元素占多大空间

    所以channel缓冲区用一个数组实现。并且有缓冲区支持交替读写数据,因此需要两个值记录读写下标的位置。

    当读和写不能立即完成。要让当前channel的协程等待,待到可以读写的时候,要能立即唤醒等待的协程。所以要有两个等待队列,分别对应读写操作,也就是发送和接受队列。

    另外channel还有记录channel本身是否关闭的状态。因此channel底层的数据结构hchan结构体应该包括:类型信息,元素占用空间,元素大小,容量,发送和接受的下标值,两个等待队列,锁以及关闭状态等信息。

    2、发送数据

    创建ch := make(chan int, 5)则会创建一个大小为5的缓冲区。协程g1持续向缓冲区写入数据比如:

    ch <- 1
    ch <- 2
    ch <- 3
    ch <- 4
    ch <- 5
    ch <- 6
    

    过程如图:

    最后发送到6次数据,但是缓冲区容量只有5,缓冲区的写下标sendx会回到缓冲区首部读下标recvx的位置0,第六个元素就无处可放。发送数的协程g1就会进入ch的发送等待队列sendq中。如下图

    队列是基于链表实现,生成链表节点sudogo加入链表,节点信息包括:哪个协程在等待,等待的是哪个channel,等待发送的数据在哪等信息。
    当有新的协程从缓冲区读取数据,读下标会前移,空出了新的缓冲区就会唤醒加入等待队列的协程g1,g1退出队列执行ch <- 6操作,把数据写入缓冲区0号位置,所以其实channel的缓冲区是一个环形缓冲区

    所以对于写操作时候发送数据的协程不阻塞情况有:
    1、缓冲区还有空闲位置
    2、有协程在等待从缓冲区接收数据

    对于写操作时候发送数据的协程阻塞情况有:
    1、channel为nil
    2、无缓冲区且没有协程等待接收数据
    3、有缓冲区,但是已用尽

    为了不阻塞可以使用select机制,代码如下:

    select {
        case ch <- 10;
            ...
        default:
            ...
    }
    

    进入select,如果检测到channel可以发送数据,执行case分支,如果不能发送数据会产生阻塞,则会进入default分支。

    3、接收数据

    接收数据的写法更多
    1、<- ch:会将读出的结果丢弃
    2、 v := <- ch:结果赋值为变量v
    3、v, ok := <- ch: ok为false时候,表示ch关闭,v为channel元素类型的0值

    只有在缓冲区有数据和有协程往缓冲区发送数据才不会阻塞。如果出现以下情况则会读协程发生阻塞:
    1、ch为nil
    2、无缓冲也无发送协程
    3、有缓冲,缓冲区无数据

    为了不阻塞也可以使用select机制。

    二、多路select

    1、select实现

    以上读写操作中的select中,只有一个case分支,只针对一个channel。多路select指的就是存在两个及以上的case分支。每个分支可以使一个channel的发送或者接受操作。比如

    var a, b int
    b = 10
    select {
        case a = <- ch1;
        case ch2 <- b;
        default;
    }
    

    把执行上述多路select的协程称为g1,编译器会将多路select转换为对底层runtime.selectgo函数的调用,函数调用中有很多参数,比如数组cas0, 数组order0以及其他参数。

    • 数组cas0:数组中存放select中的所有case分支,send在前,recv在后。
    • order0数组:指向uint16类型的数组,数组容量为cas0的两倍,被用作两个数组。用来保存对case内channel轮训的顺以及加锁的顺序。轮训需要乱序保证公平性,确定加锁顺序避免死锁。
    • 其他参数:包括发送分支数目nsends以及接受分支数目nrecvs,以及记录select是否需要阻塞的值block,如果有default则不会被阻塞,没有则会被阻塞。
    • 函数返回值:另外函数调用也有返回值,第一个返回值为int类型对哪一个case分支被执行,如果执行default分支,则返回-1。第二个返回值为bool类型用于执行channel分支的操作时候,表示分支channel返回真实数据还是channel关闭。

    2、多路select处理逻辑

    按序加锁+乱序轮训:
    多路select需要进行轮训确定哪个case分支可以操作,轮训前需要先按照顺序对case中的所有channel有序加锁。然后按照乱序的轮询顺序检查channel的等待队列和缓冲区。

    挂起等待+ 按序解锁:
    假如检查到ch1,如果ch1中的缓冲区有数据可读,则进入对应case分支,当前协程g1拷贝数据开始执行。
    假如所有channel都无法执行,就把当前协程加入到select中所有case中channel的对应发送或者接受队列中,阻塞等待。然后g1会挂起,按序解锁所有channel对应的锁。

    按序加锁+离开队列+上锁+返回:
    假如接下来有数据可用,g1的就会被唤醒执行对应分支。执行之后,将自己再次按序加锁,接着从对应的队列中将自己移除,再次对channel上锁后返回。

    三、应用及问题

    1、生产者消费者模型

    channel可以用来作为两个协程通信,常用的应用比如生产者消费者模型,上游的生产者生产数据写入缓冲区,下游的消费者从缓冲区中读取数据,这个缓冲区可以用channel实现。从而解决了并发,解耦和缓存等问题。
    比如实际应用中,同时访问同一个公共区域,同时进行不同的操作。都可以划分为生产者消费者模型,比如订单系统。很多用户的订单下达之后,放入缓冲区或者队列中,然后系统从缓冲区中去读来真正处理。系统不必开辟多个线程来对应处理多个用户的订单,减少系统并发的负担。通过生产者消费者模式,将订单系统与仓库管理系统隔离开,且用户可以随时下单(生产数据)。如果订单系统直接调用仓库系统,那么用户单击下订单按钮后,要等到仓库系统的结果返回。这样速度会很慢。
    也就是:用户变成了生产者,处理订单管理系统变成了消费者。

    相关链接:生产者消费者模型及go实现

    2、内存泄漏

    在一.2,一.3中的对缓冲读写操作都需要协程执行,但是如果协程创建之后,因为缓冲区空或者死锁等情况,协程阻塞持续不释放,go很难判断这些阻塞是人为的还是出现故障,也难判断这些协程的阻塞是暂时的还是永久的,所以不会主动去释放阻塞的协程而造成永久阻塞,而程序还在不断创建新的协程,就会造成内存持续泄漏,严重的话最终程序崩溃。

    四、总结

    主要介绍了如下:

    • channel的底层数据结构对应哪些信息
    • channel中缓冲区其实是环状缓冲区以及等待队列
    • 读写下的阻塞和非阻塞操作
    • 多路select的用法和处理逻辑

    注:以上仅作为幼麟实验室Golang学习笔记,总结有错误谅解。视频链接

  • 相关阅读:
    关于冥想
    Read Later
    你追求的跟我相反
    UML for Java Programmers之dx实战
    20140525
    面试基础-语言基础篇
    面试基础-linux操作系统篇
    面试基础-数据库篇
    面试基础-计算机网络篇
    Eclipse同时编译多个cpp文件
  • 原文地址:https://www.cnblogs.com/welan/p/15935846.html
Copyright © 2020-2023  润新知