二. go 常见数据结构实现原理之 channel

一. 基础

  1. 在前面go 基础入门二十二 channel 管道,总结一下中间的一些小问题点

二. channel 底层原理相关

什么是CSP 模型

  1. CSP 模型: 在讲 channel 之前,有必要先提一下 CSP 模型,

传统的并发模型主要分为 Actor 模型和 CSP 模型,CSP 模型(communicating sequential processes)由并发执行实体(进程,线程或协程),和消息通道组成,实体之间通过消息通道发送消息进行通信。和 Actor 模型不同,CSP 模型关注的是消息发送的载体,即通道,而不是发送消息的执行实体。Go 语言的并发模型参考了 CSP 理论,其中执行实体对应的是 goroutine, 消息通道对应的就是 channe

  1. 说人话 Actor 模型,所有的事物都可以看作是一个个的 Actor, 每个行动者有自己的状态和行为,更关注的是数据本身和状态,而CSP 模型关注的是消息发送的载体,在Actor 模型中通过主要通过锁同步原语等解决并发安全问题, 在CSP中以go举例,通过channel传递消息的通道解决并发安全问题, 这也是golang的一句名言的体现:使用通信来共享内存,而不是通过共享内存来通信

channel 底层的数据结构是什么? hchan 环形队列

  1. 在使用channel时需要通过 make(chan T, cap) 来初始化,如下:
	///1.创建一个可以存放3个int类型数据的channel
	var intChan chan int
	//2.在使用channel是要先make
	intChan = make(chan int, 2)

	//3.或者不指定容量创建无缓冲管道
	myChan = make(chan int)
  1. make函数会在编译时转换为 makechan64 和 makechan,以makeChan为例,查看该函数
  1. 内部会判断元素类型如果不含指针或者 size 大小为 0无缓时只进行一次内存分配
  2. 否则会再次执行new函数,两次分配内存
  3. 最终会返回一个hchan 指针
const (
	//最大对齐值,为了减少CPU读取内存的次数
	maxAlign  = 8
	//channel 结构体的大小。这个值表示 hchan 结构体在内存中所占的大小
	hchanSize = unsafe.Sizeof(hchan{}) + uintptr(-int(unsafe.Sizeof(hchan{}))&(maxAlign-1))
	//是否开启 channel 的调试模式
	debugChan = false
)
func makechan(t *chantype, size int64) *hchan {
    elem := t.elem
    // 省略了检查 channel size,align 的代码
    // ……
    var c *hchan
    // 如果元素类型不含指针 或者 size 大小为 0(无缓冲类型)
    // 只进行一次内存分配
    if elem.kind&kindNoPointers != 0 || size == 0 {
        // 如果 hchan 结构体中不含指针,GC 就不会扫描 chan 中的元素
        // 只分配 "hchan 结构体大小 + 元素大小*个数" 的内存
        c = (*hchan)(mallocgc(hchanSize+uintptr(size)*elem.size, nil, true))
        // 如果是缓冲型 channel 且元素大小不等于 0(大小等于 0的元素类型:struct{})
        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).
            // 1. 非缓冲型的,buf 没用,直接指向 chan 起始地址处
            // 2. 缓冲型的,能进入到这里,说明元素无指针且元素类型为 struct{},也无影响
            // 因为只会用到接收和发送游标,不会真正拷贝东西到 c.buf 处(这会覆盖 chan的内容)
            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)
    // 返回 hchan 指针
    return c
}
  1. 查看hchan结构, 是channel底层的底层结构,是一个环形队列
func makechan(t *chantype, size int) *hchan {
    var c *hchan
    c = new(hchan)
    c.buf = malloc(元素类型大小*size)
    c.elemsize = 元素类型大小
    c.elemtype = 元素类型
    c.dataqsiz = size
    return c
}

type hchan struct {
    qcount   uint           // 当前队列中剩余元素个数
    // 环形队列长度,即可以存放的元素个数,可以缓冲的数量,如 ch := make(chan int, 10)。 此处的 10 即 dataqsiz
    dataqsiz uint          
	closed   uint32 // 关闭状态 
	//当 channel 设置了缓冲数量时,该 buf 指向一个环形缓冲区
    buf      unsafe.Pointer
    elemsize uint16         // 每个元素的大小
    closed   uint32            // 标识关闭状态
    elemtype *_type         // 元素类型
    sendx    uint           // 队列下标,指示元素写入时存放到队列中的位置
    recvx    uint           // 队列下标,指示元素从队列的该位置读出
    recvq    waitq          // 等待读消息的goroutine队列
    sendq    waitq          // 等待写消息的goroutine队列
    lock mutex              // 互斥锁,chan不允许并发读写
}

//双向链表waitq 结构体: 包含一个头结点和一个尾结点
//每个节点是一个sudog结构体变量,记录哪个协程在等待,等待的是哪个channel,等待发送/接收的数据在哪里
type waitq struct {
   first *sudog
   last  *sudog
}

type sudog struct {
    g *g
    next *sudog
    prev *sudog
    elem unsafe.Pointer 
    c        *hchan 
    ...
}

1. 环形队列

在这里插入图片描述

  1. hchan 是一个环形队列, 队列的长度是创建chan时指定的,如果不指定则会创建一个无缓冲通道,hchan 中几个属性的解释
  1. recvq: 等待接收队列,
  2. sendq: 等待发送队列,
  3. elemtype: 元素类型
  4. buf: 环形队列指针
  5. lock: 互斥锁,chan不允许并发读写
  1. channel 在进行读写数据时,会根据无缓冲、有缓冲设置进行对应的阻塞唤起动作
  2. hchan 中的 buf属性: channel分为无缓冲和有缓冲两种
  1. 对于有缓冲的channel存储数据,使用了 ring buffer(环形缓冲区) 来缓存写入的数据,本质是循环数组
  2. 为啥是循环数组?普通数组不行吗,普通数组容量固定更适合指定的空间,弹出元素时,普通数组需要全部都前移,当下标超过数组容量后会回到第一个位置,所以需要有两个字段记录当前读和写的下标位置

2. 等待队列

  1. 首先从channel读数据时,如果channel缓冲区为空或者没有缓冲区,也就是没有数据时,当前goroutine会被阻塞。向channel写数据时,如果channel缓冲区已满或者没有缓冲区,当前goroutine会被阻塞
  2. 等待队列就是指hchan中的recvq等待接收队列属性,与sendq 等待发送队列属性, 类型是waitq结构,包含一个头结点和一个尾结点,每个节点是一个sudog结构体变量,记录哪个协程在等待,等待的是哪个channel,在操作channel读写数据时,被阻塞的goroutine将会挂在channel的等待队列中:
  1. 因读阻塞的goroutine会被向channel写入数据的goroutine唤醒;
  2. 因写阻塞的goroutine会被从channel读数据的goroutine唤醒
  1. 一个没有缓冲区的channel,发送读数据阻塞时的示例,读请求会被阻塞在recvq等待读消息的goroutine队列
    在这里插入图片描述
  2. 注意: 一般情况下recvq和sendq至少有一个为空,只有一个例外那就是同一个goroutine使用select语句向channel一边写数据,一边读数据

3. 类型信息

  1. 一个channel只能传递一种类型的值,类型信息存储在hchan数据结构中(如果实际业务中需要多类型,考虑使用接口)

elemtype代表类型,用于数据传递过程中的赋值;
elemsize代表类型大小,用于在buf中定位元素位置

4. 锁

  1. 在hchan结构中存在一个lock属性,一个channel同时仅允许被一个goroutine读写,为简单起见

5. 创建队列时执行的一些判断策略

  1. 创建时的检查:
  1. 元素的对齐大小不能超过 maxAlign 也就是 8 字节
  2. 计算出来的内存是否超过限制
  1. 创建时的策略:
  1. 如果是无缓冲的 channel,会直接给 hchan 分配内存
  2. 如果是有缓冲的 channel,并且元素不包含指针,那么会为 hchan 和底层数组分配一段连续的地址
  3. 如果是有缓冲的 channel,并且元素包含指针,那么会为 hchan 和底层数组分别分配地址

向channel写数据

  1. Channel 发送和接收元素的本质是什么?: channel 的发送和接收操作本质上都是 “值的拷贝”,无论是从 sender goroutine 的栈到 chan buf,还是从 chan buf 到 receiver goroutine,或者是直接从 sender goroutine 到 receiver goroutine
  2. 执行写操作时,编译时转换为runtime.chansend函数
// 位于 src/runtime/chan.go
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
    // 如果 channel 是 nil
    if c == nil {
        // 不能阻塞,直接返回 false,表示未发送成功
        if !block {
            return false
        }
        // 当前 goroutine 被挂起
        gopark(nil, nil, "chan send (nil chan)", traceEvGoStop, 2)
        throw("unreachable")
    }
    // 省略 debug 相关……
    // 对于不阻塞的 send,快速检测失败场景
    //
    // 如果 channel 未关闭且 channel 没有多余的缓冲空间。这可能是:
    // 1. channel 是非缓冲型的,且等待接收队列里没有 goroutine
    // 2. channel 是缓冲型的,但循环数组已经装满了元素
    if !block && c.closed == 0 && ((c.dataqsiz == 0 && c.recvq.first == nil) ||
        (c.dataqsiz > 0 && c.qcount == c.dataqsiz)) {
        return false
    }
    var t0 int64
    if blockprofilerate > 0 {
        t0 = cputicks()
    }
    // 锁住 channel,并发安全
    lock(&c.lock)
    // 如果 channel 关闭了
    if c.closed != 0 {
        // 解锁
        unlock(&c.lock)
        // 直接 panic
        panic(plainError("send on closed channel"))
    }
    // 如果接收队列里有 goroutine,直接将要发送的数据拷贝到接收 goroutine
    if sg := c.recvq.dequeue(); sg != nil {
        send(c, sg, ep, func() { unlock(&c.lock) }, 3)
        return true
    }
    // 对于缓冲型的 channel,如果还有缓冲空间
    if c.qcount < c.dataqsiz {
        // qp 指向 buf 的 sendx 位置
        qp := chanbuf(c, c.sendx)
        // ……
        // 将数据从 ep 处拷贝到 qp
        typedmemmove(c.elemtype, qp, ep)
        // 发送游标值加 1
        c.sendx++
        // 如果发送游标值等于容量值,游标值归 0
        if c.sendx == c.dataqsiz {
            c.sendx = 0
        }
        // 缓冲区的元素数量加一
        c.qcount++
        // 解锁
        unlock(&c.lock)
        return true
    }
    // 如果不需要阻塞,则直接返回错误
    if !block {
        unlock(&c.lock)
        return false
    }
    // channel 满了,发送方会被阻塞。接下来会构造一个 sudog
    // 获取当前 goroutine 的指针
    gp := getg()
    mysg := acquireSudog()
    mysg.releasetime = 0
    if t0 != 0 {
        mysg.releasetime = -1
    }
    mysg.elem = ep
    mysg.waitlink = nil
    mysg.g = gp
    mysg.selectdone = nil
    mysg.c = c
    gp.waiting = mysg
    gp.param = nil
    // 当前 goroutine 进入发送等待队列
    c.sendq.enqueue(mysg)
    // 当前 goroutine 被挂起
    goparkunlock(&c.lock, "chan send", traceEvGoBlockSend, 3)
    // 从这里开始被唤醒了(channel 有机会可以发送了)
    if mysg != gp.waiting {
        throw("G waiting list is corrupted")
    }
    gp.waiting = nil
    if gp.param == nil {
        if c.closed == 0 {
            throw("chansend: spurious wakeup")
        }
        // 被唤醒后,channel 关闭了。坑爹啊,panic
        panic(plainError("send on closed channel"))
    }
    gp.param = nil
    if mysg.releasetime > 0 {
        blockevent(mysg.releasetime-t0, 2)
    }
    // 去掉 mysg 上绑定的 channel
    mysg.c = nil
    releaseSudog(mysg)
    return true
}
  1. 在上面我们已经知道了channel底层数据结构是一个hchan环形链表,并且知道了内部的几个属性
  2. 首先会进行一下判断
  1. 如果检测到 channel 是空的,当前 goroutine 会被挂起。
  2. 如果检测到 channel 已经关闭,直接 panic
  3. 如果能从等待接收队列 recvq 里出队一个 sudog(代表一个 goroutine),说明此时 channel 是空的,没有元素,所以才会有等待接收者。这时会调用 send 函数将元素直接从发送者的栈拷贝到接收者的栈,关键操作由 sendDirect 函数完成
// send 函数处理向一个空的 channel 发送操作
// ep 指向被发送的元素,会被直接拷贝到接收的 goroutine
// 之后,接收的 goroutine 会被唤醒
// c 必须是空的(因为等待队列里有 goroutine,肯定是空的)
// c 必须被上锁,发送操作执行完后,会使用 unlockf 函数解锁
// sg 必须已经从等待队列里取出来了
// ep 必须是非空,并且它指向堆或调用者的栈
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
    // 省略一些用不到的
    // ……
    // sg.elem 指向接收到的值存放的位置,如 val <- ch,指的就是 &val
    if sg.elem != nil {
        // 直接拷贝内存(从发送者到接收者)
        sendDirect(c.elemtype, sg, ep)
        sg.elem = nil
    }
    // sudog 上绑定的 goroutine
    gp := sg.g
    // 解锁
    unlockf()
    gp.param = unsafe.Pointer(sg)
    if sg.releasetime != 0 {
        sg.releasetime = cputicks()
    }
    // 唤醒接收的 goroutine. skip 和打印栈相关,暂时不理会
    goready(gp, skip+1)
}
  1. 继续看 sendDirect 函数
// 向一个非缓冲型的 channel 发送数据、从一个无元素的(非缓冲型或缓冲型但空)的 channel
// 接收数据,都会导致一个 goroutine 直接操作另一个 goroutine 的栈
// 由于 GC 假设对栈的写操作只能发生在 goroutine 正在运行中并且由当前 goroutine 来写
// 所以这里实际上违反了这个假设。可能会造成一些问题,所以需要用到写屏障来规避
func sendDirect(t *_type, sg *sudog, src unsafe.Pointer) {
    // src 在当前 goroutine 的栈上,dst 是另一个 goroutine 的栈
    // 直接进行内存"搬迁"
    // 如果目标地址的栈发生了栈收缩,当我们读出了 sg.elem 后
    // 就不能修改真正的 dst 位置的值了
    // 因此需要在读和写之前加上一个屏障
    dst := sg.elem
    typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.size)
    memmove(dst, src, t.size)
}
  1. channel写数据简单过程如下
  1. 首先会判断等待读取消息队列recvq,如不不为空,说明缓冲区中没有数据或者没有缓冲区(也就是说前面有读取阻塞的协程),此时直接从recvq取出这个阻塞的协程G,写入数据,并执行该协程的goready函数,设置等待下次调度运行,也就是唤醒阻塞的读协程执行,结束发送过程
  2. 等待读取消息队列recvq为空,此时判断buf缓冲区中是否有空余位置,如果有将数据写入缓冲区,结束发送过程;
  3. 如果uf缓冲区中没有空余位置,封装waitq 结构,将待发送数据写入G,将当前G加入等待写消息队列sendq,进入睡眠,等待被读的协程唤醒唤醒
    在这里插入图片描述

通过channel读数据

  1. 在通过channel读取数据时,实际底层有两个函数chanrecv1与chanrecv2,但是这两个函数内部最终都会调用chanrecv()去读取数据
  1. chanrecv1(): 不会返回ok
  2. chanrecv2(): 会返回ok,通过“received” 这个字段来返回 channel 是否被关闭
func chanrecv1(c *hchan, elem unsafe.Pointer) {
    chanrecv(c, elem, true)
}
func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {
    _, received = chanrecv(c, elem, true)
    return
}
  1. 查看读取数据时执行的chanrecv()源码:
// 位于 src/runtime/chan.go
// chanrecv 函数接收 channel c 的元素并将其写入 ep 所指向的内存地址。
// 如果 ep 是 nil,说明忽略了接收值。
// 如果 block == false,即非阻塞型接收,在没有数据可接收的情况下,返回 (false, false)
// 否则,如果 c 处于关闭状态,将 ep 指向的地址清零,返回 (true, false)
// 否则,用返回值填充 ep 指向的内存地址。返回 (true, true)
// 如果 ep 非空,则应该指向堆或者函数调用者的栈
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    // 省略 debug 内容 …………
    // 如果是一个 nil 的 channel
    if c == nil {
        // 如果不阻塞,直接返回 (false, false)
        if !block {
            return
        }
        // 否则,接收一个 nil 的 channel,goroutine 挂起
        gopark(nil, nil, "chan receive (nil chan)", traceEvGoStop, 2)
        // 不会执行到这里
        throw("unreachable")
    }
    // 在非阻塞模式下,快速检测到失败,不用获取锁,快速返回
    // 当我们观察到 channel 没准备好接收:
    // 1. 非缓冲型,等待发送列队 sendq 里没有 goroutine 在等待
    // 2. 缓冲型,但 buf 里没有元素
    // 之后,又观察到 closed == 0,即 channel 未关闭。
    // 因为 channel 不可能被重复打开,所以前一个观测的时候 channel 也是未关闭的,
    // 因此在这种情况下可以直接宣布接收失败,返回 (false, false)
    if !block && (c.dataqsiz == 0 && c.sendq.first == nil ||
        c.dataqsiz > 0 && atomic.Loaduint(&c.qcount) == 0) &&
        atomic.Load(&c.closed) == 0 {
        return
    }
    var t0 int64
    if blockprofilerate > 0 {
        t0 = cputicks()
    }
    // 加锁
    lock(&c.lock)
    // channel 已关闭,并且循环数组 buf 里没有元素
    // 这里可以处理非缓冲型关闭 和 缓冲型关闭但 buf 无元素的情况
    // 也就是说即使是关闭状态,但在缓冲型的 channel,
    // buf 里有元素的情况下还能接收到元素
    if c.closed != 0 && c.qcount == 0 {
        if raceenabled {
            raceacquire(unsafe.Pointer(c))
        }
        // 解锁
        unlock(&c.lock)
        if ep != nil {
            // 从一个已关闭的 channel 执行接收操作,且未忽略返回值
            // 那么接收的值将是一个该类型的零值
            // typedmemclr 根据类型清理相应地址的内存
            typedmemclr(c.elemtype, ep)
        }
        // 从一个已关闭的 channel 接收,selected 会返回true
        return true, false
    }
    // 等待发送队列里有 goroutine 存在,说明 buf 是满的
    // 这有可能是:
    // 1. 非缓冲型的 channel
    // 2. 缓冲型的 channel,但 buf 满了
    // 针对 1,直接进行内存拷贝(从 sender goroutine -> receiver goroutine)
    // 针对 2,接收到循环数组头部的元素,并将发送者的元素放到循环数组尾部
    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
    }
    // 缓冲型,buf 里有元素,可以正常接收
    if c.qcount > 0 {
        // 直接从循环数组里找到要接收的元素
        qp := chanbuf(c, c.recvx)
        // …………
        // 代码里,没有忽略要接收的值,不是 "<- ch",而是 "val <- ch",ep 指向 val
        if ep != nil {
            typedmemmove(c.elemtype, ep, qp)
        }
        // 清理掉循环数组里相应位置的值
        typedmemclr(c.elemtype, qp)
        // 接收游标向前移动
        c.recvx++
        // 接收游标归零
        if c.recvx == c.dataqsiz {
            c.recvx = 0
        }
        // buf 数组里的元素个数减 1
        c.qcount--
        // 解锁
        unlock(&c.lock)
        return true, true
    }
    if !block {
        // 非阻塞接收,解锁。selected 返回 false,因为没有接收到值
        unlock(&c.lock)
        return false, false
    }
    // 接下来就是要被阻塞的情况了
    // 构造一个 sudog
    gp := getg()
    mysg := acquireSudog()
    mysg.releasetime = 0
    if t0 != 0 {
        mysg.releasetime = -1
    }
    // 待接收数据的地址保存下来
    mysg.elem = ep
    mysg.waitlink = nil
    gp.waiting = mysg
    mysg.g = gp
    mysg.selectdone = nil
    mysg.c = c
    gp.param = nil
    // 进入channel 的等待接收队列
    c.recvq.enqueue(mysg)
    // 将当前 goroutine 挂起
    goparkunlock(&c.lock, "chan receive", traceEvGoBlockRecv, 3)
    // 被唤醒了,接着从这里继续执行一些扫尾工作
    if mysg != gp.waiting {
        throw("G waiting list is corrupted")
    }
    gp.waiting = nil
    if mysg.releasetime > 0 {
        blockevent(mysg.releasetime-t0, 2)
    }
    closed := gp.param == nil
    gp.param = nil
    mysg.c = nil
    releaseSudog(mysg)
    return true, !closed
}
  1. 当channel以关闭时: 非缓冲型关闭和缓冲型关闭但 buf 无元素的情况,返回对应类型的零值,但 received 标识是 false,告诉调用者此 channel 已关闭,取出的值并不是正常由发送者发送过来的数据。但是如果处于 select 语境下,这种情况是被选中了的
  2. channel读数据简单过程如下:
  1. 首先判断等待读取消息队列sendq不为空,且没有缓冲区,说明前面有写请求在阻塞等待,直接从sendq中取出G,把G中的数据读出,执行设goready 函数,把G唤醒,结束读取过程
  2. 如果等待写消息队列sendq不为空, 并且有缓冲区时,说明缓冲区已满,优先从缓冲数据区域首部读取数据,并且在读取完后,判断 sendq 队列,如果 goroutine 有等待队列,获取阻塞协程,把G中数据写入缓冲区尾部,执行 goready 函数,唤醒这个阻塞的写请求
  3. 如果sendq 队列中没有阻塞等待写的请求,并且缓冲区中有数据,则从缓冲区取出数据,结束读取过程;
  4. 如果没有等待下的请求,并且缓冲区中没有数据,封装waitq 结构,goroutine加入recvq,进入睡眠,等待被写goroutine唤醒
    在这里插入图片描述
  1. 通过为nil的channel读取数据时
  1. 在非阻塞模式下会直接返回一个零值
  2. 在阻塞模式下,将永久阻塞当前的 goroutine,而不是调用 gopark 函数挂起该 goroutine。而调用 gopark 函数通常是指当前 goroutine 进入睡眠状态,等待被唤醒,以避免浪费 CPU 资源
  3. 要想不阻塞,只有关闭它,但关闭一个 nil 的 channel 又会发生 panic。更详细地可以在 closechan 函数的时候再看

三. 总结

  1. 参考博客: Go 语言问题集(Go Questions)
  2. 什么是channel: channel是Go语言中用于实现协程之间通信和同步的一种机制,它允许一个goroutine向另一个goroutine发送数据,并等待接收响应。使用channel可以有效地避免goroutine之间的竞争条件和死锁问题,从而提高程序的稳定性和并发性能

总结channel的底层结构与读写过程

  1. 什么是CSP模型
  1. CSP模型是并发模型中的一种,传统的并发模型主要分为 Actor 模型和 CSP 模型,
  2. 我理解的Actor 模型 更关注的是数据, 而CSP 模型关注的是消息发送的载体,在Actor 模型中通过主要通过锁同步原语等解决并发安全问题, 在CSP中以go举例,通过channel传递消息的通道解决并发安全问题, 这也是golang的一句名言的体现:使用通信来共享内存,而不是通过共享内存来通信, channel是如何保证并发安全的,这块要看源码

1. 底层结构

  1. 在使用channel时首先要通过内置的make()函数进行初始化,实际会执行底层的makechan64 和 makechan,以makeChan为例,查看该函数
  1. 内部首先会创建一个hchan结构图变量
  2. 然后判断元素类型如果是指针或者 size 为0无缓时调用mallocgc()计算"hchan 结构体大小 + 元素大小*个数"只进行一次内存分配,否则会再次执行new函数,两次分配内存,第一次分配hchan结构体的内存,第二次分配是在向channel中发送或接收元素时进行的,分配的是元素本身的内存。如果元素大小为0,则不会进行第二次内存分配
  3. 调用new()函数初始化hchan,调用newarray()初始化hchan内部的buf数组, 这个hchan就是channel的底层数据类型
    ,最终置hchan完成并返回
  1. 实际hchan就是channel的底层结构,是一个环形队列,内部存在一些几个比较重要的属性
  1. recvq: 等待接收队列,
  2. sendq: 等待发送队列,
  3. elemtype: 元素类型
  4. buf: 环形队列指针
  5. lock: 互斥锁,chan不允许并发读写
  1. 其中sendx和recvx比较重要,是一个waitq 双向链表结构

2. 写数据

  1. 在向channel写数据时会调用runtime/chan.go下的chansend()函数,
  1. 该函数中首先会进行一些校验,例如当前channel是否为nil,是否存满等
  2. 如果校验通过,调用lock()加锁,支持并发安全
  3. 后续根据recvq等待接收队列中是否有阻塞的协程执行写操作
  1. 如果能从等待接收队列 recvq 里出队一个 sudog(代表一个 goroutine),说明此时 channel 是空的,没有元素,所以才会有等待接收者。这时会调用 send 函数将元素直接从发送者的栈拷贝到接收者的栈,关键操作由 sendDirect 函数完成
  2. channel写数据简单过程如下
  1. 首先会判断等待读取消息队列recvq,如不不为空,说明缓冲区中没有数据或者没有缓冲区(也就是说前面有读取阻塞的协程),此时直接从recvq取出这个阻塞的协程G,写入数据,并执行该协程的goready函数,设置等待下次调度运行,也就是唤醒阻塞的读协程执行,结束发送过程
  2. 等待读取消息队列recvq为空,此时判断buf缓冲区中是否有空余位置,如果有将数据写入缓冲区,结束发送过程;
  3. 如果uf缓冲区中没有空余位置,封装waitq 结构,将待发送数据写入G,将当前G加入等待写消息队列sendq,进入睡眠,等待被读的协程唤醒唤醒

3. 读操作

  1. 在通过channel读取数据时,实际底层有两个函数chanrecv1与chanrecv2,但是这两个函数内部最终都会调用chanrecv()去读取数据(一个不会返回ok一个会),查看chanrecv()
  2. 当channel为nil时: 在非阻塞模式下,会直接返回。在阻塞模式下,会调用 gopark 函数挂起 goroutine,这个会一直阻塞下去。因为在 channel 是 nil 的情况下,要想不阻塞,只有关闭它,但关闭一个 nil 的 channel 又会发生 panic,所以没有机会被唤醒了。更详细地可以在 closechan 函数的时候再看
  3. 当channel以关闭时: 非缓冲型关闭和缓冲型关闭但 buf 无元素的情况,返回对应类型的零值,但 received 标识是 false,告诉调用者此 channel 已关闭,取出的值并不是正常由发送者发送过来的数据。但是如果处于 select 语境下,这种情况是被选中了的
  4. channel读数据简单过程如下:
  1. 首先判断等待读取消息队列sendq不为空,且没有缓冲区,说明前面有写请求在阻塞等待,直接从sendq中取出G,把G中的数据读出,执行设goready 函数,把G唤醒,结束读取过程
  2. 如果等待写消息队列sendq不为空, 并且有缓冲区时,说明缓冲区已满,优先从缓冲数据区域首部读取数据,并且在读取完后,判断 sendq 队列,如果 goroutine 有等待队列,获取阻塞协程,把G中数据写入缓冲区尾部,执行 goready 函数,唤醒这个阻塞的写请求
  3. 如果sendq 队列中没有阻塞等待写的请求,并且缓冲区中有数据,则从缓冲区取出数据,结束读取过程;
  4. 如果没有等待下的请求,并且缓冲区中没有数据,封装waitq 结构,goroutine加入recvq,进入睡眠,等待被写goroutine唤醒

问题

单向channel

  1. 实际上也没有单向channel,所谓单向channel只是对channel的一种使用限制

select 与 channel

  1. select语句是Go语言中用于处理channel的一种机制,它允许在多个channel上等待数据,并在其中任意一个channel有数据可读时立即执行相应的操作。使用select语句可以实现非常灵活的并发编程,可以避免出现死锁和竞争条件等问题
  2. select语句由多个case分支组成,每个case表示一个channel的操作(包括发送和接收操作),当其中任意一个channel有数据可读时,就会执行对应的case分支,并终止select语句的执行。如果多个channel同时有数据可读,那么会随机选择一个channel并执行对应的case分支,select语句还支持default分支
  3. select读取channel中的数据有什么特点
  1. 阻塞式的等待: select 语句会阻塞当前 goroutine,直到至少有一个 channel 可用。如果没有任何 channel 可操作,则 select 语句会一直等待,直到有一个或多个 channel 可用。
  2. 非阻塞式的读取: 使用 select 语句读取 channel 的数据时,通过在 case 中添加 default 操作,在没有可操作的 channel 时立即执行 default 中的操作
    3.多路复用:使用select语句可以同时监听多个channel,并在其中任意一个channel中有数据可读时立即执行对应的case分支,从而实现多路复用的效果
  3. 随机选择: 当多个 channel 都可以操作时,select 语句会按照随机的顺序选择其中一个 case 进行处理。这个特点让 select 语句具有更好的灵活性
  4. 死锁问题:如果所有的case分支都没有可执行的操作,那么select语句会一直阻塞,从而导致死锁的问题。因此,在使用select语句时,需要保证至少有一个case分支是能够执行的,或者在必要情况下使用default分支来避免死锁问题

range 与 channel

  1. 通过range可以像遍历数组一样从channel中读出数据,与读channel时阻塞处理机制一样,当channel中没有数据时会阻塞当前goroutine,直到通道被关闭range自动退出循环,如果关闭后仍有数据可读,range会继续读取这些数据,直到读取完毕然后退出
  2. 注意:
  1. 如果在 range 中使用了 break 或者 return 等跳出循环的语句,即使 channel 中还有数据,循环也会被立刻终止
  2. 如果在 range 循环中某个时刻有其他的 goroutine 关闭了相同的 channel,当前range 语句会立即退出循环

channel 可能会引发 goroutine 泄漏

  1. goroutine 操作 channel 时由于channel 处于满或空的状态,多个goroutine阻塞一直得不到改变。垃圾回收器也不会回收此类资源,进而导致 gouroutine 会一直处于等待队列中,造成泄漏, 解决方案:
  1. 使用带缓冲的channel,设置合理缓冲数,避免goroutine向channel中写或读数据时由于channel处于满或空的状态而阻塞
  2. 使用超时机制,例如select+超时机制来避免由于channel处于满或空的状态而导致的阻塞问题
  3. 使用context包控制goroutine的生命周期也是一种超时机制,例如通过context包中的WithTimeout和WithCancel等函数来设置goroutine的超时时间或者取消信号,及时终止goroutine的执行
  4. 使用sync.WaitGroup:sync.WaitGroup可以用来等待一组goroutine执行完毕,例如在向channel中写入或读取数据时,通过sync.WaitGroup来等待所有相关的goroutine执行完毕后再继续程序的执行

channel 有哪些应用

  1. 停止信号:经常是关闭某个 channel 或者向 channel 发送一个元素,使得接收 channel 的那一方获知道此信息,进而做一些其他的操作
  2. 任务定时: select +time.After()
  3. 解耦生产方和消费方:
  4. 控制并发数: 通过带缓冲类型channel实现,指定channel缓冲大小,

从一个关闭的 channel 仍然能读出数据吗:

  1. 可以通过取值符, select, range读取一个channel
  1. 使用取值符读取时: 依然能读出有效值,并且取值符读取时会返回读到的值和代表是否有效的flag,两个变量,当flag为false时表示无效
  2. 使用select语句读取已关闭的channel时,case分支会立即执行,如果channel中有数据会返回对应的数据,如果没有会返回该channel类型的零值或者nil指针
  3. 使用range读取关闭的channel,如果有数据数据会被正常读出,然后range循环退出,如果没有数据,range直接退出
  1. 通过为nil的channel读取数据时
  1. 在非阻塞模式下会直接返回一个零值
  2. 在阻塞模式下,将永久阻塞当前的 goroutine,而不是调用 gopark 函数挂起该 goroutine。而调用 gopark 函数通常是指当前 goroutine 进入睡眠状态,等待被唤醒,以避免浪费 CPU 资源
  3. 要想不阻塞,只有关闭它,但关闭一个 nil 的 channel 又会发生 panic。更详细地可以在 closechan 函数的时候再看
  1. 当channel以关闭时: 非缓冲型关闭和缓冲型关闭但 buf 无元素的情况,返回对应类型的零值,但 received 标识是 false,告诉调用者此 channel 已关闭,取出的值并不是正常由发送者发送过来的数据。但是如果处于 select 语境下,这种情况是被选中了的

关于 channel 的 happened-before 有哪些

  1. 关于 channel 的发送send,发送完成send finished,接收receive,接收完成receive finished的 happened-before 关系如下
  1. 第 n 个 send 一定 happened before 第 n 个 receive finished,无论是缓冲型还是非缓冲型的 channel
  2. 对于容量为 m 的缓冲型 channel,第 n 个 receive 一定 happened before 第 n+m 个 send finished
  3. 对于非缓冲型的 channel,第 n 个 receive 一定 happened before 第 n 个 send finished
  4. channel close 一定 happened before receiver 得到通知
  1. 逐条解释:

第一条,send 不一定是 happened before receive,因为有时候是先 receive,然后 goroutine 被挂起,之后被 sender 唤醒,send happened after receive。要想完成接收,一定是要先有发送
第二条,缓冲型的 channel,当第 n+m 个 send 发生后,有下面两种情况:若第 n 个 receive 没发生。这时,channel 被填满了,send 就会被阻塞。那当第 n 个 receive 发生时,sender goroutine 会被唤醒,之后再继续发送过程。这样,第 n 个 receive 一定 happened before 第 n+m 个 send finished。若第 n 个 receive 已经发生过了,这直接就符合了要求。
第三条,第 n 个 send 如果被阻塞,sender goroutine 挂起,第 n 个 receive 这时到来,先于第 n 个 send finished。如果第 n 个 send 未被阻塞,说明第 n 个 receive 早就在那等着了,它不仅 happened before send finished,它还 happened before send
第四条,回忆一下源码,先设置完 closed = 1,再唤醒等待的 receiver,并将零值拷贝给 receiver。

关闭一个 channel 的过程是怎样的

  1. 关闭channel时会调用底层的closechan()函数,channel在底层的recvq 和 sendq 中分别保存了阻塞的发送者和接收者,关闭channel时,对于等待接收者,会收到一个相应类型的零值。对于等待发送者,会直接 panic。所以在不知道 channel 还有没有接收者的情况下不能贸然关闭 channel, close()函数详细逻辑:
  1. 获取channel对应的锁,加锁,以保证在关闭channel时不会有其他的goroutine同时访问
  2. 遍历这个channel 上所有的sudog,包括所有的sender和receiver,将它们添加到一个sudog链表中,并设置状态为waiting
  3. 释放channel对应的锁,以便其他goroutine可以访问该channel
  4. 遍历sudog链表,依次将其中的sudog状态设置为ready并将其加入到调度器的运行队列中,唤醒
  5. 在sudog被唤醒后,如果是一个sender,会尝试向channel发送数据;如果它是一个receiver,它会尝试从channel接收数
  6. 由于channel已经关闭,如果sudog是一个sender并且channel中还有数据可读,那么该sudog会将数据发送给channel并立即退出。如果没有,检测到 channel 已经关闭了则panic
  7. 如果sudog是一个receiver并且channel中还有数据可读,那么该sudog会将数据从channel中读取并立即退出。
  8. 注意这里的selected 返回 true,而返回值 received 则要根据 channel 是否关闭,返回不同的值。如果 channel 关闭,received 为 false,否则为 true。这我们分析的这种情况下,received 返回 false
func closechan(c *hchan) {
    // 关闭一个 nil channel,panic
    if c == nil {
        panic(plainError("close of nil channel"))
    }
    // 上锁
    lock(&c.lock)
    // 如果 channel 已经关闭
    if c.closed != 0 {
        unlock(&c.lock)
        // panic
        panic(plainError("close of closed channel"))
    }
    // …………
    // 修改关闭状态
    c.closed = 1
    var glist *g
    // 将 channel 所有等待接收队列的里 sudog 释放
    for {
        // 从接收队列里出队一个 sudog
        sg := c.recvq.dequeue()
        // 出队完毕,跳出循环
        if sg == nil {
            break
        }
        // 如果 elem 不为空,说明此 receiver 未忽略接收数据
        // 给它赋一个相应类型的零值
        if sg.elem != nil {
            typedmemclr(c.elemtype, sg.elem)
            sg.elem = nil
        }
        if sg.releasetime != 0 {
            sg.releasetime = cputicks()
        }
        // 取出 goroutine
        gp := sg.g
        gp.param = nil
        if raceenabled {
            raceacquireg(gp, unsafe.Pointer(c))
        }
        // 相连,形成链表
        gp.schedlink.set(glist)
        glist = gp
    }
    // 将 channel 等待发送队列里的 sudog 释放
    // 如果存在,这些 goroutine 将会 panic
    for {
        // 从发送队列里出队一个 sudog
        sg := c.sendq.dequeue()
        if sg == nil {
            break
        }
        // 发送者会 panic
        sg.elem = nil
        if sg.releasetime != 0 {
            sg.releasetime = cputicks()
        }
        gp := sg.g
        gp.param = nil
        if raceenabled {
            raceacquireg(gp, unsafe.Pointer(c))
        }
        // 形成链表
        gp.schedlink.set(glist)
        glist = gp
    }
    // 解锁
    unlock(&c.lock)
    // Ready all Gs now that we've dropped the channel lock.
    // 遍历链表
    for glist != nil {
        // 取最后一个
        gp := glist
        // 向前走一步,下一个唤醒的 g
        glist = glist.schedlink.ptr()
        gp.schedlink = 0
        // 唤醒相应 goroutine
        goready(gp, 3)
    }
}

如何优雅地关闭 channel

  1. 为什么会有这个问题:
  1. 关闭一个 closed channel 会导致 panic
  2. 向一个 closed channel 发送数据会导致 panic
  3. 在不改变 channel 自身状态的情况下,无法获知一个 channel 是否关闭。
  1. 关闭channel的几个原则:
  1. 不要在 receiver 接收侧关闭 channel,sender 发送侧可以决定何时不发送数据,但是如果有多个 sender,某个 sender 同样没法确定其它 sender 的情况,所以多个 sender 发送侧时也不推荐在sender发送侧关闭,
  2. 使用 defer-recover 机制,即使发生了 panic,有 defer-recover 在兜底。
  3. 使用 sync.Once 来保证只关闭一次
  1. 最优一: 增加一个传递关闭信号的 channel,receiver 通过信号 channel 下达关闭数据 channel 指令。senders 监听到关闭信号后,停止接收数据
  1. stopCh 是信号 channel,它本身只有一个 sender,因此可以直接关闭它。senders 收到了关闭信号后,select 分支 “case <- stopCh” 被选中,退出函数,不再发送数据
  2. 下方代码并没有明确关闭 dataCh。在 Go 语言中,对于一个 channel,如果最终没有任何 goroutine 引用它,不管 channel 有没有被关闭,最终都会被 gc 回收。所以,在这种情形下,所谓的优雅地关闭 channel 就是不关闭 channel
func main() {
    rand.Seed(time.Now().UnixNano())
    const Max = 100000
    const NumSenders = 1000
    dataCh := make(chan int, 100)
    stopCh := make(chan struct{})
    // senders
    for i := 0; i < NumSenders; i++ {
        go func() {
            for {
                select {
                case <- stopCh:
                    return
                case dataCh <- rand.Intn(Max):
                }
            }
        }()
    }
    // the receiver
    go func() {
        for value := range dataCh {
            if value == Max-1 {
                fmt.Println("send stop signal to senders.")
                close(stopCh)
                return
            }
            fmt.Println(value)
        }
    }()
    select {
    case <- time.After(time.Hour):
    }
}
  1. 最优2: 和上方不同,有 M 个 receiver,如果还是采取第 3 种解决方案,由 receiver 直接关闭 stopCh 的话,就会重复关闭一个 channel,导致 panic。因此需要增加一个中间人,M 个 receiver 都向它发送关闭 dataCh 的“请求”,中间人收到第一个请求后,就会直接下达关闭 dataCh 的指令(通过关闭 stopCh,这时就不会发生重复关闭的情况,因为 stopCh 的发送方只有中间人一个)。另外,这里的 N 个 sender 也可以向中间人发送关闭 dataCh 的请求

代码里 toStop 就是中间人的角色,使用它来接收 senders 和 receivers 发送过来的关闭 dataCh 请求。

func main() {
    rand.Seed(time.Now().UnixNano())
    const Max = 100000
    const NumReceivers = 10
    const NumSenders = 1000
    dataCh := make(chan int, 100)
    stopCh := make(chan struct{})
    // It must be a buffered channel.
    toStop := make(chan string, 1)
    var stoppedBy string
    // moderator
    go func() {
        stoppedBy = <-toStop
        close(stopCh)
    }()
    // senders
    for i := 0; i < NumSenders; i++ {
        go func(id string) {
            for {
                value := rand.Intn(Max)
                if value == 0 {
                    select {
                    case toStop <- "sender#" + id:
                    default:
                    }
                    return
                }
                select {
                case <- stopCh:
                    return
                case dataCh <- value:
                }
            }
        }(strconv.Itoa(i))
    }
    // receivers
    for i := 0; i < NumReceivers; i++ {
        go func(id string) {
            for {
                select {
                case <- stopCh:
                    return
                case value := <-dataCh:
                    if value == Max-1 {
                        select {
                        case toStop <- "receiver#" + id:
                        default:
                        }
                        return
                    }
                    fmt.Println(value)
                }
            }
        }(strconv.Itoa(i))
    }
    select {
    case <- time.After(time.Hour):
    }
}
  1. 假设 toStop 声明的是一个非缓冲型的 channel,那么第一个发送的关闭 dataCh 请求可能会丢失。因为无论是 sender 还是 receiver 都是通过 select 语句来发送请求,如果中间人所在的 goroutine 没有准备好,那 select 语句就不会选中,直接走 default 选项,什么也不做。这样,第一个关闭 dataCh 的请求就会丢失
  2. 如果,我们把 toStop 的容量声明成 Num(senders) + Num(receivers),那发送 dataCh 请求的部分可以改成更简洁的形式

直接向 toStop 发送请求,因为 toStop 容量足够大,所以不用担心阻塞,自然也就不用 select 语句再加一个 default case 来避免阻塞

...
toStop := make(chan string, NumReceivers + NumSenders)
...
            value := rand.Intn(Max)
            if value == 0 {
                toStop <- "sender#" + id
                return
            }
...
                if value == Max-1 {
                    toStop <- "receiver#" + id
                    return
                }
...

channel怎么保证并发安全

  1. 在使用Channel是需要通过make()函数进行初始化,反编译后底层实际执行的是makeChan(),查看该函数源码,内部首先会创建一个hchan结构
  2. hchan就是channel的底层结构,是一个环形队列,内部存在一些几个比较重要的属性: … lock: 互斥锁,并发安全就是通过这个lock互斥锁实现的
  3. 在向channel写数据时,底层会调用到一个chansend()函数,该函数中首先会判断当前操作的channel是否为nil,如果不为nil,会先执行"lock(&c.lock)"获取锁
  4. 在读取channel数据时,底层会调用chanrecv()函数,该函数中,也是先进行一些校验,例如是否为nil等等,校验通过后也是先调用"lock(&c.lock)"获取锁,

channel 带缓冲不带缓冲的区别

  1. 不带缓冲的channel是指在创建channel时没有设置缓冲区大小,即make(chan int)。在向不带缓冲的channel中写入数据时,如果没有对应的goroutine在读取该数据,写入操作会阻塞,直到有对应的goroutine开始读取该channel中的数据。同理在不带缓冲的channel中读取数据时,如果没有对应的goroutine在向该channel中写入数据,读取操作也会阻塞,直到有对应的goroutine开始向该channel中写入数据。
  2. 带缓冲的channel是指在创建channel时设置了缓冲区大小,即make(chan int, 10)。在向带缓冲的channel中写入数据时,如果缓冲区未满,那么写入操作会立即完成,不会导致当前goroutine阻塞。只有当缓冲区已满时,写入操作才会阻塞,直到有对应的goroutine开始读取该channel中的数据。同理在带缓冲的channel中读取数据时,如果缓冲区非空,那么读取操作会立即完成,不会导致当前goroutine阻塞。只有当缓冲区为空时,读取操作才会阻塞,直到有对应的goroutine开始向该channel中写入数据
  3. 在并发数据读写时,带缓冲的channel通常可以提高程序的并发性能,因为它可以在一定程度上减少goroutine之间的阻塞和切换。但是需要注意,如果缓冲区的大小设置过大,可能会占用过多的系统资源,降低程序的性能。因此,在使用带缓冲的channel时,需要根据实际需求设置合适的缓冲区大小

go信号捕捉应用案例

  1. 用go做过信号捕捉吗?(优雅下线使用,SIGTERM和Ctrl+C): go语言中提供了"os/signal"信号包,可以实现信号监听例如监听SIGINT或SIGTERM优雅地关闭服务器
package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
)

func main() {
    // 创建一个通道用于接收信号
    sigs := make(chan os.Signal, 1)

    // 注册要捕捉的信号
    signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)

    // 阻塞等待信号
    sig := <-sigs

    // 输出捕捉到的信号
    fmt.Println("Caught signal:", sig)

    // 处理信号
    // ...

    // 退出程序
    os.Exit(0)
}

其它问题

  1. 一个channel中可以存放多种数据类型吗,不可以,可以考虑使用接口定义channel类型
  2. 一个channel存满或取空时,继续存储,或取出数据会阻塞,在没有使用协程的情况下数据取完了,继续取回报dead lock异常
  3. 可以使用基础for循环遍历管道吗? 可以,但是需要注意:
  1. 首先管道的数据长度是随着取而改变的,
  2. cap是make容量并不是实际存储数据的长度,
  3. 可以使用基础for循环,或死循环在循环内部通过 <-管道 读取数据,该方式不会造成阻塞死锁)
  1. 管道支持使用for-range进行遍历,但是在遍历管道时需要先关闭管道,否则会阻塞,因阻塞而导致死锁报错deadlock或一直不停循环读数据直到关闭
  2. channel可以通过内置函数close进行关闭
  3. 关闭一个未初始化(nil) 的 channel 会产生 panic
  4. 重复关闭同一个 channel 会产生 panic
  5. 当管道关闭后,则该管道只可以读数据,不能写数据 “close(管道变量)”,当向关闭的管道中添加数据是返回false
  6. 向一个已关闭的 channel 中发送消息会产生 panic
  7. 从已关闭的 channel 读取消息不会产生 panic,且能读出 channel 中还未被读取的消息,若消息均已读出,则会读到类型的零值。从一个已关闭的 channel 中读取消息永远不会阻塞,并且会返回一个为 false 的 ok-idiom,可以用它来判断 channel 是否关闭
  8. 关闭 channel 会产生一个广播机制,所有向 channel 读取消息的 goroutine 都会收到消息
  9. 从无缓存的 channel 中读取消息会阻塞,直到有 goroutine 向该 channel 中发送消息;同理,向无缓存的 channel 中发送消息也会阻塞,直到有 goroutine 从 channel 中读取消息。
  10. Go channel是先进先出还是先进后出?: channel的读取顺序与goroutine的写入顺序一致,因此channel是先进先出(FIFO)的
  11. 讲一下你对channel的理解。没有goroutine在读channel时去写会反发生什么。读的时候呢。channel读的时候是有序还是无序的?
  1. 当多个goroutine向同一个channel中写入数据时,channel会按照写入顺序保存这些数据,可以看为l是先进先出
  2. 但是同一个channel可能被一个或多个goroutine读取,channel的读取顺序取决于goroutine的调度顺序和channel中数据的写入顺序,因此channel的读取是无序的
  1. channel的底层结构?接收、发送消息的过程?
  2. go channel详细底层结构 有无缓冲并发数据读写问题 底层锁
  3. channel的实现看过吗 底层是切片
  4. channel要加锁吗 两个协程同时写或者读 怎么保证数据安全
  5. 对已经关闭的channel进行读写操作会发生什么
  6. chan缓冲区大小
  7. goroutine+channel使用场景剖析,有无缓存通道问题(抠细节)
  8. golang的channel读写流程,详细说明
  9. 关闭的channel(有和无缓冲)可以读到数据吗?确定吗?分别使用什么场景?多个协程访问会怎么样?
  10. go读写关闭的channel
  11. golang channel 你会用在什么地方 (一个是控制 goroutine数量 一个是主main 控制关闭 子 goroutine)
  12. channel的底层结构?接收、发送消息的过程?
  13. go channel详细底层结构 有无缓冲并发数据读写问题 底层锁
  14. 平时用 go channel的场景
  15. channel关闭后 再写入会怎样 -> 读写过程分别是怎样的
  16. 有缓冲的channel是否同步,
  17. chan是线程安全的吗、chan为什么安全: channel是线程安全的。Go语言有个很出名的话是“以通信的手段来共享内存”,channel就是其最佳的体现,channel提供一种机制,可以同步两个并发执行的函数,还可以让两个函数通过互相传递特定类型的值来通信。channel有两种初始化方式,分别是带缓存的和不带缓存的,当向一个已满的channel发送数据会被阻塞,此时发送协程会被添加到sendq中,同理,当向一个空的channel接收数据时,接收协程也会被阻塞

channel的一些应用案例

1. channel实现生产者消费者消息通知模型

  1. 在某些特殊场景下,或者业务代码比较复杂可以通过channel实现生产者消费者模型实现解耦,通过channel实现消息通知,当业务需要执行时只需要向channel中生产一个消息,其它消费方去接收这个消息消费即可,另外比如多人入住,所有入住完毕后通知消费执行其他业务场景等
package main

import (
	"fmt"
)

func producer(ch chan<- int) {
	for i := 0; i < 5; i++ {
		ch <- i // 往通道发送数据
	}
	close(ch) // 关闭通道
}

func consumer(ch <-chan int) {
	for num := range ch {
		fmt.Println("Received:", num) // 从通道接收数据并处理
	}
}

func main() {
	ch := make(chan int) // 创建一个通道

	go producer(ch) // 启动生产者协程
	consumer(ch)    // 主 goroutine 充当消费者

	fmt.Println("Done")
}
  1. 进阶: 批量生产消费消息
  1. 上方示例中创建的是无缓冲通道,每次只能生产消费一个消息,解决这个问题
  2. 创建一个带缓冲通道,批量处理的数量就是缓冲的大小
  3. 然后使用协程批量消费消息,注意为了等待所有消费完成统一返回,可能需要用到sync.WaitGroup设置等待
package main

import (
	"fmt"
	"sync"
)

func producer(ch chan<- int, wg *sync.WaitGroup) {
	defer wg.Done()

	for i := 1; i <= 5; i++ {
		ch <- i // 往通道发送数据
	}

	close(ch) // 关闭通道
}

func consumer(id int, ch <-chan int, wg *sync.WaitGroup) {
	defer wg.Done()

	for num := range ch {
		fmt.Printf("Consumer %d received %d\n", id, num)
	}
}

func main() {
	ch := make(chan int) // 创建一个无缓冲通道

	var wg sync.WaitGroup
	wg.Add(4)

	go producer(ch, &wg) // 启动生产者协程

	// 启动多个消费者协程
	for i := 1; i <= 3; i++ {
		go consumer(i, ch, &wg)
	}

	// 等待所有协程执行完成
	wg.Wait()
	fmt.Println("Done")
}

2. 实现锁控制并发等

  1. 基于channel的特性可以实现一个简单的锁,用来控制并发,并且go本身就推荐CSP模型:使用通信来共享内存,而不是通过共享内存来通信,在实现时:
  1. 创建一个缓冲区为1的channel变量作为锁
  2. 在执行业务时先向这个chnnel中存储一个数据,业务继续执行,当业务执行完毕后在channel中取出数据
  3. 此时如果有第二个业务进来,再次向channel中存储数据,如果上一个业务没有执行完毕,则会阻塞等待
  4. 注意此处只是一个简单的示例,并没有实际意义,因为在channel底层逻辑中也用到了lock锁保证并发安全,在实际开发中如果真需要锁的功能,可以直接用go中的lock
package main

import (
	"fmt"
)

func main() {
	lock := make(chan bool, 1) // 创建一个带有缓冲区大小为1的channel

	// goroutine A
	go func() {
		lock <- true // 获取锁
		fmt.Println("goroutine A: Lock acquired")

		// 执行临界区操作
		// ...

		<-lock // 释放锁
		fmt.Println("goroutine A: Lock released")
	}()

	// goroutine B
	go func() {
		lock <- true // 获取锁
		fmt.Println("goroutine B: Lock acquired")

		// 执行临界区操作
		// ...

		<-lock // 释放锁
		fmt.Println("goroutine B: Lock released")
	}()

	// 等待两个goroutine执行完毕
	select {}
}
  1. 进阶: 使用channel+time.Timer+select 实现等待锁
  1. 通过time.After(时间)获取一个time.Timer,实际是一个定时存储时间的channel变量
  2. 通过select 同时监听锁channel和time.Timer
  3. 在获取锁时通过select向锁channel中存储数据,如果锁已经被获取会阻塞等待,由于select同时监听time.Timer当等待指定时间后会走time.Timer的case,说明等待锁超时
package main

import (
	"errors"
	"fmt"
	"sync"
	"time"
)

type Lock struct {
	ch chan struct{}
}

func NewLock() *Lock {
	return &Lock{make(chan struct{}, 1)}
}

func (l *Lock) Lock(timeout time.Duration) error {
	select {
	case l.ch <- struct{}{}:
		return nil
	case <-time.After(timeout):
		return errors.New("timeout")
	}
}

func (l *Lock) Unlock() {
	<-l.ch
}

func main() {
	var wg sync.WaitGroup
	l := NewLock()

	wg.Add(1)
	go func() {
		defer wg.Done()
		fmt.Println("Acquiring lock...")

		if err := l.Lock(time.Second); err != nil {
			fmt.Println("Failed to acquire lock:", err)
			return
		}

		fmt.Println("Lock acquired")
		defer l.Unlock()

		// 模拟需要独占资源的耗时操作
		time.Sleep(3 * time.Second)
		fmt.Println("Done")
	}()

	wg.Wait()
}
  1. 进阶: golang标准库中多种Context,基于这个Context还可以实现在等待锁时可手动取消的功能
package main

import (
    "context"
    "fmt"
    "sync"
    "time"
)

type Mutex struct {
    ch     chan struct{}
    cancel context.CancelFunc // 用于手动取消操作
}

func NewMutex() *Mutex {
    _, cancel := context.WithCancel(context.Background()) //下划线省略的是context变量
    return &Mutex{ch: make(chan struct{}, 1), cancel: cancel}
}

func (m *Mutex) Lock(ctx context.Context, timeout time.Duration) error {
    select {
    case m.ch <- struct{}{}:
        return nil
    case <-ctx.Done():
        return fmt.Errorf("lock acquisition cancelled")
    case <-time.After(timeout):
        return fmt.Errorf("lock acquisition timed out after %v", timeout)
    }
}

func (m *Mutex) Unlock() {
    <-m.ch
}

func (m *Mutex) Cancel() {
    m.cancel() // 手动取消操作
}

func main() {
    mutex := NewMutex()

    // 创建一个带有超时时间的 Context 对象
    ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
    defer cancel()

    // 在锁超时时间内获取锁
    if err := mutex.Lock(ctx, time.Second); err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println("Lock acquired")

    // 模拟执行一些需要锁保护的代码
    time.Sleep(time.Second)

    // 释放锁
    mutex.Unlock()
    fmt.Println("Lock released")
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值