Go底层channel实现原理及示例详解

目录
  • 概念:
  • 使用场景:
  • 底层数据结构:
  • 操作:
    • 创建
    • 发送
    • 接收
    • 关闭
    • 案例分析:

概念:

Go中的channel 是一个队列,遵循先进先出的原则,负责协程之间的通信(Go 语言提倡不要通过共享内存来通信,而要通过通信来实现内存共享,CSP(Communicating Sequential Process)并发模型,就是通过 goroutine 和 channel 来实现的)

使用场景:

停止信号监听

定时任务

生产方和消费方解耦

控制并发数

底层数据结构:

通过var声明或者make函数创建的channel变量是一个存储在函数栈帧上的指针,占用8个字节,指向堆上的hchan结构体

源码包中src/runtime/chan.go定义了hchan的数据结构:

hchan结构体:

type hchan struct {
 closed   uint32   // channel是否关闭的标志
 elemtype *_type   // channel中的元素类型
 // channel分为无缓冲和有缓冲两种。
 // 对于有缓冲的channel存储数据,使用了 ring buffer(环形缓冲区) 来缓存写入的数据,本质是循环数组
 // 为啥是循环数组?普通数组不行吗,普通数组容量固定更适合指定的空间,弹出元素时,普通数组需要全部都前移
 // 当下标超过数组容量后会回到第一个位置,所以需要有两个字段记录当前读和写的下标位置
 buf      unsafe.Pointer // 指向底层循环数组的指针(环形缓冲区)
 qcount   uint           // 循环数组中的元素数量
 dataqsiz uint           // 循环数组的长度
 elemsize uint16                 // 元素的大小
 sendx    uint           // 下一次写下标的位置
 recvx    uint           // 下一次读下标的位置
 // 尝试读取channel或向channel写入数据而被阻塞的goroutine
 recvq    waitq  // 读等待队列
 sendq    waitq  // 写等待队列
 lock mutex //互斥锁,保证读写channel时不存在并发竞争问题
}

等待队列:

双向链表,包含一个头结点和一个尾结点

每个节点是一个sudog结构体变量,记录哪个协程在等待,等待的是哪个channel,等待发送/接收的数据在哪里

type waitq struct {
   first *sudog
   last  *sudog
}
type sudog struct {
    g *g
    next *sudog
    prev *sudog
    elem unsafe.Pointer
    c        *hchan
    ...
}

操作:

创建

使用 make(chan T, cap) 来创建 channel,make 语法会在编译时,转换为 makechan64 和 makechan

func makechan64(t *chantype, size int64) *hchan {
    if int64(int(size)) != size {
        panic(plainError("makechan: size out of range"))
    }
    return makechan(t, int(size))
}

创建channel 有两种,一种是带缓冲的channel,一种是不带缓冲的channel

// 带缓冲
ch := make(chan int, 3)
// 不带缓冲
ch := make(chan int)

创建时会做一些检查:

  • 元素大小不能超过 64K
  • 元素的对齐大小不能超过 maxAlign 也就是 8 字节
  • 计算出来的内存是否超过限制

创建时的策略:

  • 如果是无缓冲的 channel,会直接给 hchan 分配内存
  • 如果是有缓冲的 channel,并且元素不包含指针,那么会为 hchan 和底层数组分配一段连续的地址
  • 如果是有缓冲的 channel,并且元素包含指针,那么会为 hchan 和底层数组分别分配地址

发送

发送操作,编译时转换为runtime.chansend函数

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool

阻塞式:

调用chansend函数,并且block=true

ch <- 10

非阻塞式:

调用chansend函数,并且block=false

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

向 channel 中发送数据时大概分为两大块:检查和数据发送,数据发送流程如下:

如果 channel 的读等待队列存在接收者goroutine

  • 将数据直接发送给第一个等待的 goroutine, 唤醒接收的 goroutine

如果 channel 的读等待队列不存在接收者goroutine

  • 如果循环数组buf未满,那么将会把数据发送到循环数组buf的队尾
  • 如果循环数组buf已满,这个时候就会走阻塞发送的流程,将当前 goroutine 加入写等待队列,并挂起等待唤醒

接收

发送操作,编译时转换为runtime.chanrecv函数

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool)

阻塞式:

调用chanrecv函数,并且block=true

<ch
v := <ch
v, ok := <ch
// 当channel关闭时,for循环会自动退出,无需主动监测channel是否关闭,可以防止读取已经关闭的channel,造成读到数据为通道所存储的数据类型的零值
for i := range ch {
    fmt.Println(i)
}

非阻塞式:

调用chanrecv函数,并且block=false

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

向 channel 中接收数据时大概分为两大块,检查和数据发送,而数据接收流程如下:

如果 channel 的写等待队列存在发送者goroutine

  • 如果是无缓冲 channel,直接从第一个发送者goroutine那里把数据拷贝给接收变量,唤醒发送的 goroutine
  • 如果是有缓冲 channel(已满),将循环数组buf的队首元素拷贝给接收变量,将第一个发送者goroutine的数据拷贝到 buf循环数组队尾,唤醒发送的 goroutine

如果 channel 的写等待队列不存在发送者goroutine

  • 如果循环数组buf非空,将循环数组buf的队首元素拷贝给接收变量
  • 如果循环数组buf为空,这个时候就会走阻塞接收的流程,将当前 goroutine 加入读等待队列,并挂起等待唤醒

关闭

关闭操作,调用close函数,编译时转换为runtime.closechan函数

close(ch)
func closechan(c *hchan)

案例分析:

package main
import (
    "fmt"
    "time"
    "unsafe"
)
func main() {
  // ch是长度为4的带缓冲的channel
  // 初始hchan结构体重的buf为空,sendx和recvx均为0
    ch := make(chan string, 4)
    fmt.Println(ch, unsafe.Sizeof(ch))
    go sendTask(ch)
    go receiveTask(ch)
    time.Sleep(1 * time.Second)
}
// G1是发送者
// 当G1向ch里发送数据时,首先会对buf加锁,然后将task存储的数据copy到buf中,然后sendx++,然后释放对buf的锁
func sendTask(ch chan string) {
    taskList := []string{"this", "is", "a", "demo"}
    for _, task := range taskList {
        ch <- task //发送任务到channel
    }
}
// G2是接收者
// 当G2消费ch的时候,会首先对buf加锁,然后将buf中的数据copy到task变量对应的内存里,然后recvx++,并释放锁
func receiveTask(ch chan string) {
    for {
        task := <-ch                  //接收任务
        fmt.Println("received", task) //处理任务
    }
}

总结hchan结构体的主要组成部分有四个:

  • 用来保存goroutine之间传递数据的循环数组:buf
  • 用来记录此循环数组当前发送或接收数据的下标值:sendx和recvx
  • 用于保存向该chan发送和从该chan接收数据被阻塞的goroutine队列: sendq 和 recvq
  • 保证channel写入和读取数据时线程安全的锁:lock

以上就是Go底层channel实现原理及示例详解的详细内容,更多关于Go channel底层原理的资料请关注我们其它相关文章!

(0)

相关推荐

  • vue.js基于ElementUI封装了CRUD的弹框组件

    目录 前言 开始封装 json对象如下所示 table表头作为列表传入,数据结构如下 在子组件中循环渲染出表头 在父组件中调用 前言 代码写得不好,为什么不封装一下呢,如果用的是ElementUI框架,也可以在此基础上进行二次封装.譬如说,这个用来对列表数据进行增删改查的弹框. 开始封装 原本只是个小功能,但是别的模块也需要用到. 我的想法就是,把弹框标题,table表头,必填字节,接口请求路径,增删改查CRUD,等等,放在一个json对象里面.通过父组件向子组件传参的方式,展示不同内容,调用不

  • Golang channel为什么不会阻塞的原因详解

    正文 最近在学通道channel,发现一个简单的demo: package main import "fmt" func main() { chanInt := make(chan int) go func() { chanInt <- 100 }() res := <-chanInt fmt.Println(res) } 输出结果是100,这个没有问题.但是之前在学goroutine的时候有看到过一个例子: package main import "fmt&qu

  • golang channel管道使用示例解析

    目录 定义channel管道 channel管道塞值和取值 通过channel管道实现同步,和数据交互 无缓冲的channel 有缓冲的channel管道 关闭channel管道 单向channel管道,读写分离 管道消费者生产者模型 定义channel管道 定义一个channel时,也需要定义发送到管道的值类型.channel可以使用内置的make()函数来创建: var ch = make(chan int) //等价于:make(chan Type,0) var ch = make(cha

  • Go语言CSP并发模型goroutine channel底层实现原理

    目录 Go的CSP并发模型(goroutine + channel) 1.goroutine goroutine的优点: 2.channel 无缓存channel 有缓存channel 3.Go并发模型的底层实现原理 4.一个CSP例子 参考Go的CSP并发模型实现:M, P, G Go语言是为并发而生的语言,Go语言是为数不多的在语言层面实现并发的语言. 并发(concurrency):多个任务在同一段时间内运行. 并行(parallellism):多个任务在同一时刻运行. Go的CSP并发模

  • Go通道channel通过通信共享内存

    目录 引言 通道的声明与创建 接收 & 发送数据 引言 不要通过共享内存来通信 应该通过通信来共享内存 这句话有网友的解释如下: 这句俏皮话具体说来就是,不同的线程不共享内存不用锁,线程之间通讯用通道(channel)同步也用channel. chanel是协程之间传递信息的媒介,优雅地解决了某些后端开发常用语言中随处可见的lock,unlock,临界区等,把从很多线程层面解决的问题移到协程,从而静态地保证没有数据竞争. 通道的声明与创建 伪代码如下: //声明类型 var 通道名 chan 数

  • GO语言协程创建使用并通过channel解决资源竞争

    目录 创建协程 主协程终止,子协程也终止 runtime包 Gosched让出CPU时间片 Goexit立即结束当前协程 GOMAXPROCS设置并行CPU核数最大值,并返回之前的值 runtime.NumGoroutine()获取当前运行中的goroutine数量 多任务资源竞争问题 通过channel解决资源竞争问题 主协程如何等其余协程完再退出 创建协程 goroutine是go的设计核心,就是协程主协程终止了,子协程也终止 package main import ( "fmt"

  • Go底层channel实现原理及示例详解

    目录 概念: 使用场景: 底层数据结构: 操作: 创建 发送 接收 关闭 案例分析: 概念: Go中的channel 是一个队列,遵循先进先出的原则,负责协程之间的通信(Go 语言提倡不要通过共享内存来通信,而要通过通信来实现内存共享,CSP(Communicating Sequential Process)并发模型,就是通过 goroutine 和 channel 来实现的) 使用场景: 停止信号监听 定时任务 生产方和消费方解耦 控制并发数 底层数据结构: 通过var声明或者make函数创建

  • java中Servlet监听器的工作原理及示例详解

    监听器就是一个实现特定接口的普通java程序,这个程序专门用于监听另一个java对象的方法调用或属性改变,当被监听对象发生上述事件后,监听器某个方法将立即被执行. 监听器原理 监听原理 1.存在事件源 2.提供监听器 3.为事件源注册监听器 4.操作事件源,产生事件对象,将事件对象传递给监听器,并且执行监听器相应监听方法 监听器典型案例:监听window窗口的事件监听器 例如:swing开发首先制造Frame**窗体**,窗体本身也是一个显示空间,对窗体提供监听器,监听窗体方法调用或者属性改变:

  • Vue响应式原理的示例详解

    Vue 最独特的特性之一,是非侵入式的响应系统.数据模型仅仅是普通的 JavaScript 对象.而当你修改它们时,视图会进行更新.聊到 Vue 响应式实现原理,众多开发者都知道实现的关键在于利用 Object.defineProperty , 但具体又是如何实现的呢,今天我们来一探究竟. 为了通俗易懂,我们还是从一个小的示例开始: <body> <div id="app"> {{ message }} </div> <script> v

  • epoll封装reactor原理剖析示例详解

    目录 reactor是什么? reactor模型三个重要组件与流程分析 组件 流程 将epoll封装成reactor事件驱动 封装每一个连接sockfd变成ntyevent 封装epfd和ntyevent变成ntyreactor 封装读.写.接收连接等事件对应的操作变成callback 给每个客户端的ntyevent设置属性 将ntyevent加入到epoll中由内核监听 将ntyevent从epoll中去除 读事件回调函数 写事件回调函数 接受新连接事件回调函数 reactor运行 react

  • 无UI 组件Headless框架逻辑原理用法示例详解

    目录 概述 精读 总结 概述 Headless 组件即无 UI 组件,框架仅提供逻辑,UI 交给业务实现.这样带来的好处是业务有极大的 UI 自定义空间,而对框架来说,只考虑逻辑可以让自己更轻松的覆盖更多场景,满足更多开发者不同的诉求. 我们以 headlessui-tabs 为例看看它的用法,并读一读 源码. headless tabs 最简单的用法如下: import { Tab } from "@headlessui/react"; function MyTabs() { ret

  • Go语言基础闭包的原理分析示例详解

    目录 一. 闭包概述 二. 代码演示 运行结果 代码说明 一. 闭包概述 闭包就是解决局部变量不能被外部访问的一种解决方案 闭包是把函数当作返回值的一种应用 二. 代码演示 总体思想为:在函数内部定义局部变量,把另一个函数当作返回值,局部变量对于返回值函数相当于全部变量,所以多次调用返回值函数局部变量的值跟随变化. // closure.go package main import ( "fmt" "strings" ) func main() { f := clo

  • Python+OpenCV人脸检测原理及示例详解

    关于opencv OpenCV 是 Intel 开源计算机视觉库 (Computer Version) .它由一系列 C 函数和少量 C++ 类构成,实现了图像处理和计算机视觉方面的很多通用算法. OpenCV 拥有包括 300 多个 C 函数的跨平台的中.高层 API .它不依赖于其它的外部库 -- 尽管也可以使用某些外部库. OpenCV 对非商业应用和商业应用都是免费 的.同时 OpenCV 提供了对硬件的访问,可以直接访问摄像头,并且 opencv 还提供了一个简单的 GUI(graph

  • Kotlin协程Dispatchers原理示例详解

    目录 前置知识 demo startCoroutineCancellable intercepted()函数 DefaultScheduler中找dispatch函数 Runnable传入 Worker线程执行逻辑 小结 前置知识 Kotlin协程不是什么空中阁楼,Kotlin源代码会被编译成class字节码文件,最终会运行到虚拟机中.所以从本质上讲,Kotlin和Java是类似的,都是可以编译产生class的语言,但最终还是会受到虚拟机的限制,它们的代码最终会在虚拟机上的某个线程上被执行. 之

  • Sentinel熔断规则原理示例详解分析

    目录 概述 熔断(降级)策略 慢调用比例 概念 测试 异常比例 概念 测试 异常数 概念 测试 概述 除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一. 由于调用关系的复杂性,如果调用链路中的某个资源不稳定,最终会导致请求发生堆积. Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时.异常比例升高.异常数堆积) 对这个资源的调用进行限制,让请求快速失败从而避免影响到其它的资源而导致级联错误. 当资源被降级后,在接下来的降级时间窗口之内

  • C++递归与分治算法原理示例详解

    目录 1. 汉诺塔问题 2. 全排列问题 4. 归并排序 5. 快速排序 6. 棋盘覆盖问题 1. 汉诺塔问题 递归算法,分为 3 步:将 n 个 a 上的盘子借助 c 移动到 b ① 将 n-1 个 a 上的盘子借助 b 移动到 c ② 将 a 上的盘子移动到 b ③ 将 c 上的 n-1 个盘子借助 a 移动到 b 所有盘子都移动到 b 上了 void hanoi(int n,char a,char b,char c)//将n个碟子从a借助c 移到b { if(n==0) return; e

随机推荐