深入理解 Golang: Channel 管道

news2024/9/9 4:56:12

Channel 的使用

Channel 声明方法

  • chInt := make(chan int) // unbuffered channel 非缓冲通道
  • chInt := make(chan int, 0) // unbuffered channel 非缓冲通道
  • chInt := make(chan int, 2) // bufferd channel 缓冲通道

Channel 基本用法

  • ch <- x // channel 接收数据 x
  • x <- ch // channel 发送数据并赋值给 x
  • <- ch // channel 发送数据,忽略接受者

如果使用了非缓冲通道,此时向缓冲区塞数据需要有地方能立即接收数据,不然会一致阻塞。原理是此时缓冲区无数据(无缓冲),向 channel 发送数据视为直接发送,即直接发送到正在休眠等待的协程中。

func main() {
    ch := make(chan string)
    // 阻塞
    ch <- "ping"
    <-ch
}

启动协程来拿数据:

func main() {
    ch := make(chan string)

    // 程序能通过
    go func() {
        <-ch
    }()

    ch<-"ping"
}

内存与通信

不要通过共享内存的方式进行通信,而是应该通过通信的方式共享内存。主要是为了:

  • 避免协程竞争和数据冲突。
  • 更高级的抽象,降低开发难度,增加程序可读性。
  • 模块之间更容易解耦,增强扩展性和可维护性。

通过共享内存案列:

func watch(p *int) {
    for {
        if *p == 1 {
            fmt.Println("go")
            break
        }
    }
}

func main() {
    i := 0
    go watch(&i)

    time.Sleep(time.Second)

    i = 1

    time.Sleep(time.Second)
}

通过通信的方式如下:

func watch(c chan int) {

    if <-c == 1 {
        fmt.Println("go")
    }
}

func main() {
    c := make(chan int)
    go watch(c)

    time.Sleep(time.Second)

    c <- 1

    time.Sleep(time.Second)
}

Channel 的设计

在这里插入图片描述

Channel 在 Go 的底层表示为一个 hchan 结构体:

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
    elemtype *_type // element type
    /* 缓存区结构结束*/

    // 发送队列
    sendx    uint   // send index
    sendq    waitq  // list of send waiters

    // 接收队列
    recvx    uint   // receive index
    recvq    waitq  // list of recv waiters

    lock mutex
    // 0:关闭状态;1:开启状态
    closed   uint32
}

type waitq struct {
    first *sudog
    last  *sudog
}

数据存放在一个环形缓冲区 Ring Buffer,可以降低内存/GC的开销

关于 c <- “x” 语法糖,channel 数据发送原理
Go 中会把 c<- 编译为 chansend1 方法:

// %GOROOT%src/runtime/chan.go
//go:nosplit
func chansend1(c *hchan, elem unsafe.Pointer) {
    chansend(c, elem, true, getcallerpc())
}

发送数据的 3 种情形:

  1. 缓冲区无数据,向 channel 发送数据视为直接发送,将数据直接拷贝给等待接收的协程的接受量,并唤醒该协程;如果无等待中的接收协程,则将数据放入缓冲区。
  2. 缓冲区有数据但缓冲区未满,则数据存到缓冲区。
  3. 接收队列中无休眠等待的协程,且缓冲区已满,则将数据包装成 sudog,放入 sendq 队列休眠等待,然受给 channel 解锁。
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
    // ...
    if c.closed != 0 {
        unlock(&c.lock)
        panic(plainError("send on closed channel"))
    }
    // 1. 取出接收等待队列 recvq 的协程,将数据发送给它
    if sg := c.recvq.dequeue(); sg != nil {
        send(c, sg, ep, func() { unlock(&c.lock) }, 3)
        return true
    }

    // 接收等待队列中没有协程时
    if c.qcount < c.dataqsiz {
        // 2. 缓存空间还有余量,将数据放入缓冲区
        qp := chanbuf(c, c.sendx)
        if raceenabled {
            racenotify(c, c.sendx, nil)
        }
        typedmemmove(c.elemtype, qp, ep)
        c.sendx++
        if c.sendx == c.dataqsiz {
            c.sendx = 0
        }
        c.qcount++
        unlock(&c.lock)
        return true
    }
    // ...

    // 3. 休眠等待
    gp := getg()
    // 包装为 sudog
    mysg := acquireSudog()
    mysg.releasetime = 0
    if t0 != 0 {
        mysg.releasetime = -1
    }

    // 将数据,协程指针等记录到 mysq 中
    mysg.elem = ep
    mysg.waitlink = nil
    mysg.g = gp
    mysg.isSelect = false
    mysg.c = c
    gp.waiting = mysg
    gp.param = nil

    // 将 mysg 自己入队
    c.sendq.enqueue(mysg)

    // 休眠
    gp.parkingOnChan.Store(true)
    gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
    KeepAlive(ep)

    // 被唤醒后再维护一些数据,注意,此时的记录的数据已经被拿走
    if mysg != gp.waiting {
    throw("G waiting list is corrupted")
    }
    gp.waiting = nil
    gp.activeStackChans = false
    closed := !mysg.success
    gp.param = nil
    // ...
}

关于 rec <-c 语法糖,channel 数据接收原理
Go 中会把 <-c 编译为 func chanrecv 方法,具体如下:

  • 编译阶段,rec <- c 转化为 runtime.chanrecv1()
  • 编译阶段,rec, ok <- c 转化为 runtime.chanrecv2()
  • 最终会调用 chanrecv() 方法
//go:nosplit
func chanrecv1(c *hchan, elem unsafe.Pointer) {
    chanrecv(c, elem, true)
}

//go:nosplit
func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {
    _, received = chanrecv(c, elem, true)
    return
}

接收数据的 4 种情形:

  1. 有等待中的发送协程(sendq),但缓冲区为空,从协程接收
  2. 有等待中的发送协程(sendq),但缓冲区非空,从缓存接收
  3. 接收缓存
  4. 阻塞接收
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    // ...
    if c.closed != 0 {
        // ...
    } else {
        // 1.,2. 接收数据前,已经有协程在休眠等待发送数据
        if sg := c.sendq.dequeue(); sg != nil {
            // 在 1 的情况下,缓存为空,直接从 sendq 的协程取数据
            // 在 2 的情况下,从缓存(缓冲)取走数据后,将 sendq 里的等待中的协程的数据放入缓存,并唤醒该协程
            // 这就是为什么 sendq 队列中的协程被唤醒后,其携带的数据已经被取走的原因
            recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
            return true, true
        }
    }

    // 3. 直接从缓存接收数据
    if c.qcount > 0 {
        qp := chanbuf(c, c.recvx)
        if raceenabled {
            racenotify(c, c.recvx, nil)
        }
        if ep != nil {
            typedmemmove(c.elemtype, ep, qp)
        }
        typedmemclr(c.elemtype, qp)
        c.recvx++
        if c.recvx == c.dataqsiz {
            c.recvx = 0
        }
        c.qcount--
        unlock(&c.lock)
        return true, true
    }

    // ...
    // 缓冲区为空,同时 sendq 里也没休眠的协程,则休眠等待
    gp := getg()
    mysg := acquireSudog()
    mysg.releasetime = 0
    if t0 != 0 {
        mysg.releasetime = -1
    }
    mysg.elem = ep
    mysg.waitlink = nil
    gp.waiting = mysg
    mysg.g = gp
    mysg.isSelect = false
    mysg.c = c
    gp.param = nil
    c.recvq.enqueue(mysg)

    // 休眠
    gp.parkingOnChan.Store(true)
    gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)

    // ...
}

从 Channel 接收数据的过程中被唤醒,说明之前因为没有数据而休眠等待,当发送方发送数据时,会主动将数据拷贝至接收方本地。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/709446.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python 使用 NetworkX

Python 使用 NetworkX 说明&#xff1a;本篇文章主要讲述 python 使用 networkx 绘制有向图&#xff1b; 1. 介绍&安装 NetworkX 是一个用于创建、操作和研究复杂网络的 Python 库。它提供了丰富的功能&#xff0c;可以帮助你创建、分析和可视化各种类型的网络&#xff…

Linux-Bash的基本特性

Linux-Bash的特性应用&#xff1a; 按命令展开&#xff1a; {} 花括号是Bash支持的一种特殊符号&#xff0c;可以简单的理解为小学时学的乘法结合率。 例如&#xff1a; echo {1..10} 1 2 3 4 5 6 7 8 9 101…10 表示1到10&#xff0c;则整个命令可以理解为echo 1 echo 2 ec…

MFC多文档绘制不同图形

VC6新建一个多文档项目&#xff1b;根据窗口标题的最后数字&#xff0c;绘制不同图形&#xff1b; void CPrdView::OnDraw(CDC* pDC) {CPrdDoc* pDoc GetDocument();ASSERT_VALID(pDoc);// TODO: add draw code for native data hereCString str1 pDoc->GetTitle();CPoin…

[abc周赛复盘] AtCoder Beginner Contest 308 20230701

[abc周赛复盘] AtCoder Beginner Contest 308 20230701 总结A - New Scheme1. 题目描述2. 思路分析3. 代码实现 B - Default Price1. 题目描述2. 思路分析3. 代码实现 C - Standings1. 题目描述2. 思路分析3. 代码实现 D - Snuke Maze1. 题目描述2. 思路分析3. 代码实现 E - M…

Apache Doris (十四) :聚合模型的局限性、模型选择建议及列定义建议

目录 1. 聚合模型的局限性 2.数据模型的选择建议 2.1 Aggregate数据模型选择 ​​​​​​​​​​​​​​2.2 Unique数据模型选择 ​​​​​​​​​​​​​​2.3 Duplicate数据模型选择 ​​​​​​​3. 列定义建议 进入正文之前&#xff0c;欢迎订阅专题、对博文点…

天津大学计算机考研分析

关注我们的微信公众号 姚哥计算机考研 更多详情欢迎咨询 天津大学&#xff08;B&#xff09;考研难度&#xff08;☆☆☆☆&#xff09; 天津大学计算机考研主要招生学院在智能与计算学部、佐治亚理工深圳学院、新媒体与传播学院。招生学院较多&#xff0c;目前均已出拟录取…

时间序列分解 | Matlab奇异谱分析SSA做信号去噪、分解

文章目录 效果一览文章概述部分源码参考资料效果一览 文章概述 时间序列分解 | Matlab奇异谱分析SSA做信号去噪、分解 部分源码 %----------------------

HPM6750系列--第四篇 搭建Visual Studio Code开发调试环境

一、目的 在之前的博客《HPM6750系列--第二篇 搭建Ubuntu开发环境》、《HPM6750系列--第三篇 搭建MACOS编译和调试环境》我们介绍了基于命令行的编译调试过程&#xff0c;整个过程稍微有些繁琐可能有些小伙伴不太习惯&#xff0c;那么本篇就介绍一下在Visual Studio Code下的开…

【Linux系统编程】shell的感性理解

文章目录 1. shell是什么&#xff1f;它有什么作用&#xff1f;2. 通过一个故事感性理解shell的运行机制开端发展波澜渐起&#xff08;正常命令的处理&#xff09;故事角色与处理过程中各部分的映射走向高潮&#xff08;非法请求的处理&#xff09;shell 存在的意义结尾 1. she…

Leetcode-每日一题【142.环形链表Ⅱ】

题目 给定一个链表的头节点 head &#xff0c;返回链表开始入环的第一个节点。 如果链表无环&#xff0c;则返回 null。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&#xff0c;评测系统内部…

每天一点Python——day45

#第四十五天 #字典元素的特点&#xff1a; #例&#xff1a;字典中的所有元素都是一个key-value对【键值对】&#xff0c;key不允许重复&#xff0c;value可以重复 a{name:张三,name:李四} print(a) #只会输出李四&#xff0c;因为键不允许重复&#xff0c;否则会出现值覆盖的情…

QMenu代码生成器

共4种选项&#xff1a; 文本&#xff1a;输入父对象名&#xff0c;文本即可 文本图标&#xff1a;输入父对象名&#xff0c;文本&#xff0c;图标 文本图标菜单&#xff1a;输入父对象名&#xff0c;子对象名&#xff0c;文本&#xff0c;图标 文本菜单&#xff1a;输入父对象名…

14 | count(*)这么慢,我该怎么办?

一下内容出自《MySQL 实战 45 讲》 14 | count(*)这么慢&#xff0c;我该怎么办&#xff1f; count(*) 的实现方式 不同的 MySQL 引擎中&#xff0c;count(*) 有不同的实现方式。 MyISAM 引擎把一个表的总行数存在了磁盘上&#xff0c;执行 count(*) 的时候会直接返回这个数…

斯坦福大学吴佳俊:通过自然监督编码理解视觉世界

导读 在智源大会的生成模型论坛上&#xff0c;斯坦福大学助理教授吴佳俊带来了精彩的演讲 “通过自然监督编码理解视觉世界”&#xff08;Understanding the Visual World Through Naturally Supervised Code&#xff09;。此次演讲从二维图像拓展到三维世界&#xff0c;从人类…

linux高并发网络编程开发(xml json)16_xml和Json相关api及文件解析制作

pdf详细版 01 学习目标 xml xml基础语法和规范C程序中如何使用xml开源库借助开源库,在C程序中生成xml文件已知一个xml文件,如何借助开源库解析xml文件数据 Json json的基础语法和规范C程序中如何使用json开源库 - cjson使用cjson生成json文件已知一个json文件,使用cjson库解析…

列存储、行存储

一、定义 1.1定义 Sybase在2004年左右就推出了列存储的Sybase IQ数据库系统,主要用于在线分析、数据挖掘等查询密集型应用。列存储&#xff0c;缩写为DSM&#xff0c;相对于NSM(N-ary storage model)&#xff0c;其主要区别在于&#xff1a; DSM将所有记录中相同字段的数据聚…

【AUTOSAR】BMS开发实际项目讲解(二十三)----电池管理系统高压互锁保护

高压互锁保护 关联的系统需求 TSR-BMS-6101、TSR-BMS-6102、TSR-BMS-6103、TSR-BMS-6104、TSR-BMS-6105、TSR-BMS-6106、TSR-BMS-6107、TSR-BMS-6108、TSR-BMS-6109、TSR-BMS-6110、TSR-BMS-6111; TSR-BMS-6201; TSR-BMS-6301; TSR-BMS-S101、TSR-BMS-S102、TSR-BMS-S103、TS…

AutoSAR系列讲解(入门篇)4.3-BSW的Communication功能

一、架构与术语解释 BSW中以后每一节我都会放上一张模块图&#xff0c;所以就先上图&#xff1a; 由于汽车上一般都使用CAN总线&#xff0c;图中的bus大家可以就当成CAN来看待&#xff0c;如果使用的是LIN或者其他的&#xff0c;也相应的换成其总线看待就行。后续在实践篇中将会…

你需要了解的 50 个 ChatGPT 统计数据和事实

Rest assured that with the ChatGPT statistics you’re about to read, you’ll confirm that the popular chatbot from OpenAI is just the beginning of something bigger. Since its launch in November 2022, ChatGPT has broken unexpected records. For example, it r…

数据结构之串

1.串的基本概念 • 一个串是由n&#xff08;n≥0&#xff09;个字符组成的有限序列&#xff0c;记为s“s0s1 ⋯ sn-1”&#xff0c;其 中&#xff0c;s是串名&#xff0c;双引号括起来的字符序列s0s1 ⋯ sn-1是串值。 • 一个字符在串中的位置称为该字符在串中的序号&#xff…