深入理解Golang Channel 的底层结构

目录
  • makechan
  • 发送和接收
  • GoroutinePause/Resume
  • waitemptychannel

Golang 使用 Groutine 和 channels 实现了 CSP(Communicating Sequential Processes) 模型,channlesgoroutine 的通信和同步中承担着重要的角色。

GopherCon 2017 中,Golang 专家 Kavya 深入介绍了 Go Channels 的内部机制,以及运行时调度器和内存管理系统是如何支持 Channel 的,本文根据 Kavya 的 ppt 学习和分析一下 go channels 的原理,希望能够对以后正确高效使用 golang 的并发带来一些启发。

以一个简单的 channel 应用开始,使用 goroutine channel 实现一个任务队列,并行处理多个任务。

func main(){  
    //带缓冲的 channel  
    ch := make(chan Task, 3)  

    //启动固定数量的 worker  
    for i := 0; i< numWorkers; i++ {  
        go worker(ch)  
    }  

    //发送任务给 worker  
    hellaTasks := getTaks()  

    for _, task := range hellaTasks {  
        ch <- task  
    }  

    ...  
}  

func worker(ch chan Task){  
    for {  
       //接受任务  
       task := <- ch  
       process(task)  
    }  
}  

从上面的代码可以看出,使用 golang goroutinechannel 可以很容易的实现一个生产者-消费者模式的任务队列,相比 Java, c++简洁了很多。

channel 可以天然的实现了下面四个特性:

  • goroutine 安全
  • 在不同的 goroutine 之间存储和传输值 – 提供 FIFO 语义 (buffered channel 提供)
  • 可以让 goroutine block/unblock

那么 channel 是怎么实现这些特性的呢?下面我们看看当我们调用 make 来生成一个 channel 的时候都做了些什么。

make chan

上述任务队列的例子第三行,使用 make 创建了一个长度为 3 的带缓冲的 channel,channel 在底层是一个 hchan 结构体,位于 src/runtime/chan.go 里。

其定义如下:

type hchan struct {  
    qcount   uint           // total data in the queue  
    dataqsiz uint           // size of the circular queue  
    buf      unsafe.Pointer // points to an array of dataqsiz elements  
    elemsize uint16  
    closed   uint32  
    elemtype *_type // element type  
    sendx    uint   // send index  
    recvx    uint   // receive index  
    recvq    waitq  // list of recv waiters  
    sendq    waitq  // list of send waiters  

    // lock protects all fields in hchan, as well as several  
    // fields in sudogs blocked on this channel.  
    //  
    // Do not change another G's status while holding this lock  
    // (in particular, do not ready a G), as this can deadlock  
    // with stack shrinking.  
    lock mutex  
}  

make 函数在创建 channel 的时候会在该进程的 heap 区申请一块内存,创建一个 hchan 结构体,返回执行该内存的指针,所以获取的的 ch 变量本身就是一个指针,在函数之间传递的时候是同一个 channel。

hchan 结构体使用一个环形队列来保存 groutine 之间传递的数据(如果是缓存 channel 的话),使用**两个 list **保存像该 chan 发送和从该 chan 接收数据的 goroutine,还有一个 mutex 来保证操作这些结构的安全。

发送和接收

channel 发送和从 channel 接收数据主要涉及 hchan 里的四个成员变量,借用 Kavya ppt 里的图示,来分析发送和接收的过程。

还是以前面的任务队列为例:

//G1  
func main(){  
    ...  

    for _, task := range hellaTasks {  
        ch <- task    //sender  
    }  

    ...  
}  

//G2  
func worker(ch chan Task){  
    for {  
       //接受任务  
       task := <- ch  //recevier  
       process(task)  
    }  
}  

其中 G1 是发送者,G2 是接收,因为 ch 是长度为 3 的带缓冲 channel,初始的时候 hchan 结构体的 buf 为空,sendx 和 recvx 都为 0,当 G1 向 ch 里发送数据的时候,会首先对 buf 加锁,然后将要发送的数据 copy 到 buf 里,并增加 sendx 的值,最后释放 buf 的锁。然后 G2 消费的时候首先对 buf 加锁,然后将 buf 里的数据 copy 到 task 变量对应的内存里,增加 recvx,最后释放锁。整个过程,G1 和 G2 没有共享的内存,底层通过 hchan 结构体的 buf,使用 copy 内存的方式进行通信,最后达到了共享内存的目的,这完全符合 CSP 的设计理念

Do not comminute by sharing memory;instead, share memory by communicating

一般情况下,G2 的消费速度应该是慢于 G1 的,所以 buf 的数据会越来越多,这个时候 G1 再向 ch 里发送数据,这个时候 G1 就会阻塞,那么阻塞到底是发生了什么呢?

Goroutine Pause/Resume

goroutine 是 Golang 实现的用户空间的轻量级的线程,有 runtime 调度器调度,与操作系统的 thread 有多对一的关系,相关的数据结构如下图:

其中 M 是操作系统的线程,G 是用户启动的 goroutine,P 是与调度相关的 context,每个 M 都拥有一个 P,P 维护了一个能够运行的 goutine 队列,用于该线程执行。

当 G1 向 buf 已经满了的 ch 发送数据的时候,当 runtine 检测到对应的 hchan 的 buf 已经满了,会通知调度器,调度器会将 G1 的状态设置为 waiting, 移除与线程 M 的联系,然后从 P 的 runqueue 中选择一个 goroutine 在线程 M 中执行,此时 G1 就是阻塞状态,但是不是操作系统的线程阻塞,所以这个时候只用消耗少量的资源。

那么 G1 设置为 waiting 状态后去哪了?怎们去 resume 呢?我们再回到 hchan 结构体,注意到 hchan 有个 sendq 的成员,其类型是 waitq,查看源码如下:

type hchan struct {   
    ...   
    recvq waitq // list of recv waiters   
    sendq waitq // list of send waiters   
    ...   
}   
//   
type waitq struct {   
    first *sudog   
    last *sudog   
}   

实际上,当 G1 变为 waiting 状态后,会创建一个代表自己的 sudog 的结构,然后放到 sendq 这个 list 中,sudog 结构中保存了 channel 相关的变量的指针(如果该 Goroutine 是 sender,那么保存的是待发送数据的变量的地址,如果是 receiver 则为接收数据的变量的地址,之所以是地址,前面我们提到在传输数据的时候使用的是 copy 的方式)

当 G2 从 ch 中接收一个数据时,会通知调度器,设置 G1 的状态为 runnable,然后将加入 P 的 runqueue 里,等待线程执行。

wait empty channel

前面我们是假设 G1 先运行,如果 G2 先运行会怎么样呢?如果 G2 先运行,那么 G2 会从一个 empty 的 channel 里取数据,这个时候 G2 就会阻塞,和前面介绍的 G1 阻塞一样,G2 也会创建一个 sudog 结构体,保存接收数据的变量的地址,但是该 sudog 结构体是放到了 recvq 列表里,当 G1 向 ch 发送数据的时候,runtime 并没有对 hchan 结构体题的 buf 进行加锁,而是直接将 G1 里的发送到 ch 的数据 copy 到了 G2 sudog 里对应的 elem 指向的内存地址!

总结:

Golang 的一大特色就是其简单高效的天然并发机制,使用 goroutine channel 实现了 CSP 模型。

理解 channel 的底层运行机制对灵活运用 golang 开发并发程序有很大的帮助,看了 Kavya 的分享,然后结合 golang runtime 相关的源码(源码开源并且也是 golang 实现简直良心!), 对 channel 的认识更加的深刻,当然还有一些地方存在一些疑问,比如 goroutine 的调度实现相关的,还是要潜心膜拜大神们的源码!

到此这篇关于深入理解Golang Channel 的底层结构的文章就介绍到这了,更多相关深入理解Golang Channel 结构内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Go语言七篇入门教程四通道及Goroutine

    目录 1. 前言 2. 通道简介 2.1 声明 2.1 读写 2.3 通道详解 2.3.1 例子 2.3.2 死锁 2.3.3 关闭通道 2.3.4 缓冲区 2.3.5 通道的长度和容量 2.3.6 单向通道 2.3.7 Select 2.3.8 default case 块 2.3.9 空 select 2.3.10 Deadlock 2.3.11 nil通道 2.4 多协程协同工作 2.5 WaitGroup 2.5.1 简介 2.5.2工作池 2.5.3 Mutex 3. 结语 如何学习G

  • Go语言中的通道channel详情

    目录 一.Go语言通道基础概念 1.channel产生背景 2.channel工作方式 二.通道使用语法 1.通道的声明与初始化 2.将数据放入通道内 3.从通道内取出数据 4.关闭通道close 三.单项通道及通道的状态分析 1.单项输出通道 2.单项输入通道 3.通道的状态 四.通道死锁原因分析 一.Go语言通道基础概念 1.channel产生背景 线程之间进行通信的时候,会因为资源的争夺而产生竟态问题,为了保证数据交换的正确性,必须使用互斥量给内存进行加锁,go语言并发的模型是CSP,提倡

  • Golang 并发以及通道的使用方式

    Golang最擅长的就是并发编程,使用Golang可以很方便的进行并发编程.先看一段普通的代码 package main import ( "fmt" "time" ) func Foo(i int) { fmt.Printf("%d will sleep\n", i) time.Sleep(5 * time.Second) fmt.Printf("%d wake up\n", i) } func main() { for i

  • Golang通道的无阻塞读写的方法示例

    无论是无缓冲通道,还是有缓冲通道,都存在阻塞的情况,但其实有些情况,我们并不想读数据或者写数据阻塞在那里,有1个唯一的解决办法,那就是使用select结构. 这篇文章会介绍,哪些情况会存在阻塞,以及如何使用select解决阻塞. 阻塞场景 阻塞场景共4个,有缓存和无缓冲各2个. 无缓冲通道的特点是,发送的数据需要被读取后,发送才会完成,它阻塞场景: 通道中无数据,但执行读通道. 通道中无数据,向通道写数据,但无协程读取. // 场景1 func ReadNoDataFromNoBufCh() {

  • Golang实现Directional Channel(定向通道)

    通道可以是定向的( directional ).在默认情况下,通道将以双向的( bidirectional )形式运作,用户既可以把值放人通道,也可以从通道取出值;但是,通道也可以被限制为只能执行发送操作( send-only )或者只能执行接收操作( receive-only ). 通常可以叫 定向通道 ,也有人叫 单向通道 ,两者其实都是指向这篇短文要讨论的 Directional Channel . 下面直接举例子说明: package onlyChannelTest import ( "

  • GO使用socket和channel实现简单控制台聊天室

    使用socket和channel,实现简单控制台聊天室 这里使用socket和channel,演示在GO中如何编写一个简单网络程序 功能分析 聊天室主要功能:用户可以加入/离开聊天室:每个用户发送的消息,广播给所有人 聊天室分为客户端和服务端,客户端负责发送消息和打印服务器消息,服务器负责接收客户端消息,并广播给所有人 客户端可以使用telnet程序 服务端是需要实现的.需要实现的功能, 如何保存多个客户端的连接,管理连接的接入与断开 如何接收和广播客户端消息 实现思路 通过功能分析,拆分为聊天

  • Go语言带缓冲的通道实现

    Go语言中有缓冲的通道(buffered channel)是一种在被接收前能存储一个或者多个值的通道.这种类型的通道并不强制要求 goroutine 之间必须同时完成发送和接收.通道会阻塞发送和接收动作的条件也会不同.只有在通道中没有要接收的值时,接收动作才会阻塞.只有在通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞. 这导致有缓冲的通道和无缓冲的通道之间的一个很大的不同:无缓冲的通道保证进行发送和接收的 goroutine 会在同一时间进行数据交换:有缓冲的通道没有这种保证. 在无缓冲通

  • 深入理解Golang Channel 的底层结构

    目录 make chan 发送和接收 Goroutine Pause/Resume wait empty channel Golang 使用 Groutine 和 channels 实现了 CSP(Communicating Sequential Processes) 模型,channles在 goroutine 的通信和同步中承担着重要的角色. 在GopherCon 2017 中,Golang 专家 Kavya 深入介绍了 Go Channels 的内部机制,以及运行时调度器和内存管理系统是如

  • 深入理解Golang Channel 的底层结构

    目录 makechan 发送和接收 GoroutinePause/Resume waitemptychannel Golang 使用 Groutine 和 channels 实现了 CSP(Communicating Sequential Processes) 模型,channles在 goroutine 的通信和同步中承担着重要的角色. 在GopherCon 2017 中,Golang 专家 Kavya 深入介绍了 Go Channels 的内部机制,以及运行时调度器和内存管理系统是如何支持

  • 深入理解Golang channel的应用

    目录 前言 整体结构 创建 发送 接收 关闭 前言 channel是用于 goroutine 之间的同步.通信的数据结构 channel 的底层是通过 mutex 来控制并发的,但它为程序员提供了更高一层次的抽象,封装了更多的功能,这样并发编程变得更加容易和安全,得以让程序员把注意力留到业务上去,提升开发效率 channel的用途包括但不限于以下几点: 协程间通信,同步 定时任务:和timer结合 解耦生产方和消费方,实现阻塞队列 控制并发数 本文将介绍channel的底层原理,包括数据结构,c

  • 深入理解golang chan的使用

    目录 前言 见真身 结构体 发送数据 接收数据 上手 定义 发送与接收 前言 之前在看golang多线程通信的时候, 看到了go 的管道. 当时就觉得这玩意很神奇, 因为之前接触过的不管是php, java, Python, js, c等等, 都没有这玩意, 第一次见面, 难免勾起我的好奇心. 所以就想着看一看它具体是什么东西. 很明显, 管道是go实现在语言层面的功能, 所以我以为需要去翻他的源码了. 虽然最终没有翻到C的层次, 不过还是受益匪浅. 见真身 结构体 要想知道他是什么东西, 没什

  • 深入理解Golang make和new的区别及实现原理

    目录 前言 new的使用 底层实现 make的使用 底层实现 总结 前言 在Go语言中,有两个比较雷同的内置函数,分别是new和make方法,二者都可以用来分配内存,那他们有什么区别呢?对于初学者可能会觉得有点迷惑,尤其是在掌握不牢固的时候经常遇到panic,下面我们就从底层来分析一下二者的不同.感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助. new的使用 new可以对类型进行内存创建和初始化,其返回值是所创建类型的指针引用,这是与make函数的区别之一.我们通过一个示例代码看下: fun

  • Golang 语言map底层实现原理解析

    在开发过程中,map是必不可少的数据结构,在Golang中,使用map或多或少会遇到与其他语言不一样的体验,比如访问不存在的元素会返回其类型的空值.map的大小究竟是多少,为什么会报"cannot take the address of"错误,遍历map的随机性等等. 本文希望通过研究map的底层实现,以解答这些疑惑. 基于Golang 1.8.3 1. 数据结构及内存管理 hashmap的定义位于 src/runtime/hashmap.go 中,首先我们看下hashmap和buck

  • 彻底理解golang中什么是nil

    nil是什么 相信写过Golang的程序员对下面一段代码是非常非常熟悉的了: if err != nil { // do something.... } 当出现不等于nil的时候,说明出现某些错误了,需要我们对这个错误进行一些处理,而如果等于nil说明运行正常.那什么是nil呢?查一下词典可以知道,nil的意思是无,或者是零值.零值,zero value,是不是有点熟悉?在Go语言中,如果你声明了一个变量但是没有对它进行赋值操作,那么这个变量就会有一个类型的默认零值. 这是每种类型对应的零值:

  • 深入理解Java虚拟机 JVM 内存结构

    目录 前言 JVM是什么 JVM内存结构概览 运行时数据区 程序计数器 Java虚拟机栈 本地方法栈 方法区 运行时常量池 Java堆 直接内存 前言 JVM是Java中比较难理解和掌握的一部分,也是面试中被问的比较多的,掌握好JVM底层原理有助于我们在开发中写出效率更高的代码,可以让我们面对OutOfMemoryError时不再一脸懵逼,可以用掌握的JVM知识去查找分析问题.去进行JVM的调优.去让我们的应用程序可以支持更高的并发量等......总之一句话,学好JVM很重要! JVM是什么 J

  • 深入string理解Golang是怎样实现的

    目录 引言 内容介绍 字符串数据结构 字符串会分配到内存中的哪块区域 编译期即可确定的字符串 如果我们创建两个hello world字符串, 他们会放到同一内存区域吗? 运行时通过+拼接的字符串会放到那块内存中 字面量是否会在编译器合并 当我们用+连接多个字符串时, 会发生什么 rawstring函数 go中字符串是不可变的吗, 我们如何得到一个可变的字符串 []byte和string的更高效转换 结尾 引言 本身打算先写完sync包的, 但前几天在复习以前笔记的时候突然发现与字符串相关的寥寥无

  • 深入理解Golang的反射reflect示例

    目录 编程语言中反射的概念 interface 和 反射 Golang的反射reflect reflect的基本功能TypeOf和ValueOf 说明 从relfect.Value中获取接口interface的信息 已知原有类型[进行“强制转换”] 说明 未知原有类型[遍历探测其Filed] 说明 通过reflect.Value设置实际变量的值 说明 通过reflect.ValueOf来进行方法的调用 说明 Golang的反射reflect性能 小结 总结 参考链接 编程语言中反射的概念 在计算

随机推荐