首页 > 其他分享 >GO 语言中 chan 的理解

GO 语言中 chan 的理解

时间:2023-07-03 19:35:02浏览次数:41  
标签:语言 队列 goroutine chan sudog bool GO 等待

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 中传递等待

完整的发送和接受方法实现如下:

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
}

标签:语言,队列,goroutine,chan,sudog,bool,GO,等待
From: https://www.cnblogs.com/orchidzjl/p/17523772.html

相关文章

  • Go-使用反射创建并操作结构体
    typeUserstruct{UserIdstringNamestring}funcTestReflectStructPtr(){var(model*Userstreflect.Typeelemreflect.Value}st=reflect.Type(model)//获取结构体指针类*Userst=st.Elem()//使st=......
  • How to use handleChange() function in react component?
    An onChange eventistriggeredwhenvaluesareenteredintheinput.Thisfiresafunction handleChange(),thatisusedtosetanewstatefortheinput.1.HandlingSingleInputFirst,wehavetosetuptheinputfieldasacontrolledcomponentsothatw......
  • C语言运算符优先级
    在C语言中,运算符优先级是一个比较麻烦的概念,如果搞不清楚优先级可能会产生一些难以察觉的错误第一优先级:[]().->第一优先级包括方括号,圆括号,对象,对象指针第二优先级:-~++–*&!(类型)sizeof第二优先级包括取负,按位取反,自增,自减,取值运算符,取地址符,逻辑非运算符,强制......
  • Django自身提供测试类、工具-调研
    Django自身提供测试类、工具django.test.Client他的作用是模拟客户端。提供一系列的方法,例如get、post、delete、login等其中login是用django自身的验证,特殊之处是实例化的Client可以拿到session、cookie【Client.cookies、Client.session】SimpleTestCase非数据库查询的T......
  • django.db.models.query.QuerySet格式的数据输出
    1、deffindmtm2(request):importserializerimportjson#多对多跨表正向查询#res=softlist.objects.filter(hostlists__ip="10.116.6.177").values("softname")res2=softlist.objects.filter(hostlists__ip="10.116.9.233"......
  • golang解决go get下载失败解决办法
    原因:所下载的库依赖有官方库,而官方被封禁网导致。方法:设置代理goenv-wGOPROXY=https://goproxy.cnps:go1.11发布后,还发布一个goproxy提供代理服务,goproxy.cn是专门服务于中国的,依赖于七牛云github地址:https://github.com/goproxy......
  • C语言(2)
    目录6.数组6.1数组的概念6.2一维数组6.3排序问题6.3.1插入排序6.3.2冒泡排序6.3.3选择排序6.4二分查找6.5字符数组6.6二维数组6.数组6.1数组的概念一组具有相同类型,相同含义的数据类型的有序集合。数组不是基本类型,是构造类型。数组的本质/数组的存储方式:一片地址连续的空间......
  • 电脑迷宫鼠(Java语言实现)
    电脑迷宫鼠基础要求1.概述:用java面向对象程序设计语言,设计和实现一电脑鼠走迷宫的软件程序,即一个假想的小车能在图示的迷宫中根据设定的起始点和终点自主寻找路径。本综合实践分成两部分:第一部分为算法设计和实现部分,第二部分为界面展现部分。2.第一部......
  • go语言结构体
    结构体Go语言的结构体有点像面向对象语言中的“类”,但不完全是,Go语言也没打算真正实现面向对象范式。定义使用type定义结构体,可以把结构体看做类型使用。必须指定结构体的字段(属性)名称和类型。typeUserstruct{idintname,addrstring//多个字段类型相同可......
  • C语言(1)
    目录1.数据类型1.1基本类型1.2构造类型1.3指针类型1.4空类型(void)2.变量和常量2.1C语言中常量的表示方式2.2数据在内存中的存储2.3C语言中变量的表示方式2.3.1变量的定义2.3.2变量的含义3.整数之间的赋值问题3.1长的赋值给短的3.2短的赋值给长的4.运算符与表达式4.1运算符的分类4.2......