Go 的 Channel 很强大,理解其内在概念会让它更强大

栏目: IT技术 · 发布时间: 3年前

内容简介:推荐阅读

点击上方蓝色“ Go语言中文网 ”关注我们, 领全套 Go 资料 ,每天学习 Go 语言

Go 的 Channel 很强大,理解其内在概念会让它更强大

"Go 之旅 插图,由 Go Gopher 的 Renee French 创作

Go 中的通道(channel)机制十分强大,但是理解内在的概念甚至可以使它更强大。实际上,选择缓冲通道或无缓冲通道将改变应用程序的行为和性能。

无缓冲通道

无缓冲通道是在消息发送到通道时需要接收器的通道。声明一个无缓冲通道时,你不需要声明容量。例如:

package main

import (
    "sync"
    "time"
)

func main() {
    c := make(chan string)

    var wg sync.WaitGroup
    wg.Add(2)

    go func() {
        defer wg.Done()
        c <- `foo`
    }()

    go func() {
        defer wg.Done()

        time.Sleep(time.Second * 1)
        println(`Message: `+ <-c)
    }()

    wg.Wait()
}

由于没有准备就绪的接收者,第一个 goroutine 在发送消息 foo 时将被阻塞。这个 说明文档 [1] 很好地解释了这种行为:

如果容量为零或未设置,则通道将被无缓冲,只有在发送方和接收方都准备就绪时通信才能成功。

这一点, 《Effective Go》 [2] 中描述的也很清晰:

如果通道是无缓冲的,发送者将被阻塞,直到接收者接收到值。

通道的内部描绘可以给我们更多关于此行为的有趣的细节

无缓冲通道内部结构

channel 结构体 在 runtime 包的 chan.go 文件中可以找到。该结构包含与通道缓冲区相关的属性,但是为了说明无缓存的通道,我将省略我们稍后将看到的那些属性。下面是无缓冲通道的示意图:

Go 的 Channel 很强大,理解其内在概念会让它更强大

hchan 结构

通道维护了指向接收方( recvq )和发送方( sendq )列表的指针,由链表 waitq.sudog 表示 ,包含指向下一个元素的指针(next)和指向上一个元素的指针(previous),以及与处理 接收方/发送方 的 goroutine 相关的信息。有了这些信息,Go 程序就很容易知道,如果没有了发送方,通道就应该阻塞接收方,反之,没有了接收方,通道就应该阻塞发送方。

下面是我们前面示例的工作流:

  1. 通道是用一个空的接收方和发送方列表创建的。

  2. 第 16 行,我们的第一个 goroutine 将值 foo 发送到通道。
  3. 通道从(缓冲)池中获取一个结构体 sudog ,用以表示发送者。这个结构将维护对 goroutine 和值 foo 的引用。
  4. 这个发送者现在进入队列(enqueued ) sendq
  5. 由于“ chan send ”阻塞,goroutine 进入等待状态。

  6. 第 23 行,我们的第二个 goroutine 将读取来自通道的消息。

  7. 通道将弹出 sendq 队列,以获取步骤 3 中的等待发送的结构体。
  8. 通道将使用 memmove 函数将发送方发送的值(封装装在 sudog 结构中)复制到读取的通道的变量。
  9. 现在,我们的第一个 goroutine 可以恢复在第 5 步,并将释放在第 3 步获得的 sudog

正如我们在工作流中再次看到的,goroutine 必须切换到等待,直到接收器可用为止。但是,如果需要,这种阻塞行为可以通过缓冲通道避免。

缓冲通道内部结构

稍微改动之前的例子,以添加一个缓冲区:

package main

import (
    "sync"
    "time"
)

func main() {
    c := make(chan string, 2)

    var wg sync.WaitGroup
    wg.Add(2)

    go func() {
        defer wg.Done()

        c <- `foo`
        c <- `bar`
    }()

    go func() {
        defer wg.Done()

        time.Sleep(time.Second * 1)
        println(`Message: `+ <-c)
        println(`Message: `+ <-c)
    }()

    wg.Wait()
}

现在让我们根据这个例子分析结构 hchan 和与缓冲区相关的字段:

Go 的 Channel 很强大,理解其内在概念会让它更强大

缓冲通道的 hchan 结构

buffer(缓冲)由以下五个属性组成:

  • qcount 存储缓冲区中元素的当前数量
  • dataqsiz 存储缓冲区中最大元素的数量
  • buf 指向一个内存段,该内存段包含缓冲区中元素的最大数量的空间
  • sendx 存储缓冲区中的位置,以便通道接收下一个元素
  • recvx 在缓冲区中存储通道返回的下一个元素的位置

通过 sendxrecvx ,这个缓冲区就像一个循环队列:

Go 的 Channel 很强大,理解其内在概念会让它更强大

通道结构中的循环队列

这个循环队列允许我们在缓冲区中维护一个顺序,而不需要在其中一个元素从缓冲区弹出时不断移动元素。

正如我们在前一节中看到的那样,一旦达到缓冲区的上限,尝试在缓冲区中发送元素的 goroutine 将被移动到发送者列表中,并切换到等待状态。然后,一旦程序读取缓冲区,从缓冲区中返回位于 recvx 位置的元素,将释放等待的 goroutine ,它的值将被推入缓冲中。这种属性使 通道有 FIFO(先进先出) [3] 的行为。

由于缓冲区大小不足造成的延迟

我们在通道创建期间定义的缓冲区大小可能会极大地影响性能。我使用扇出模式来密集使用通道,以查看不同缓冲区大小的影响。以下是一些压力测试:

package bench

import (
    "sync"
    "sync/atomic"
    "testing"
)

func BenchmarkWithNoBuffer(b *testing.B) {
    benchmarkWithBuffer(b, 0)
}

func BenchmarkWithBufferSizeOf1(b *testing.B) {
    benchmarkWithBuffer(b, 1)
}

func BenchmarkWithBufferSizeEqualsToNumberOfWorker(b *testing.B) {
    benchmarkWithBuffer(b, 5)
}

func BenchmarkWithBufferSizeExceedsNumberOfWorker(b *testing.B) {
    benchmarkWithBuffer(b, 25)
}

func benchmarkWithBuffer(b *testing.B, size int) {
    for i := 0; i < b.N; i++ {
        c := make(chan uint32, size)

        var wg sync.WaitGroup
        wg.Add(1)

        go func() {
            defer wg.Done()

            for i := uint32(0); i < 1000; i++ {
                c <- i%2
            }
            close(c)
        }()

        var total uint32
        for w := 0; w < 5; w++ {
            wg.Add(1)
            go func() {
                defer wg.Done()

                for {
                    v, ok := <-c
                    if !ok {
                        break
                    }
                    atomic.AddUint32(&total, v)
                }
            }()
        }

        wg.Wait()
    }
}

在我们的基准测试中,一个生产者将在通道中注入一个 100 万个整数元素,而十个消费者将读取他们,并将它们累加到一个名为 total 的结果变量中。

我使用 benchstat 运行他们 10 次来分析结果:

name                                    time/op
WithNoBuffer-8                          306µs ± 3%
WithBufferSizeOf1-8                     248µs ± 1%
WithBufferSizeEqualsToNumberOfWorker-8  183µs ± 4%
WithBufferSizeExceedsNumberOfWorker-8   134µs ± 2%

一个适当大小的缓冲区确实可以使您的应用程序更快!让我们跟踪分析基准测试,以确定延迟在哪里。

追踪延迟

跟踪基准测试将使您访问同步阻塞概要文件,该概要文件显示等待同步原语的 goroutines 阻塞位于何处。Goroutines 在同步过程中花费了 9ms 的时间来等待无缓冲通道的值,而 50 大小的缓冲区只等待 1.9ms:

Go 的 Channel 很强大,理解其内在概念会让它更强大

同步阻塞概要

由于缓冲的存在,来自发送值的等待延迟减小了 5 倍:

Go 的 Channel 很强大,理解其内在概念会让它更强大

同步阻塞概要

我们现在确实证实了我们以前的怀疑。缓冲区的大小对应用程序的性能有重要影响。

via: https://medium.com/a-journey-with-go/go-buffered-and-unbuffered-channels-29a107c00268

作者: Vincent Blanchon [4] 译者: TomatoAres [5] 校对: DingdingZhou [6]

本文由 GCTT [7] 原创编译, Go 中文网 [8] 荣誉推出

参考资料

[1]

说明文档: https://golang.org/ref/spec#Channel_types

[2]

《Effective Go》: https://golang.org/doc/effective_go.html#channels

[3]

FIFO(先进先出): http://lsm6ds3%20fifo%20pattern/

[4]

Vincent Blanchon: https://medium.com/@blanchon.vincent

[5]

TomatoAres: https://github.com/TomatoAres

[6]

DingdingZhou: https://github.com/DingdingZhou

[7]

GCTT: https://github.com/studygolang/GCTT

[8]

Go 中文网: https://studygolang.com/

推荐阅读

喜欢本文的朋友,欢迎关注“ Go语言中文网

Go 的 Channel 很强大,理解其内在概念会让它更强大

Go语言中文网启用微信学习交流群,欢迎加微信: 274768166 ,投稿亦欢迎


以上所述就是小编给大家介绍的《Go 的 Channel 很强大,理解其内在概念会让它更强大》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

格蠹汇编

格蠹汇编

张银奎 / 电子工业出版社 / 2013-3-1 / 66.00元

《格蠹汇编——软件调试案例集锦》以案例形式讨论了使用调试技术解决复杂软件问题的工具和方法。全书共36章,分为四篇。前两篇每章讲述一个有代表性的真实案例,包括从堆里抢救丢失的博客,修复因误杀而瘫痪的系统,徒手战木马,拯救“发疯”的windows7,经典阅读器的经典死锁,拯救挂死的powerpoint,转储分析之双误谜团,是谁动了我的句柄,寻找系统中的“耗电大王”,解救即将被断网的系统,转储分析之系统......一起来看看 《格蠹汇编》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

html转js在线工具
html转js在线工具

html转js在线工具