设为首页 加入收藏

TOP

golang channel 源码剖析(一)
2019-05-23 14:35:57 】 浏览:326
Tags:golang channel 源码 剖析

channelgolang 中是一个非常重要的特性,它为我们提供了一个并发模型。对比锁,通过 chan 在多个 goroutine 之间完成数据交互,可以让代码更简洁、更容易实现、更不容易出错。golangchannel 设计模型遵循 CSP(Communicating Sequential Processes,序列通信处理) 的设计理念。

本文将从源码角度来分析 golang 的 channel 是怎样实现的。先看一下 **channel*8 给我们提供的一些特性。

1. channel 的使用

关于这一小节,熟悉 channel 使用的读者可以快速浏览一下这一部分,这里没有什么特别的东西。

1.1 使用通道传输数据

func main() {
    c := make(chan int, 8)
    go func() {
        c <- 1
    }()
    fmt.Println(<-c)
}

上面的代码中,make(chan int, 8) 创建并返回一个缓冲区大小为 8 的通道,通道的元素类型为 int。如果把这个 8 去掉,像这样:c := make(chan int),那么创建的通道就是没有缓冲区的通道。如果你熟悉 go,你一定知道我们可以一直向缓冲区发送数据,直到缓冲区变满为止才会阻塞。而如果我们向无缓冲区的通道发送数据,就有存在其它的接收者正在等待,发送才不会不阻塞。

在创建通道之后,接下来使用 go 语句启动一个 goroutine,这个 goroutine 中,将 1 写入通道 c。最后使用 <-c 读取通道数据并且打印。

这很简单,但是我们需要思考几个问题:

  • 创建通道的时候发生了什么事情?我们创建了一个什么样的数据结构?
  • 向通道发送数据的时候发生了什么事情?缓冲区满了就会阻塞是怎么实现的?
  • 从通道中接收数据时发生了什么事情?
  • 带缓冲区的通道和不带缓冲区的通道有什么不同吗?

1.2 select

然后让我们看一个稍微复杂一点的: selectselect 会从所有的 case 中挑选出一个不会阻塞的通道读操作、写操作或者是 default 操作执行。如果都会阻塞,那么 select 就会等待,对应的 goroutine 也会被挂起。

如下面的代码, c1c2 是两个通道, go 启动一个 goroutine,如果 c1 可读且 c2 不可写,那么就会执行第一个 case, 如果 c1 不可读但 c2 可写,那么就会执行第二个 case。如果 c1 可读而且 c2 可写,那么就会随机执行第一个 case 或者第二个 case。如果 c1 不可读而且 c2 不可写,那么就会执行 default。这里,如果我们没有实现 default 分支,那么 select 就会阻塞。

package main

import (
    "fmt"
    "math/rand"
)

func main() {

    c1 := make(chan int)
    c2 := make(chan int)

    go func() {
        for {
            select {
            case x := <-c1:
                fmt.Println("从 c1 接受数据;", x)
            case c2 <- 100:
                fmt.Println("向 c2 发送数据")
            default:
                fmt.Println("c1 和 c2 都没什么可操作的")
            }
        }
    }()

    for i := 0; i < 500; i++ {
        rd := rand.Intn(2)
        switch rd {
        case 0:
            c1 <- 200
        case 1:
            <-c2
        }
    }
}

只是稍微复杂了一点点,但是还是有很多东西我们需要去探索:

  • select 的工作原理是什么?它是怎么选出一个可执行的语句的?
  • select 为什么可以在多个通道上阻塞?
  • 为什么没有 default 分支时会阻塞,有 default 时会执行 default 的内容?
  • 有多个可执行的语句时,为什么会是随机选的,而不是按照我们代码的顺序?

带着上面的所有问题,我们来看一看 channel 的源码。

2. 预备知识

在深入 channel 源码之前,先了解一下需要有哪些预备知识

2.1 goroutine 的表示

runtime 库中,goroutine 用一个叫做 g 的结构表示,每个 g 对象表示一个 goroutine

type g struct {
  // ...
  atomicstatus   uint32  // 表示 goroutine 的状态
  param          unsafe.Pointer // 唤醒时参数
  waiting        *sudog // 等待队列,后文会说到
  // ...
}

通过 getg() 函数可以拿到当前 goroutineg 对象:

func getg() *g

2.2 sudog

g 对象中,有一个名字为 waiting 的 *sudog 指针,它表示这个 goroutine 正在等待什么东西或者正在等待哪些东西。

sudog 是一个链表形式的类型,waitlink 表示它的下一个节点。对于 cisSelectelem 字段,我们后文会说到。

type sudog struct {
        // ....
        isSelect bool
        elem     unsafe.Pointer // data element (may point to stack)      
        waitlink    *sudog // g.waiting list or semaRoot
        c           *hchan // channel
}

acquireSudog 申请一个 sudog 对象。 releaseSudog 释放 sudog 对象

func acquireSudog() *sudog {}
func releaseSudog(s *sudog) {}

2.3 gopark 和 goready

gopark 将当前的 goroutine 修改成等待状态,然后等待被唤醒。

func gopark(unlockf func(*g, unsafe.Pointer) bool, 
  lock unsafe.Pointer, 
  reason waitReason, 
  traceEv byte, 
  traceskip int)

goready 函数用来唤醒一个 goroutine,它将 goroutine 的状态修改为可运行状态,随后会被调度器运行。当被调度时,对应的 gopark 函数返回。

2.4 race***

在编译时,使用 -race 参数,可以执行竞态检查,在我们即将要分析的源码中,有相当部分代码为 race 提供了支持。分析时会跳过这一部分,有兴趣的读者可以参考: https://blog.golang.org/race-detector

3. 基本数据结构

chan 使用 hchan 表示,它的传参与赋值始终都是指针形式,每个 hchan 对象代表着一个 chan。

  • hchan 中包含一个缓冲区 buf,它表示已经发送但是还未被接收的数据缓存。buf 的大小由创建 chan 时的参数来决定。qcount 表示当前缓冲区中有效数据的总量,dataqsiz 表示缓冲区的大小,对于无缓冲区通道而言 dataqsiz 的值为 0。如果 qcount 和 dataqsiz 的值相同,则表示缓冲区用完了。
  • 缓冲区表示的是一个环形队列 (如果你不熟悉环形队列,可以看一下 https://www.geeksforgeeks.org/circular-queue-set-1-introduction-array-implementation/)。其中 sendx 表示下一
首页 上一页 1 2 3 4 5 6 7 下一页 尾页 1/7/7
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇Golang实现requests库 下一篇Golang websocket推送

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目