88

在(简要地)回顾了 Go 语言规范、有效的 Go 和 Go 内存模型之后,我仍然有点不清楚 Go 通道是如何工作的。

它们是什么样的结构?它们的行为有点像线程安全队列/数组。

它们的实现是否依赖于架构?

4

4 回答 4

94

频道的源文件(来自您的 go 源代码根目录)位于/src/pkg/runtime/chan.go中。

hchan是通道的中心数据结构,带有发送和接收链表(保存指向它们的 goroutine 和数据元素的指针)和一个closed标志。在 runtime2.go 中定义了一个Lock嵌入式结构,根据操作系统用作互斥体(futex)或信号量。锁定实现在 lock_futex.go (Linux/Dragonfly/Some BSD) 或 lock_sema.go (Windows/OSX/Plan9/Some BSD) 中,基于构建标签。

通道操作都在这个 chan.go 文件中实现,所以可以看到 makechan、发送和接收操作,以及 select 构造、close、len 和 cap 内置。

要深入了解通道的内部工作原理,您必须阅读Dmitry Vyukov 本人的Go channels on steroids(Go 核心开发、goroutines、调度程序和通道等)。

于 2013-10-27T20:23:29.800 回答
16

这是一个很好的演讲,大致描述了渠道是如何实现的:
https ://youtu.be/KBZlN0izeiY

谈话说明:

GopherCon 2017:Kavya Joshi - 了解渠道

通道为 goroutine 提供了一种简单的通信机制,并为构建复杂的并发模式提供了强大的构造。我们将深入研究通道和通道操作的内部工作原理,包括运行时调度程序和内存管理系统如何支持它们。

于 2019-01-11T23:52:33.730 回答
7

你问了两个问题:

  1. 它们是什么样的结构?

Go 中的通道确实“有点像线程安全队列”,更准确地说,Go 中的通道具有以下属性:

  • goroutine 安全的
  • 提供 FIFO 语义
  • 可以在 goroutine 之间存储和传递值
  • 导致 goroutine 阻塞和解除阻塞

每次创建通道时,都会在堆上分配一个hchan结构,并返回指向 hchan 内存位置的指针,表示为通道,这就是 go-routines 可以共享它的方式。

上面描述的前两个属性的实现类似于带锁的队列。通道可以传递给不同 go-routines 的元素被实现为一个循环队列(环形缓冲区),在 hchan 结构中具有索引,索引说明了缓冲区中元素的位置。

循环队列:

qcount   uint           // total data in the queue
dataqsiz uint           // size of the circular queue
buf      unsafe.Pointer // points to an array of dataqsiz elements

和指数:

sendx    uint   // send index
recvx    uint   // receive index

每次 go-routine 需要访问通道结构并修改它的状态时,它都会持有锁,例如:将元素复制到缓冲区/从缓冲区复制元素、更新列表或索引。一些操作被优化为无锁,但这超出了这个答案的范围。

go channel 的 block 和 un-block 属性是使用两个队列(链表)来实现的,这些队列(链表)保存被阻塞的 go-routines

recvq    waitq  // list of recv waiters
sendq    waitq  // list of send waiters

每次 go-routine 想要将任务添加到满通道(缓冲区已满),或者从空通道(缓冲区为空)中获取任务时,都会分配一个伪 go-routine sudog结构体,然后 go-routine相应地将 sudog 作为节点添加到发送或接收服务员列表中。然后 go-routine 使用特殊调用更新 go 运行时调度程序,这会提示它们何时应该退出执行 ( gopark) 或准备运行 ( goready)。请注意,这是一个非常简化的解释,其中隐藏了一些复杂性。

  1. 它们的实现是否依赖于架构?

除了@mna已经解释的特定于操作系统的锁实现之外,我不知道任何体系结构特定的约束优化或差异。

于 2022-02-02T16:15:08.013 回答
0

查看通道的一种更简单的方法是这样,因为您可能希望在等待条件完成时暂停程序,通常用于防止 RACE 条件,这意味着一个线程可能不会在另一个之前完成,然后您的后来的线程或代码依赖有时不完整。例如,您有一个线程从数据库或其他服务器检索一些数据并将数据放入变量、切片或映射中,但由于某种原因它被延迟了。那么您有一个使用该变量的进程,但是由于它尚未初始化,或者尚未获得其数据。程序失败。所以在代码中查看的简单方法如下:package main

import "fmt"

var doneA = make(chan bool)
var doneB = make(chan bool)
var doneC = make(chan bool)

func init() { // this runs when you program starts.
    go func() {
        doneA <- true  //Give donA true
    }()
}

func initB() { //blocking
    go func() {
        a := <- doneA  //will wait here until doneA is true
        // Do somthing here
        fmt.Print(a)
        doneB <- true //State you finished
    }()
}

func initC() {
    go func() {
        <-doneB // still blocking, but dont care about the value
        // some code here
        doneC <- true // Indicate finished this function
    }()
}

func main() {
    initB()
    initC()
}

所以希望这会有所帮助。不是上面选择的答案,但我相信应该有助于消除谜团。我想知道我是否应该提出问题并自我回答?

于 2021-03-31T05:00:59.800 回答