GO 语言中 chan 的理解

这篇具有很好参考价值的文章主要介绍了GO 语言中 chan 的理解。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

GO 语言中 chan 的理解

chan 的底层实现是怎么样的?

chan 是 Go 语言中的一个关键字,用于实现并发通信。chan 可以用于在不同的 goroutine 之间传递数据,实现数据的同步和异步传输。

在底层实现上,chan 是通过一个结构体来表示的,这个结构体包含了一个指向数据的指针和两个指向信道的指针。其中,一个指针用于发送数据,另一个指针用于接收数据。

下面是 chan 的底层实现代码:

type hchan struct {
    qcount   uint           // 当前队列中的元素数量
    dataqsiz uint           // 队列的容量
    buf      unsafe.Pointer // 指向队列的指针
    elemsize uint16         // 元素的大小
    closed   uint32         // 是否关闭
    elemtype *_type         // 元素的类型
    sendx    uint           // 发送的位置
    recvx    uint           // 接收的位置
    recvq    waitq          // 接收等待队列
    sendq    waitq          // 发送等待队列
    lock     mutex          // 锁
}

chan 的发送和接收操作的底现

当我们向 chan 发送数据时,会先检查 chan 是否已经关闭。如果 chan 已经关闭,那么发送操作会直接返回一个 panic。否则,会将数据复制到队列中,并更新发送位置。

下面是 chan 发送操作的底层实现代码:

func chansend(c *hchan, ep unsafe.Pointer, block bool) bool {
    // 检查 chan 是否已经关闭
    if c.closed != 0 {
        panic("send on closed channel")
    }
    // 计算发送位置
    i := c.sendx
    // 计算队列中的元素数量
    if c.qcount < c.dataqsiz {
        c.qcount++
    } else {
        // 如果队列已满,需要扩容
        grow(c)
    }
    // 更新发送位置
    c.sendx++
    // 将数据复制到队列中
    qput(c, i, ep)
    return true
}

当我们从 chan 接收数据时,也会先检查 chan 是否已经关闭。如果 chan 已经关闭并且队列中没有数据,那么接收操作会直接返回一个零值。否则,会从队列中取出数据,并更新接收位置。

下面是 chan 接收操作的底层实现代码:

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    // 检查 chan 是否已经关闭
    if c.closed != 0 && c.qcount == 0 {
        return false, false
    }
    // 计算接收位置
    i := c.recvx
    // 如果队列中没有数据,需要阻塞等待
    for c.qcount <= 0 {
        if !block {
            return false, false
        }
        gopark(chanparkcommit, unsafe.Pointer(c), "chan receive", traceEvGoBlockRecv, 1)
    }
    // 从队列中取出数据
    qget(c, i, ep)
    // 更新接收位置
    c.recvx++
    // 更新队列中的元素数量
    c.qcount--
    return true, true
}

chan 是如何实现多个 gorouting 并发安全访问的?

如上 hchan 结构中的 recvq 和 sendq 分别表示接收等待队列和发送等待队列,它们的定义如下:

type waitq struct {
    first *sudog // 等待队列的第一个元素
    last  *sudog // 等待队列的最后一个元素
}

sudog 表示等待队列中的一个元素,它的定义如下:

type sudog struct {
    // 等待的 goroutine
    g *g
    // 是否是 select 操作
    isSelect bool
    // 等待队列中的下一个元素
    next *sudog
    // 等待队列中的上一个元素
    prev *sudog
    // 等待的元素
    elem unsafe.Pointer
    // 获取锁的时间
    acquiretime int64
    // 保留字段
    release2 uint32
    // 等待的 ticket
    ticket uint32
    // 父 sudog
    parent *sudog
    // 等待链表
    waitlink *sudog
    // 等待链表的尾部
    waittail *sudog
    // 关联的 chan
    c *hchan
    // 唤醒时间
    releasetime int64
}

当 chan 的队列已满或为空时,当前 goroutine 会被加入到发送等待队列或接收等待队列中,并释放锁。当另一个 goroutine 从 chan 中取出数据或向 chan 发送数据时,它会重新获取锁,并从等待队列中取出一个 goroutine,将其唤醒。这样,多个 goroutine 就可以通过等待队列来实现并发访问 chan。

sudog 是 Go 中非常重要的数据结构,因为 g 与同步对象关系是多对多的。

一个 g 可以出现在许多等待队列上,因此一个 g 可能有很多sudog:在 select 操作中,一个 goroutine 可以等待多个 chan 中的任意一个就绪, sudog 中的 isSelect 字段被用来标记它是否是 select 操作。当一个 chan 就绪时,它会唤醒对应的 sudog,并将其从等待队列中移除。如果一个 sudog 是 select 操作,它会在唤醒后返回一个特殊的值,表示哪个 chan 就绪了

多个 g 可能正在等待同一个同步对象,因此一个对象可能有许多 sudog:chan 在不同的 gorouting 中传递等待

完整的发送和接受方法实现如下:文章来源地址https://www.toymoban.com/news/detail-515755.html

func chansend(c *hchan, ep unsafe.Pointer, block bool) bool {
    // 获取 chan 的锁
    lock(&c.lock)
    // 检查 chan 是否已经关闭
    if c.closed != 0 {
        unlock(&c.lock)
        panic("send on closed channel")
    }
    // 计算发送位置
    i := c.sendx
    // 计算队列中的元素数量
    if c.qcount < c.dataqsiz {
        c.qcount++
    } else {
        // 如果队列已满,需要将当前 goroutine 加入到发送等待队列中
        g := getg()
        gp := g.m.curg
        if !block {
            unlock(&c.lock)
            return false
        }
        // 创建一个 sudog,表示当前 goroutine 等待发送
        sg := acquireSudog()
        sg.releasetime = 0
        sg.acquiretime = 0
        sg.g = gp
        sg.elem = ep
        sg.c = c
        // 将 sudog 加入到发送等待队列中
        c.sendq.enqueue(sg)
        // 释放锁,并将当前 goroutine 阻塞
        unlock(&c.lock)
        park_m(gp, waitReasonChanSend, traceEvGoBlockSend, 1)
        // 当 goroutine 被唤醒时,重新获取锁
        lock(&c.lock)
        // 检查 chan 是否已经关闭
        if c.closed != 0 {
            unlock(&c.lock)
            panic("send on closed channel")
        }
        // 从发送等待队列中取出 sudog
        sg = c.sendq.dequeue()
        if sg == nil {
            throw("chan send inconsistency")
        }
        // 将数据复制到队列中
        qput(c, i, ep)
    }
    // 更新发送位置
    c.sendx++
    // 释放锁
    unlock(&c.lock)
    return true
}
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    // 获取 chan 的锁
    lock(&c.lock)
    // 检查 chan 是否已经关闭
    if c.closed != 0 && c.qcount == 0 {
        unlock(&c.lock)
        return false, false
    }
    // 计算接收位置
    i := c.recvx
    // 如果队列中没有数据,需要将当前 goroutine 加入到接收等待队列中
    if c.qcount <= 0 {
        g := getg()
        gp := g.m.curg
        if !block {
            unlock(&c.lock)
            return false, false
        }
        // 创建一个 sudog,表示当前 goroutine 等待接收
        sg := acquireSudog()
        sg.releasetime = 0
        sg.acquiretime = 0
        sg.g = gp
        sg.elem = ep
        sg.c = c
        // 将 sudog 加入到接收等待队列中
        c.recvq.enqueue(sg)
        // 释放锁,并将当前 goroutine 阻塞
        unlock(&c.lock)
        park_m(gp, waitReasonChanReceive, traceEvGoBlockRecv, 1)
        // 当 goroutine 被唤醒时,重新获取锁
        lock(&c.lock)
        // 检查 chan 是否已经关闭
        if c.closed != 0 && c.qcount == 0 {
            unlock(&c.lock)
            return false, false
        }
        // 从接收等待队列中取出 sudog
        sg = c.recvq.dequeue()
        if sg == nil {
            throw("chan receive inconsistency")
        }
        // 从队列中取出数据
        qget(c, i, ep)
    } else {
        // 从队列中取出数据
        qget(c, i, ep)
    }
    // 更新接收位置
    c.recvx++
    // 更新队列中的元素数量
    c.qcount--
    // 释放锁
    unlock(&c.lock)
    return true, true
}

到了这里,关于GO 语言中 chan 的理解的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • go chan基本使用

    怎么理解这个缓冲,我个人的理解是是执行这个chan 操作的时候是否发送阻塞。 操作:读和写。 读取的时候,我们都应该要是阻塞的,例如我们的socket、的recv函数。当然取决于你设置的是阻塞的套接字还是非阻塞的套接字了。 无缓冲的chan,讲究读写对称,也就是你在读的时

    2024年02月01日
    浏览(39)
  • 派森语言python干什么的,派森编程课程怎么样

    大家好,小编来为大家解答以下问题,派森语言python干什么的,派森编程课程怎么样,今天让我们一起来看看吧!   简单的数学运算 整数相加,得到整数: 2 + 2 4 浮点数相加,得到浮点数: 2.0 + 2.5 4.5 整数和浮点数相加,得到浮点数: 2 + 2.5 4.5 变量赋值 Python使用变量名=表

    2024年02月14日
    浏览(56)
  • 6.Go语言学习笔记-结合chatGPT辅助学习Go语言底层原理

    1、Go版本 2、汇编基础 推荐阅读:GO汇编语言简介 推荐阅读:A Quick Guide to Go\\\'s Assembler - The Go Programming Language 精简指令集 数据传输: MOV/LEA 跳转指令: CMP/TEST/JMP/JCC 栈指令: PUSH/POP 函数调用指令: CALL/RET 算术指令: ADD/SUB/MUL/DIV 逻辑指令: AND/OR/XOR/NOT 移位指令: SHL/SHR JCC有条件跳转: JE

    2024年02月04日
    浏览(41)
  • 华为云流水线CodeArts Pipeline怎么样?能实现哪些功能?

    华为云流水线服务 CodeArts Pipeline ,旨在提升编排体验,开放插件平台,并提供标准化的DevOps企业治理模型,将华为公司内的优秀研发实践赋能给伙伴和客户。 灵活编排、高效调度 开放流水线插件 内置企业DevOps研发治理模型 体验通道:https://devcloud.cn-north-4.huaweicloud.com/cicd/

    2024年02月15日
    浏览(44)
  • HDFS的工作原理是怎么样的?是如何实现HA模式?

    原文链接:http://www.ibearzmblog.com/#/technology/info?id=714dcb3957e29185493239b269a9ef65 HDFS是能够提供一个分布式文件存储的系统,在大型数据文件的存储中,能够提供高吞吐量的数据访问,那么它是如何实现数据文件的读写的呢?作为集群老大的NameNode当出现服务不可用的情况,HDFS又如何

    2024年02月11日
    浏览(39)
  • 【PHP面试题42】Laravel依赖注入实现的原理是怎么样的

    本文已收录于PHP全栈系列专栏:PHP面试专区。 计划将全覆盖PHP开发领域所有的面试题, 对标资深工程师/架构师序列 ,欢迎大家提前关注锁定。 Laravel是一个流行的PHP开发框架,它提供了许多方便和强大的功能来简化开发过程。其中一个关键功能是依赖注入(Dependency Injecti

    2024年02月16日
    浏览(43)
  • 深入理解Go语言接口

    接口是一种定义了软件组件之间交互规范的重要概念,其促进了代码的解耦、模块化和可扩展性,提供了多态性和抽象的能力,简化了依赖管理和替换,方便进行单元测试和集成测试。这些特性使得接口成为构建可靠、可维护和可扩展的软件系统的关键工具之一。 在现代编程

    2024年02月09日
    浏览(48)
  • go中string的底层是如何实现的

    今天来了解一下go语言的string的底层是怎么实现的,我们知道java的string是一个类实现的,其实go也是类似的,go使用结构体实现了string。 string的底层实现 go的string源码位于 src/runtime/string.go 中,通过 stringStruct 定义了string的数据结构 当我们在赋值一个字符串的时候发生了什么?

    2024年02月16日
    浏览(37)
  • Go语言网络编程:HTTP服务端之底层原理与源码分析——http.HandleFunc()、http.ListenAndServe()

    在 Golang只需要几行代码便能启动一个 http 服务,在上述代码中,完成了两件事: 调用 http.HandleFunc 方法,注册了对应于请求路径 /ping 的 handler 函数 调用 http.ListenAndServe,启动了一个端口为 8999 的 http 服务 2.1 server 结构 Addr :表示服务器监听的地址。如\\\":8080\\\"表示服务器在本地

    2024年02月08日
    浏览(62)
  • 深入理解 Go 语言中的 iota

    iota是go语言的常量计数器,只能在常量表达式中使用,iota在const出现时将被重置为0,const中每新增一行常量声明将使iota计数一次,可理解为const语句块中的行索引。它会自动递增,从0开始。 尽管默认步长为1,但我们也可以通过在常量声明中显式赋值来修改 iota 的步长

    2024年02月10日
    浏览(47)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包