图解Golang的channel底层原理

废话不多说,直奔主题。

channel的整体结构图

简单说明:

  • buf是有缓冲的channel所特有的结构,用来存储缓存数据。是个循环链表
  • sendxrecvx用于记录buf这个循环链表中的~发送或者接收的~index
  • lock是个互斥锁。
  • recvqsendq分别是接收(<-channel)或者发送(channel <- xxx)的goroutine抽象出来的结构体(sudog)的队列。是个双向链表

源码位于/runtime/chan.go中(目前版本:1.11)。结构体为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
    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。

ch := make(chan int, 3)
复制代码

创建channel实际上就是在内存中实例化了一个hchan的结构体,并返回一个ch指针,我们使用过程中channel在函数之间的传递都是用的这个指针,这就是为什么函数传递中无需使用channel的指针,而直接用channel就行了,因为channel本身就是一个指针。

channel中发送send(ch <- xxx)和recv(<- ch)接收

先考虑一个问题,如果你想让goroutine以先进先出(FIFO)的方式进入一个结构体中,你会怎么操作? 加锁!对的!channel就是用了一个锁。hchan本身包含一个互斥锁mutex

channel中队列是如何实现的

channel中有个缓存buf,是用来缓存数据的(假如实例化了带缓存的channel的话)队列。我们先来看看是如何实现“队列”的。 还是刚才创建的那个channel

ch := make(chan int, 3)
复制代码

当使用send (ch <- xx)或者recv ( <-ch)的时候,首先要锁住hchan这个结构体。

然后开始send (ch <- xx)数据。 一

ch <- 1
复制代码

ch <- 1
复制代码

ch <- 1
复制代码

这时候满了,队列塞不进去了 动态图表示为:

然后是取recv ( <-ch)的过程,是个逆向的操作,也是需要加锁。

然后开始recv (<-ch)数据。 一

<-ch
复制代码

<-ch
复制代码

<-ch
复制代码

图为:

注意以上两幅图中bufrecvx以及sendx的变化,recvxsendx是根据循环链表buf的变动而改变的。 至于为什么channel会使用循环链表作为缓存结构,我个人认为是在缓存列表在动态的sendrecv过程中,定位当前send或者recvx的位置、选择send的和recvx的位置比较方便吧,只要顺着链表顺序一直旋转操作就好。

缓存中按链表顺序存放,取数据的时候按链表顺序读取,符合FIFO的原则。

send/recv的细化操作

注意:缓存链表中以上每一步的操作,都是需要加锁操作的!

每一步的操作的细节可以细化为:

  • 第一,加锁
  • 第二,把数据从goroutine中copy到“队列”中(或者从队列中copy到goroutine中)。
  • 第三,释放锁

每一步的操作总结为动态图为:(发送过程)

或者为:(接收过程)

所以不难看出,Go中那句经典的话:Do not communicate by sharing memory; instead, share memory by communicating.的具体实现就是利用channel把数据从一端copy到了另一端! 还真是符合channel的英文含义:

当channel缓存满了之后会发生什么?这其中的原理是怎样的?

使用的时候,我们都知道,当channel缓存满了,或者没有缓存的时候,我们继续send(ch <- xxx)或者recv(<- ch)会阻塞当前goroutine,但是,是如何实现的呢?

我们知道,Go的goroutine是用户态的线程(user-space threads),用户态的线程是需要自己去调度的,Go有运行时的scheduler去帮我们完成调度这件事情。关于Go的调度模型GMP模型我在此不做赘述,如果不了解,可以看我另一篇文章(Go调度原理)

goroutine的阻塞操作,实际上是调用send (ch <- xx)或者recv ( <-ch)的时候主动触发的,具体请看以下内容:

//goroutine1 中,记做G1

ch := make(chan int, 3)

ch <- 1
ch <- 1
ch <- 1
复制代码

这个时候G1正在正常运行,当再次进行send操作(ch<-1)的时候,会主动调用Go的调度器,让G1等待,并从让出M,让其他G去使用

同时G1也会被抽象成含有G1指针和send元素的sudog结构体保存到hchan的sendq中等待被唤醒。

那么,G1什么时候被唤醒呢?这个时候G2隆重登场。

G2执行了recv操作p := <-ch,于是会发生以下的操作:

G2从缓存队列中取出数据,channel会将等待队列中的G1推出,将G1当时send的数据推到缓存中,然后调用Go的scheduler,唤醒G1,并把G1放到可运行的Goroutine队列中。

假如是先进行执行recv操作的G2会怎么样?

你可能会顺着以上的思路反推。首先:

这个时候G2会主动调用Go的调度器,让G2等待,并从让出M,让其他G去使用。 G2还会被抽象成含有G2指针和recv空元素的sudog结构体保存到hchan的recvq中等待被唤醒

此时恰好有个goroutine G1开始向channel中推送数据 ch <- 1。 此时,非常有意思的事情发生了:

G1并没有锁住channel,然后将数据放到缓存中,而是直接把数据从G1直接copy到了G2的栈中。 这种方式非常的赞!在唤醒过程中,G2无需再获得channel的锁,然后从缓存中取数据。减少了内存的copy,提高了效率。

之后的事情显而易见:

更多精彩内容,请关注我的微信公众号 互联网技术窝 或者加微信共同探讨交流:

参考文献:

转载于:https://juejin.im/post/5cb3445f6fb9a068b748ab75

<think>嗯,用户想了解Golang中HTTP模块的底层实现原理。我需要先回忆一下Go的net/http包的结构和底层机制。首先,HTTP模块分为客户端和服务端,用户可能想知道两者的实现,但问题里没有特别说明,可能需要都涵盖一下。 记得Go的HTTP服务端是基于goroutine和epoll(或者kqueue等)的事件驱动模型。每个连接进来后,会有一个goroutine处理。需要解释底层是如何监听端口,接受连接,以及处理请求的。比如,使用net.Listener来监听端口,然后进入循环接受连接,每个连接交给单独的goroutine处理。 客户端方面,可能需要提到Transport和Client的结构。Transport负责管理连接池,复用TCP连接,处理HTTP请求的发送和响应的接收。这里涉及到连接复用的机制,比如Keep-Alive,以及如何管理空闲连接。 还要考虑HTTP协议解析的部分,比如Request和Response的结构是如何解析的,可能涉及到Reader和Writer接口的使用。比如,服务端读取请求时,使用bufio.Reader来解析HTTP头部和正文,然后构造http.Request对象。 另外,中间件和处理器链路的实现也是重要部分。比如,ServeMux作为路由处理器,如何匹配URL路径,调用对应的HandlerFunc。还有中间件的实现,通过装饰器模式层层包装Handler。 底层网络库可能涉及到非阻塞I/O和事件循环,但Go通过goroutine简化了这一点,每个连接一个goroutine,同步处理,但底层可能还是用到了非阻塞的IO多路复用,比如在netpoller中。需要解释Go的运行时如何管理这些网络请求,避免阻塞。 可能还需要提到一些性能优化点,比如连接复用、减少内存分配、goroutine的轻量级特性带来的高并发能力。引用中的其他底层实现,比如channel、map、slice的实现,可能和HTTP模块的关系不大,但可以提到在处理并发请求时,goroutine之间的通信可能用到channel,但HTTP模块本身更多是处理网络IO。 用户的问题可能还涉及到如何高效处理大量并发连接,Go的并发模型如何支持这一点,比如每个连接一个goroutine,利用多核CPU,而netpoller负责将IO事件通知给goroutine,避免线程阻塞。 最后,需要确保回答结构清晰,分点说明服务端和客户端的实现原理,并结合底层数据结构、goroutine调度、连接管理等方面。可能还需要生成相关问题,比如HTTP模块如何处理高并发、性能优化方法、路由匹配机制等。</think>### Golang HTTP模块底层实现原理分步解析 #### 一、服务端实现核心机制 1. **网络监听层** - 使用$socket()$系统调用创建监听套接字,通过$bind()$和$listen()$完成端口绑定与监听 - 基于`epoll/kqueue`的I/O多路复用机制(通过Go运行时netpoller实现实现非阻塞事件驱动[^2] - 每个TCP连接对应一个`net.Conn`结构体,底层通过`runtime.netpoll`实现高效事件通知 2. **连接处理模型** - 每个新连接创建独立goroutine处理 - 通过状态机解析HTTP协议: $$ \text{请求行} \rightarrow \text{请求头} \rightarrow \text{请求体} \rightarrow \text{响应生成} $$ - 使用`bufio.Reader`缓冲读取数据,通过有限状态机解析HTTP报文 3. **路由与处理器** - `ServeMux`实现路由匹配树结构 - 默认采用`path.Clean()`规范化URL路径 - 中间件通过`HandlerFunc`链式调用实现: $$ \text{Handler}_1(\text{Handler}_2(\text{Handler}_3(...))) $$ #### 二、客户端实现机制 1. **连接池管理** - `Transport`对象维护TCP连接池,默认最大空闲连接数100 - 支持HTTP/1.1 Keep-Alive和HTTP/2多路复用 - 空闲连接超时机制通过`IdleConnTimeout`控制(默认90秒) 2. **请求处理流程** ```go // 典型请求生命周期 Transport.GetConn() // 获取连接 -> writeRequest() // 写入请求 -> readResponse() // 读取响应 -> Transport.PutConn() // 回收连接 ``` #### 三、关键数据结构 1. **服务端核心结构** ```go type Server struct { Addr string Handler Handler // 处理器接口 ReadTimeout time.Duration WriteTimeout time.Duration TLSConfig *tls.Config } ``` 2. **请求上下文** ```go type Request struct { Method string URL *url.URL Proto string Header Header Body io.ReadCloser ContentLength int64 Host string // ... 其他元数据 } ``` #### 四、性能优化设计 1. **内存重用机制** - 通过`sync.Pool`缓存常用对象(如`bufio.Reader`) - 响应对象复用避免重复内存分配 2. **零拷贝优化** - 使用`sendfile`系统调用直接传输文件 - `io.Copy()`实现缓冲区复用
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值