首页 > 其他分享 >字节二面:你怎么理解信道是golang中的顶级公民

字节二面:你怎么理解信道是golang中的顶级公民

时间:2025-01-06 13:00:22浏览次数:1  
标签:二面 字节 队列 sendq goroutine 阻塞 golang 信道 mysg

1. 信道是golang中的顶级公民

goroutine结合信道channel是golang中实现并发编程的标配。

信道给出了一种不同于传统共享内存并发通信的新思路,以一种通道复制的思想解耦了并发编程的各个参与方。

信道分为两种: 无缓冲和有缓冲信道(先入先出)。

分别用于goroutine同步和异步生产消费:

无缓冲信道: 若没有反向的goroutine在做动作, 当前goroutine会阻塞;
有缓冲信道: goroutine 直接面对的是缓冲队列, 队列满则写阻塞, 队列空则读阻塞。

一个陷阱: 信道被关闭后, 原来的goroutine阻塞状态不会维系, 能从信道读取到零值。

image.png

for range可以用于信道 :
一直从指定信道中值, 没有数据会阻塞, 直到信道关闭会自动退出循环。

var ch chan int = make(chan int, 10)
go func() {
	for i := 0; i < 20; i++ { 
		ch <- i
	}
	close(ch)
}()

time.Sleep(time.Second * 2)
for ele := range ch {
	fmt.Println(ele)
}

output: 0,1,2,3,4...19

image.png

上面的示例描述了信道4个阶段:
写完10个数据(阻塞写)、暂停2s、
读取10个数据(解除阻塞写)、读完20个数据、关闭信道。

2. 信道channel实现思路大盘点

channel是指向hchan结构体的指针.

        type hchan struct {
        	qcount   uint           // 队列中已有的缓存元素的数量
        	dataqsiz uint           // 环形队列的容量
        	buf      unsafe.Pointer // 环形队列的地址
        	elemsize uint16
        	closed   uint32        // 标记是否关闭,初始化为0,一旦close(ch)为1
        	elemtype *_type // 元素类型
        	sendx    uint   // 待发送的元素索引
        	recvx    uint   // 待接受元素索引
        	recvq    waitq  // 阻塞等待的读goroutine队列
        	sendq    waitq  // 阻塞等待的写gotoutine队列
         
        	// 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
        }
        
    type waitq struct {  
        first *sudog  
        last *sudog  
    }

image.png

2.1 静态全局解读

两个核心的结构

① 环形队列buf (buf、dataqsize、sendx、recvx 圈定了一个有固定长度,由读/写指针控制队列数据的环形队列),从这看出队列是以链表实现。

② 存放阻塞写G和阻塞读G的队列sendqrecvq, recvq、sendq存放的不是当前通信的goroutine, 而是因读写信道而阻塞的goroutine:

  • 如果 qcount <dataqsiz(队列未满),sendq就为空(写就不会阻塞);
  • 如果 qcount >0 (队列不为空),recvq就为空(读就不会阻塞)。

一旦解除阻塞,读/写动作会给到先进入阻塞队列的goroutine,也就是 recvq、sendq也是先进先出。

2.2 动态解读demo

以第一部分的demo为例:

第一阶段: 写入0到9这个10个元素

  1. goroutine在写数据之后会获取锁,以确保安全地修改信道底层的hchan结构体;
  2. 向环形队列buf入队enqueue元素,实际是将原始数据拷贝进环形队列buf的待插入位置sendx
  3. 入队操作完成,释放锁。

image.png

第二阶段:信道满,写阻塞(写goroutine会停止,并等待读操作唤醒)

① 基于写goroutine创建sudog, 并将其放进sendq队列中;

② 调用gopark函数,让调度器P终止该goroutine执行。

调度器P将该goroutine状态改为waiting, 并从调度器P挂载的runQueue中移除,调度器P重新出队一个G交给OS线程来执行,这就是上下文切换,G被阻塞了而不是OS线程。

image.png


读goroutine开始被调度执行:

第三阶段: 读前10个元素(解除写阻塞)

  1. for range chan: 读goroutine从buf中出队元素: 将信道元素拷贝到目标接收区;
  2. 写goroutine从sendq中出队,因为现在信道不满,写不会阻塞;
  3. 调度器P调用goready, 将写goroutine状态变为runnable,并移入runQueue。

image.png
下面的源码截取自chansend()
体现了写信道--> 写阻塞---> 被唤醒的过程

     // 这一部分是写数据, 从这里也可以看出是点对点的覆写,原buf内队列元素不用移动, 只用关注sendx  
     
        if c.qcount < c.dataqsiz {  // 信道未满,则写不会阻塞=>senq为空	
                qp := chanbuf(c, c.sendx)   // chanbuf(c, i) 返回的是信道buf中待插入的位置指针
                typedmemmove(c.elemtype, qp, ep)  
                c.sendx++
                if c.sendx == c.dataqsiz {
                     c.sendx = 0
                }
                c.qcount++
                return true
        }
        if !block {       // 用于select case结构中,不阻塞select case的选择逻辑
                unlock(&c.lock)
                return false
        }

  // 这二部分是: 构建sudog,放进写阻塞队列,阻塞当前写gooroutine的执行
        // Block on the channel. Some receiver will complete our operation for us.
        gp := getg()     // 获取当前的goroutine  https://go.dev/src/runtime/HACKING
        mysg := acquireSudog()   // sudog是等待队列sendq中的元素,封装了goroutine
        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.isSelect = false
        mysg.c = c
        gp.waiting = mysg
        gp.param = nil
        c.sendq.enqueue(mysg)  // 当前goroutine压栈sendq
        // Signal to anyone trying to shrink our stack that we're about
        // to park on a channel. The window between when this G's status
        // changes and when we set gp.activeStackChans is not safe for
        // stack shrinking.
        gp.parkingOnChan.Store(true)
        reason := waitReasonChanSend

        gopark(chanparkcommit, unsafe.Pointer(&c.lock), reason, traceBlockChanSend, 2)   // 这里是阻塞函数
    	
        KeepAlive(ep)
 // 这三部分: 调度器唤醒了当前goroutine
        // someone woke us up.  
        if mysg != gp.waiting {
                throw("G waiting list is corrupted")
        }
        gp.waiting = nil
        gp.activeStackChans = false
        closed := !mysg.success
        gp.param = nil
        if mysg.releasetime > 0 {
                blockevent(mysg.releasetime-t0, 2)
        }
        mysg.c = nil
        releaseSudog(mysg)
        if closed {     // 已经关闭了,再写数据会panic
             if c.closed == 0 {
                 throw("chansend: spurious wakeup")
             }
            panic(plainError("send on closed channel"))
        }
        return true

其中:

getg 获取当前的goroutine,sudog是goroutine的封装,表征一个因读写信道而阻塞的G,

typedmemmove(c.elemtype, qp, ep): 写数据到信道buf,由两个指针来完成拷贝覆写。

  //  typedmemmove copies a value of type typ to dst from src.
    func typedmemmove(typ *abi.Type, dst, src unsafe.Pointer) {
    	if dst == src {
    		return
    	}
    	if writeBarrier.enabled && typ.Pointers() {
    		// This always copies a full value of type typ so it's safe
    		// to pass typ along as an optimization. See the comment on
    		// bulkBarrierPreWrite.
    		bulkBarrierPreWrite(uintptr(dst), uintptr(src), typ.PtrBytes, typ)
    	}
    	// There's a race here: if some other goroutine can write to
    	// src, it may change some pointer in src after we've
    	// performed the write barrier but before we perform the
    	// memory copy. This safe because the write performed by that
    	// other goroutine must also be accompanied by a write
    	// barrier, so at worst we've unnecessarily greyed the old
    	// pointer that was in src.
    	memmove(dst, src, typ.Size_)
    	if goexperiment.CgoCheck2 {
    		cgoCheckMemmove2(typ, dst, src, 0, typ.Size_)
    	}
    }

③ 我们看上面源码的第三部分, 唤醒了阻塞的写goroutine, 但是这里貌似没有将写goroutine携带的值传递给信道或对端。
实际上这个行为是在recv函数内。

跟一下接收方:读第一个元素,刚解除写阻塞的源码:

// 发现sendq有阻塞的写G,则读取,并使用该写G携带的数据填充数据
// Just found waiting sender with not closed.
    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
}
if c.qcount > 0 {  // 如果sendq队里没有阻塞G, 则直接从队列中读值
    // Receive directly from queue
}

---

{
    // Queue is full. Take the item at the
    // head of the queue. Make the sender enqueue
    // its item at the tail of the queue. Since the
    // queue is full, those are both the same slot.
    qp := chanbuf(c, c.recvx)  // 拿到buf中待接受元素指针
    if raceenabled {
            racenotify(c, c.recvx, nil)
            racenotify(c, c.recvx, sg)
    }
    // copy data from queue to receiver
    if ep != nil {
            typedmemmove(c.elemtype, ep, qp)  // 将buf中待接收元素qp拷贝到目标指针ep
    }
    // copy data from sender to queue
    typedmemmove(c.elemtype, qp, sg.elem)  //  将阻塞sendq队列中出站的sudog携带的值写入到待插入指针。
    c.recvx++
    if c.recvx == c.dataqsiz {
            c.recvx = 0
    }
    c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
}
        

从上线源码可以验证:

读goroutine读取第一个元素之前,信道满,此时sendx=recvx,也即信道内读写指针指向同一个槽位;

② 读取第一个元素,解除写阻塞: sendq写G队列会出队第一个sudog, 将其携带的元素填充进buf待插入指针sendx,因为此时sendx=recvx,故第二次typedmemmove(c.elemtype, qp, sg.elem)是合理的。

如果sendq队列没有阻塞G, 则直接从buf中读取值。

3. 不要使用共享内存来通信,而是使用通信来共享内存

常见的后端java C#标配使用共享内存来通信, 比如 mutex、lock 关键词:
通过对一块共有的区域做属性变更来反映系统当前的状态,详细的请搜索同步索引块

golang 推荐使用通信来共享内存, 这个是怎么理解的呢?

你要想使用某块内存数据, 并不是直接共享给你, 而是给你一个信道作为访问的接口, 并且你得到的是目标数据的拷贝,由此形成的信道访问为通信方式;

而原始的目标数据的生命周期由产生这个数据的G来决定, 它甚至不用care自己是不是要被其他G获知,因此体现了解耦并发编程参与方的作用。

https://medium.com/womenintechnology/exploring-the-internals-of-channels-in-go-f01ac6e884dc

4. 信道的实践指南

4.1 无缓冲信道

结合了通信(值交换)和同步。

c := make(chan int)  // Allocate a channel.
// Start the sort in a goroutine; when it completes, signal on the channel.
go func() {
    list.Sort()
    c <- 1  // Send a signal; value does not matter.
}()
doSomethingForAWhile()
<-c   // Wait for sort to finish; discard sent value.

4.2 有缓冲信道

基础实践: 信号量、限流能力

下面演示了:服务端使用有缓冲信道限制并发请求

var sem = make(chan int, MaxOutstanding) 

func Serve(queue chan *Request) {
    for req := range queue {
        req:= req
        sem <- 1   
        go func() {   // 只会开启MaxOutstanding个并发协程
            process(req)
            <-sem
        }()
    }
}

上面出现了两个信道:
sem 提供了限制服务端并发处理请求的信号量
queue 提供了一个客户端请求队列,起媒介/解耦的作用

解多路复用

多路复用是网络编程中一个耳熟能详的概念,nginx redis等高性能web、内存kv都用到了这个技术 。

这个解多路复用是怎么理解呢?

我们针对上面的服务端,编写客户端请求, 独立的客户端请求被服务端Serve收敛之后, Serve就起到了多路复用的概念,在Request定义resultChan信道,就给每个客户端请求提供了独立获取请求结果的能力, 这便是一种解多路复用。

type Request struct {
    args        []int
    f           func([]int) int
    resultChan  chan int
}
request := &Request{[]int{3, 4, 5}, nil, make(chan int)}

func SendReq(req *Request){
    // Send request
    clientRequests <- request
    // Wait for response.
    fmt.Printf("answer: %d\n", <-request.resultChan)
}

在服务端,定义handler,返回响应结果

// 定义在服务端的处理handler
func sum(a []int) (s int) {
    for _, v := range a {
        s += v
    }
    return
}

func process(req *Request) {
   req.f = sum
   req.resultChan <- req.f(req.args)
}

image.png

基于cpu的并行编程

如果计算可被划分为独立的(不相互依赖的)计算分片,则可以利用信道开启CPU的并行编程能力。

var numCPU = runtime.NumCPU() // number of CPU cores

func (v Vector) DoAll(u Vector) {
    c := make(chan int, numCPU)  // Buffering optional but sensible.
    for i := 0; i < numCPU; i++ {
        go v.DoSome(i*len(v)/numCPU, (i+1)*len(v)/numCPU, u, c)
    }
    
    for i := 0; i < numCPU; i++ {
        <-c    // wait for one task to complete
    }
    // All done.
}

标签:二面,字节,队列,sendq,goroutine,阻塞,golang,信道,mysg
From: https://www.cnblogs.com/JulianHuang/p/18655087

相关文章

  • 参数减少99.5%,媲美全精度FLUX!字节跳动等发布首个1.58-bit FLUX量化模型
    文章链接:https://arxiv.org/pdf/2412.18653项目链接:https://chenglin-yang.github.io/1.58bit.flux.github.io/git主页:https://github.com/Chenglin-Yang亮点分析1.58-bitFLUX,第一个将FLUX视觉Transformer的参数(共119亿)减少99.5%至1.58-bit的量化模型,无需......
  • GoLang 2024 安装激活详细使用教程(激活至2026,实测是永久,亲测!)
    开发工具推荐:GoLang安装激活详细使用教程(激活至2026,实际上永久,亲测!)申明:本教程GoLang补丁、激活码均收集于网络,请勿商用,仅供个人学习使用,如有侵权,请联系作者删除。若条件允许,希望大家购买正版!GoLang是JetBrains公司推出的一款功能强大的GO语言集成开发环境(IDE),凭借其丰富的......
  • golang自带的死锁检测并非银弹
    网上总是能看到有人说go自带了死锁检测,只要有死锁发生runtime就能检测到并及时报错退出,因此go不会被死锁问题困扰。这说明了口口相传知识的有效性是日常值得怀疑的,同时也再一次证明了没有银弹这句话的含金量。这个说法的杀伤力在于它虽然不对,但也不是全错,真真假假很容易让人失去......
  • 即插即用,无痛增强模型生成美感!字节跳动提出VMix:细粒度美学控制,光影、色彩全搞定
    文章链接:https://arxiv.org/pdf/2412.20800代码地址:https://github.com/fenfenfenfan/VMix项目地址:https://vmix-diffusion.github.io/VMix/亮点直击分析并探索现有模型在光影、色彩等细粒度美学维度上生成图像的差异,提出在文本提示中解耦这些属性,并构建一个细粒度......
  • java字节码文件解读
    目录一、前置知识-----栈数据结构(Stack)1.概念2.基本操作3.存储结构实现4.应用场景二、java字节码解读字节码的产生背景字节码的基本结构特点操作数栈和局部变量表局部变量表1.概念2.存储内容3.变量槽(VariableSlot)4.生命周期操作数栈1.概念2.工作原理3.与局......
  • 两个int值,分别对应一个16进制字节高四位和低四位时的转换方法。
    例如:inta=1;intb=2;想要把他们转换成一个16进制QByteArray0x12分别对应高四位和低四位。使用以下方法:inta=1;intb=2;QByteArrayarray=QByteArray(1,(char)((a&0xFF)<<4|(b&0xFF)));原理:a=1&0xFF转换成二进制就是00000001&11111111,每一......
  • 可能是GitHub star星最多的Golang Web框架-Gin初识
    对比目前主流GolangWeb框架对比名称描述star数量GinGin是用Go(Golang)编写的HTTPWeb框架。它具有类似Martini的API,性能要好得多-速度提高了40倍。79.6kFiber用Go编写的受Express启发的Web框架34.4kBeegobeego是一个用于Go编程语言的......
  • delphi djson 类与JSON 互转,与 Java、Golang 一致写法
    前因为什么要开发这个JSON库?原因是delphi官方的json既没有处理null(也叫零值)的问题;举例说明吧:开发者往往需要类与JSON之间进行序列化和反序列化;接下来我们举个例子:Person{id:Int64;//IDname:string;//姓名desc:string;//描述}这样一个类在不......
  • 字节抖音团队基于qwen训练了SAIL-VL
    SAIL-VL是字节跳动抖音内容团队开发的最先进的视觉语言模型(VLM)。SAIL-VL的目标是开发一种高性能的视觉语言模型,便于在移动设备上部署,并确保广大用户的可访问性和可负担性。通过仔细调整数据和训练配方,SAIL-VL证明了即使是小型视觉语言模型也能从数据扩展中显著受益。我们......
  • 字节面试: es怎么提升性能和精准度?(尼恩独家,史上最全)
    本文原文链接文章很长,且持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录博客园版为您奉上珍贵的学习资源:免费赠送:《尼恩Java面试宝典》持续更新+史上最全+面试必备2000页+面试必备+大厂必备+涨薪必备免费赠送:《尼恩技术圣经+高并发系列PDF》,帮你实现技术自由,完......