返回顶部
首页 > 资讯 > 后端开发 > GO >Golang通道channel的源码分析
  • 866
分享到

Golang通道channel的源码分析

Golang通道channelGolang通道Golang channel 2022-12-08 20:12:07 866人浏览 泡泡鱼
摘要

目录前言channel基础结构channel初始化channel发送channel接收小结前言 channel是golang中标志性的概念之一,很好很强大! channel(通道),

前言

channel是golang中标志性的概念之一,很好很强大!

channel(通道),顾名思义,是一种通道,一种用于并发环境中数据传递的通道。通常结合Golang中另一重要概念goroutine(go协程)使用,使得在golang中的并发编程变得清晰简洁同时又高效强大。

今天尝试着读读golang对channel的实现源码,本文主要是自己个人对于Channel源码的学习笔记,需要的朋友可以参考以下内容,希望对大家有帮助。

channel基础结构

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
}

hchan结构就是channel的底层数据结构,看源码定义,可以说是非常清晰了。

  • qcount:channel缓存队列中已有的元素数量
  • dataqsiz:channel的缓存队列大小(定义channel时指定的缓存大小,这里channel用的是一个环形队列)
  • buf:指向channel缓存队列的指针
  • elemsize:通过channel传递的元素大小
  • closed:channel是否关闭的标志
  • elemtype:通过channel传递的元素类型
  • sendx:channel中发送元素在队列中的索引
  • recvx:channel中接受元素在队列中的索引
  • recvq:等待从channel中接收元素的协程列表
  • sendq:等待向channel中发送元素的协程列表
  • lock:channel上的

其中关于recvq和sendq的两个列表所用的结构waitq简单看下。

type waitq struct {
    first *sudog
    last  *sudog
}
type sudog struct {
    g          *g
    selectdone *uint32 // CAS to 1 to win select race (may point to stack)
    next       *sudog
    prev       *sudog
    elem       unsafe.Pointer // data element (may point to stack)
...
    c           *hchan // channel
}

可以看出waiq是一个双向链表结构,链上的节点是sudog。从sudog的结构定义可以粗略看出,sudog是对g(即协程)的一个封装。用于记录一个等待在某个channel上的协程g、等待的元素elem等信息。

channel初始化

func makechan(t *chantype, size int64) *hchan {
    elem := t.elem
    // compiler checks this but be safe.
    if elem.size >= 1<<16 {
        throw("makechan: invalid channel element type")
    }
    if hchanSize%maxAlign != 0 || elem.align > maxAlign {
        throw("makechan: bad alignment")
    }
    if size < 0 || int64(uintptr(size)) != size || (elem.size > 0 && uintptr(size) > (_MaxMem-hchanSize)/elem.size) {
        panic(plainError("makechan: size out of range"))
    }
    var c *hchan
    if elem.kind&kindNoPointers != 0 || size == 0 {
        // Allocate memory in one call.
        // Hchan does not contain pointers interesting for GC in this case:
        // buf points into the same allocation, elemtype is persistent.
        // SudoG's are referenced from their owning thread so they can't be collected.
        // TODO(dvyukov,rlh): Rethink when collector can move allocated objects.
        c = (*hchan)(mallocgc(hchanSize+uintptr(size)*elem.size, nil, true))
        if size > 0 && elem.size != 0 {
            c.buf = add(unsafe.Pointer(c), hchanSize)
        } else {
            // race detector uses this location for synchronization
            // Also prevents us from pointing beyond the allocation (see issue 9401).
            c.buf = unsafe.Pointer(c)
        }
    } else {
        c = new(hchan)
        c.buf = newarray(elem, int(size))
    }
    c.elemsize = uint16(elem.size)
    c.elemtype = elem
    c.dataqsiz = uint(size)
    if debugChan {
        print("makechan: chan=", c, "; elemsize=", elem.size, "; elemalg=", elem.alg, "; dataqsiz=", size, "\n")
    }
    return c
}

第一部分的3个if是对初始化参数的合法性检查。

if elem.size >= 1<<16:

检查channel元素大小,小于2字节

if hchanSize%maxAlign != 0 || elem.align > maxAlign

没看懂(对齐?)

if size < 0 || int64(uintptr(size)) != size || (elem.size > 0 && uintptr(size) > (_MaxMem-hchanSize)/elem.size)

第一个判断缓存大小需要大于等于0

int64(uintptr(size)) != size这一句实际是用于判断size是否为负数。由于uintptr实际是一个无符号整形,负数经过转换后会变成一个与原数完全不同的很大的正整数,而正数经过转换后并没有变化。

最后一句判断channel的缓存大小要小于heap中能分配的大小。_MaxMem是可分配的堆大小。

第二部分是具体的内存分配。

元素类型为kindNoPointers的时候,既非指针类型,则直接分配(hchanSize+uintptr(size)*elem.size)大小的连续空间。c.buf指向hchan后面的elem队列首地址。

如果channel缓存大小为0,则c.buf实际上是没有给他分配空间的

如果类型为非kindNoPointers,则channel的空间和buf的空间是分别分配的。

channel发送

// entry point for c <- x from compiled code
//go:nosplit
func chansend1(c *hchan, elem unsafe.Pointer) {
    chansend(c, elem, true, getcallerpc(unsafe.Pointer(&c)))
}

channel发送,即协程向channel中发送数据,与此操作对应的go代码如c <- x。

channel发送的实现源码中,通过chansend1(),调用chansend(),其中block参数为true。

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
    if c == nil {
        if !block {
            return false
        }
        gopark(nil, nil, "chan send (nil chan)", traceEvGoStop, 2)
        throw("unreachable")
    }
... 
}

chansend()首先对c进行判断, if c == nil:即channel没有被初始化,这个时候会直接调用gopark使得当前协程进入等待状态。而且用于唤醒的参数unlockf传的nil,即没有人来唤醒它,这样系统进入死锁。所以channel必须被初始化之后才能使用,否则死锁。

接下来是正式的发送处理,且后续操作会加锁。

lock(&c.lock)

close判断

if c.closed != 0 {
        unlock(&c.lock)
        panic(plainError("send on closed channel"))
    }

如果channel已经是closed状态,解锁然后直接panic。也就是说我们不可以向已经关闭的通道内在发送数据。

将数据发给接收协程

if sg := c.recvq.dequeue(); sg != nil {
        // Found a waiting receiver. We pass the value we want to send
        // directly to the receiver, bypassing the channel buffer (if any).
        send(c, sg, ep, func() { unlock(&c.lock) }, 3)
        return true
    }

尝试从接收等待协程队列中取出一个协程,如果有则直接数据发给它。也就是说发送到channel的数据会优先检查接收等待队列,如果有协程等待取数,就直接给它。发完解锁,操作完成。

这里send()方法会将数据写到从队列里取出来的sg中,通过goready()唤醒sg.g(即等待的协程),进行后续处理。

数据放到缓存

if c.qcount < c.dataqsiz {
        // Space is available in the channel buffer. Enqueue the element to send.
        qp := chanbuf(c, c.sendx)
        if raceenabled {
            raceacquire(qp)
            racerelease(qp)
        }
        typedmemmove(c.elemtype, qp, ep)
        c.sendx++
        if c.sendx == c.dataqsiz {
            c.sendx = 0
        }
        c.qcount++
        unlock(&c.lock)
        return true
    }

如果没有接收协程在等待,则去检查channel的缓存队列是否还有空位。如果有空位,则将数据放到缓存队列中。

通过c.sendx游标找到队列中的空余位置,然后将数据存进去。移动游标,更新数据,然后解锁,操作完成。

if c.sendx == c.dataqsiz {
        c.sendx = 0
    }

通过这一段游标的处理可以看出,缓存队列是一个环形。

阻塞发送协程

gp := getg()
    mysg := acquireSudog()
    mysg.releasetime = 0
    if t0 != 0 {
        mysg.releasetime = -1
    }
    // No stack splits between assigning elem and enqueuing mysg
    // on gp.waiting where copystack can find it.
    mysg.elem = ep
    mysg.waitlink = nil
    mysg.g = gp
    mysg.selectdone = nil
    mysg.c = c
    gp.waiting = mysg
    gp.param = nil
    c.sendq.enqueue(mysg)
    goparkunlock(&c.lock, "chan send", traceEvGoBlockSend, 3)

如果缓存也慢了,这时候就只能阻塞住发送协程了, 等有合适的机会了,再将数据发送出去。

getg()获取当前协程对象g的指针,acquireSudog()生成一个sudog,然后将当前协程及相关数据封装好链接到sendq列表中。然年通过goparkunlock()将其转为等待状态,并解锁。操作完成。

channel接收

// entry points for <- c from compiled code
//go:nosplit
func chanrecv1(c *hchan, elem unsafe.Pointer) {
    chanrecv(c, elem, true)
}

channel接收,即协程从channel中接收数据,与此操作对应的go代码如<- c。

channel接收的实现源码中,通过chanrecv1(),调用chanrecv(),其中block参数为true。

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
...
    if c == nil {
        if !block {
            return
        }
        gopark(nil, nil, "chan receive (nil chan)", traceEvGoStop, 2)
        throw("unreachable")
    }
...
}

同发送一样,接收也会首先检查c是否为nil,如果为nil,会调用gopark()休眠当前协程,从而最终造成死锁。

接收操作同样先进行加锁,然后开始正式操作。

close处理

if c.closed != 0 && c.qcount == 0 {
        if raceenabled {
            raceacquire(unsafe.Pointer(c))
        }
        unlock(&c.lock)
        if ep != nil {
            typedmemclr(c.elemtype, ep)
        }
        return true, false
    }

接收和发送略有不同,当channel关闭并且channel的缓存队列里没有数据了,那么接收动作会直接结束,但不会报错。

也就是说,允许从已关闭的channel中接收数据。

从发送等待协程中接收

if sg := c.sendq.dequeue(); sg != nil {
        // Found a waiting sender. If buffer is size 0, receive value
        // directly from sender. Otherwise, receive from head of queue
        // and add sender's value to the tail of the queue (both map to
        // the same buffer slot because the queue is full).
        recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
        return true, true
    }

尝试从发送等待协程列表中取出一个等待协程,如果存在,则调用recv()方法接收数据。

这里的recv()方法比send()方法稍微复杂一点,我们简单分析下。

func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
    if c.dataqsiz == 0 {
        ...
        if ep != nil {
            // copy data from sender
            recvDirect(c.elemtype, sg, ep)
        }
    } else {
        qp := chanbuf(c, c.recvx)
        ...
        // copy data from queue to receiver
        if ep != nil {
            typedmemmove(c.elemtype, ep, qp)
        }
        // copy data from sender to queue
        typedmemmove(c.elemtype, qp, sg.elem)
        c.recvx++
        if c.recvx == c.dataqsiz {
            c.recvx = 0
        }
        c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
    }
    sg.elem = nil
    gp := sg.g
    unlockf()
    gp.param = unsafe.Pointer(sg)
    if sg.releasetime != 0 {
        sg.releasetime = cputicks()
    }
    goready(gp, skip+1)
}

recv()的接收动作分为两种情况:

  • c.dataqsiz == 0:即当channel为无缓存channel时,直接将发送协程中的数据,拷贝给接收者。
  • c.dataqsiz != 0:如果channel有缓存,则:根据缓存的接收游标,从缓存队列中取出一个,拷贝给接受者

小结

channel必须初始化后才能使用;

channel关闭后,不允许在发送数据,但是还可以继续从中接收未处理完的数据。所以尽量从发送端关闭channel;

无缓存的channel需要注意在一个协程中的操作不会造成死锁;

到此这篇关于Golang通道channel的源码分析的文章就介绍到这了,更多相关Golang通道channel内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

您可能感兴趣的文档:

--结束END--

本文标题: Golang通道channel的源码分析

本文链接: https://lsjlt.com/news/174226.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • Golang通道channel的源码分析
    目录前言channel基础结构channel初始化channel发送channel接收小结前言 channel是golang中标志性的概念之一,很好很强大! channel(通道),...
    99+
    2022-12-08
    Golang通道channel Golang通道 Golang channel
  • Golang通道channel使用源码分析
    这篇文章主要介绍了Golang通道channel使用源码分析的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇Golang通道channel使用源码分析文章都会有所收获,下面我们一起来看看吧。前言channel是g...
    99+
    2023-07-04
  • Go语言中通道channel的示例分析
    这篇文章将为大家详细讲解有关Go语言中通道channel的示例分析,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。一、Go语言通道基础概念1.channel产生背景    线程...
    99+
    2023-06-29
  • GoLang channel底层代码分析详解
    目录1. channel 底层结构2. channel 的创建3. channel 的发送流程4. channel 的接收流程5. channel 使用注意事项以下源码都摘自 gol...
    99+
    2024-04-02
  • golang channel管道使用示例解析
    目录定义channel管道channel管道塞值和取值通过channel管道实现同步,和数据交互无缓冲的channel有缓冲的channel管道关闭channel管道单向channe...
    99+
    2024-04-02
  • 通过源码分析Golang cron的实现原理
    目录前言Demo示例源码实现结构体 Cron 和 EntryNew()实现AddFunc()实现Start()实现Run()实现Stop()实现Remove()实现小结前言 gola...
    99+
    2024-04-02
  • Golang中sync.Mutex源码分析
    本篇内容介绍了“Golang中sync.Mutex源码分析”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!Mutex结构type M...
    99+
    2023-07-05
  • Golang源码分析之golang/sync之singleflight
    目录1.背景1.1. 项目介绍1.2.使用方法2.源码分析2.1.项目结构2.2.数据结构2.3.API代码流程3.总结1.背景 1.1. 项目介绍 golang/sync库拓展了官...
    99+
    2022-11-13
    golang/sync golang源码分析 golang singleflight
  • Golang HTTP编程源码分析
    这篇文章主要介绍“Golang HTTP编程源码分析”,在日常操作中,相信很多人在Golang HTTP编程源码分析问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Golang H...
    99+
    2023-07-05
  • Go语言中的通道channel详情
    目录一、Go语言通道基础概念1.channel产生背景2.channel工作方式二、通道使用语法1.通道的声明与初始化2.将数据放入通道内3.从通道内取出数据4.关闭通道close三...
    99+
    2024-04-02
  • Golang Mutex互斥锁源码分析
    目录前言Mutex 特性数据结构Lock()Unlock()前言 在上一篇文章中,我们一起学习了如何使用 Go 中的互斥锁 Mutex,那么本篇文章,我...
    99+
    2024-04-02
  • Golang中Slice使用源码分析
    本文小编为大家详细介绍“Golang中Slice使用源码分析”,内容详细,步骤清晰,细节处理妥当,希望这篇“Golang中Slice使用源码分析”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。1、slice结构体首...
    99+
    2023-07-05
  • golang错误捕获源码分析
    这篇文章主要介绍“golang错误捕获源码分析”,在日常操作中,相信很多人在golang错误捕获源码分析问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”golang错误捕获源码分析”的疑惑有所帮助!接下来,请跟...
    99+
    2023-07-06
  • Netty启动流程服务端channel初始化源码分析
    目录服务端channel初始化回顾上一小节initAndRegister()方法init(Channel)方法前文传送门 Netty分布式server启动流程 服务端cha...
    99+
    2024-04-02
  • Golang中Slice的分析与使用源码解析
    目录1、slice结构体2、slice初始化3、append操作4、slice截取5、slice深拷贝6、值传递还是引用传递参考文献1、slice结构体 首先我们来看一段代码 pac...
    99+
    2023-03-09
    go slice使用 go slice
  • Angular表单、管道、绑定、指令、通信和周期源码分析
    这篇文章主要介绍了Angular表单、管道、绑定、指令、通信和周期源码分析的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇Angular表单、管道、绑定、指令、通信和周期源码分析文章都会有所收获,下面我们一起来看...
    99+
    2023-06-30
  • Java线程通信源代码分析
    本篇内容介绍了“Java线程通信源代码分析”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!其实我们在源代码中就能发现其中的奥秘。因为Threa...
    99+
    2023-06-17
  • LKWA靶场通关和源码分析
    文章目录 一、Blind RCE?二、XSSI三、PHP Object Injection四、PHP Object Injection(cookie)五、PHP Object Injectio...
    99+
    2023-09-06
    php web安全
  • DVWA靶场通关和源码分析
    文章目录 一、Brute Force1.low2、medium3、High4、Impossible 二、Command Injection1、Low2、Medium3、High 三、C...
    99+
    2023-09-11
    php web安全
  • RMAN备份中的通道(CHANNEL)相关 - PARALLELISM 、FILESPERSET
    PARALLELISM ---  我们还可以通过parallelism参数来指定同时"自动"创建多少个通道: RMAN > configure device type disk parallelism 3 ;  表...
    99+
    2023-06-06
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作