news 2026/4/18 10:41:41

Goroutine间的“灵魂管道”:Channel如何实现数据同步与因果传递?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Goroutine间的“灵魂管道”:Channel如何实现数据同步与因果传递?

Channel是连接Goroutine的“管道”,是CSP理念在Golang中的具象化实现。它不仅是数据传递的队列,更是Goroutine间同步的天然工具,让开发者无需诉诸显式的锁或条件变量。

func main() {

ch := make(chan int, 1) // 创建一个int,缓冲区大小为1的Channel

ch <- 2 // 将2发送到ch

go func() { // 开启一个异步Goroutine

n, ok := <-ch // n接收从ch发出的值,如果没有接收到数据,将会阻塞等待

if ok {

fmt.Println(n) // 2

}

}()

close(ch) // 关闭Channel

}

Channel数据结构

Channel 在运行时使用src/runtime/chan.go 结构体表示。我们在 Go 语言中创建新的 Channel 时,实际上创建的是如下所示的结构。

type hchan struct {

qcount uint // 队列中所有数据总数

dataqsiz uint // 环形队列的 size

buf unsafe.Pointer // 指向 dataqsiz 长度的数组

elemsize uint16 // 元素大小

closed uint32

elemtype *_type // 元素类型

sendx uint // 已发送的元素在环形队列中的位置

recvx uint // 已接收的元素在环形队列中的位置

recvq waitq // 接收者的等待队列

sendq waitq // 发送者的等待队列

lock mutex

}

image

runtime.hchan 结构体中的五个字段 qcount、dataqsiz、buf、sendx、recv 构建底层的循环队列。除此之外,elemsize 和 elemtype 分别表示当前 Channel 能够收发的元素类型和大小。

sendq 和 recvq 存储了当前 Channel 由于缓冲区空间不足而阻塞的 Goroutine 列表,这些等待队列使用双向链表 runtime.waitq表示,链表中所有的元素都是runtime.sudog 结构。

type waitq struct {

first *sudog

last *sudog

}

runtime.sudog(Scheduling Unit Descriptor)是用于实现Goroutine调度的一种数据结构。它包含了与Goroutine相关的信息,如Goroutine的状态、等待的条件、等待的时间等。

当一个Goroutine需要等待某个事件或条件时,它会创建一个runtime.sudog,并将其加入到等待队列中。当事件或条件满足时,等

待队列中的runtime.sudog会被唤醒,从而允许对应的Goroutine继续执行。

Channel发送数据

1)如果等待接收的队列recvq中存在Goroutine,那么直接把正在发送的值发送给等待接收的Goroutine。

image

2)当缓冲区未满时,找到sendx所指向的缓冲区数组的位置,将正在发送的值拷贝到该位置,并增加sendx索引以及释放锁。

image

3)如果是阻塞发送,那么就将当前的Goroutine打包成一个sudog结构体,并加入到Channel的发送队列sendq里。

image

之后则调用goparkunlock将当前Goroutine设置为_Gwaiting状态并解锁,进入阻塞状态等待被唤醒;如果被调度器唤醒,执行清理

工作并最终释放对应的sudog结构体。

Channel接收数据

1)如果等待发送的队列sendq里存在挂起的Goroutine,那么有两种情况:当前Channel无缓冲区,或者当前Channel已满。从sendq中取出最先阻塞的Goroutine,然后调用recv方法,此时需做如下判断:

如果无缓冲区,那么直接从sendq接收数据;

如果缓冲区已满,从buf队列的头部接收数据,并把数据加到buf队列的尾部;

最后调用goready函数将等待发送数据的Goroutine的状态从_Gwaiting置为_Grunnable,等待下一次调度。

当缓冲区已满时的处理过程。

image

2)如果缓冲区buf中还有元素,那么就走正常的接收,将从buf中取出的元素拷贝到当前协程的接收数据目标内存地址中。值得注意的是,即使此时Channel已经关闭,仍然可以正常地从缓冲区buf中接收数据。

3)如果是阻塞模式,且当前没有数据可以接收,那么就需要将当前Goroutine打包成一个sudog加入到Channel的等待接收队列recvq中,将当前Goroutine的状态置为_Gwaiting,等待唤醒。

image

Channel与happens-before 关系

Channel happens-before 规则有 4 条。

1)对一个元素的send操作happens-before对应的receive 完成操作。

var c = make(chan int, 10) // buffered或者unbuffered

var a string

func f() {

// a 的初始化 happens-before 往ch中发送数据

a = "hello, world"

c <- 0

}

func main() {

go f()

// 往ch发送数据 happens-before 从ch中读取出数据

<-c

// 打印a的值 happens-after 第12行

// 打印a的结果值“hello world”

print(a)

}

2)对Channel的close操作happens-before receive 端的收到关闭通知操作。

var c = make(chan int, 10) // buffered或者unbuffered

var a string

func f() {

// a 的初始化 happens-before close ch

a = "hello, world"

close(c)

}

func main() {

go f()

// close ch happens-before 从ch中读取出数据

<-c

// 打印a的值 happens-after 第12行

// 打印a的结果值“hello world”

print(a)

}

3)对于Unbuffered Channel,对一个元素的receive 操作happens-before对应的send完成操作。

var c = make(chan int) // unbuffered

var a string

func f() {

// a 的初始化 happens-before 从ch中读取出数据

a = "hello, world"

<-c

}

func main() {

go f()

// 从ch中读取出数据 happens-before 往ch发送数据

c <- 0

// 打印a的值 happens after 第12行

// 打印a的结果值“hello world”

print(a)

}

4)如果 Channel 的容量是 c(c>0),那么,第 n 个 receive 操作 happens-before 第 n+c 个 send 的完成操作。规则3是规则4 c=0时的特例。

Channel使用场景

1)并发控制:通过控制带缓冲的Channel 的队列大小来限制并发的数量。

func worker(id int, sem chan struct{}) {

// 获取许可

sem <- struct{}{}

time.Sleep(time.Second) // 模拟耗时操作

// 释放许可

<-sem

}

func main() {

// 创建一个缓冲区为2的Channel

sem := make(chan struct{}, 2)

for i := 0; i < 5; i++ {

go worker(i, sem)

}

}

2)信号通知:使用一个无缓冲的 Channel 来通知一个 Goroutine 任务已经完成。

func main() {

done := make(chan bool)

go func() {

time.Sleep(2 * time.Second) // 模拟耗时操作

// 发送信号表示工作已完成

done <- true

}()

<-done // 等待信号

}

3)异步操作结果获取:在一个 Goroutine 中执行异步操作,然后通过 Channel 将结果发送到另一个 Goroutine。

func asyncTask() <-chan int {

ch := make(chan int)

go func() {

// 模拟异步操作

time.Sleep(2 * time.Second)

ch <- 1 // 发送结果

close(ch)

}()

return ch

}

func main() {

ch := asyncTask()

time.Sleep(1 * time.Second) // 模拟其他操作

result := <-ch // 获取异步操作的结果

}

总结:控制与编排,殊途同归

Java 与 Golang 在并发模型上的差异,深刻地体现了两种构建程序确定性的不同哲学:

1)Java (共享内存):采用显式同步的路径。它为开发者提供了强大的底层控制能力(锁、内存屏障),但要求开发者必须承担起预见并管理资源竞态的心智负担。确定性来自于对临界区和内存可见性的严格手工控制。

2)Golang (消息传递):采用隐式因果的路径。它通过 Channel 将数据的所有权在 Goroutine 间传递,将并发问题从“共享数据访问”转化为“数据流设计”。确定性来自于消息传递建立的自然因果顺序,从而在结构上规避了竞态。

Java的路径是“先有并发,后加约束”,而Golang的路径是“通过约束,实现并发”。两者并非优劣之分,而是针对不同问题域和开发哲学的选择。Java的完备工具集赋予了处理极端复杂场景的灵活性,而Golang的简约设计则为构建清晰、可靠、易于推理的并发系统提供了优雅的范式。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/18 6:59:40

MediaPipeUnityPlugin终极指南:快速实现Unity计算机视觉应用

MediaPipeUnityPlugin终极指南&#xff1a;快速实现Unity计算机视觉应用 【免费下载链接】MediaPipeUnityPlugin Unity plugin to run MediaPipe 项目地址: https://gitcode.com/gh_mirrors/me/MediaPipeUnityPlugin 想要在Unity项目中轻松集成人脸识别、手势追踪等先进…

作者头像 李华
网站建设 2026/4/17 20:22:24

UMA实战指南:5步构建高效催化剂计算工作流

UMA实战指南&#xff1a;5步构建高效催化剂计算工作流 【免费下载链接】ocp Open Catalyst Projects library of machine learning methods for catalysis 项目地址: https://gitcode.com/GitHub_Trending/oc/ocp 在催化剂设计与筛选领域&#xff0c;UMA机器学习势能正…

作者头像 李华
网站建设 2026/4/18 5:40:26

华为数通HCIA-Datacom H12-811题库分享(带解析)

应一些同学要求&#xff0c;分享一些备考H12-811的时候练习的题库。完整的题库已经发在题主小程序上了&#xff0c;需要的同学也可以自己去找。交换机收到数据帧的处理行为有?A、转发B、泛洪C、丢弃D、从接受端口再转发出去答案&#xff1a;ABC解析&#xff1a;交换机收到数据…

作者头像 李华
网站建设 2026/4/18 8:27:29

WebSSH的简单实现

为web的便利性&#xff0c;很多传统功能都有了web端的实现&#xff0c;WebSSH就是其中之一&#xff0c;我是第一次接触&#xff0c;所以来记录一下使用。WebSSH支持终端交互&#xff0c;主要可以分为两部分&#xff0c;第一是页面输入命令行并传递给远程终端&#xff0c;第二是…

作者头像 李华
网站建设 2026/4/18 13:51:04

基于MATLAB的支持向量机在故障诊断中的应用例程

一、基本SVM故障诊断例程 %% 基于SVM的故障诊断例程 % 作者&#xff1a;MATLAB助手 % 功能&#xff1a;使用SVM进行工业设备故障分类clear; close all; clc;%% 1. 生成模拟故障数据 % 假设我们监测设备的振动信号特征 rng(1); % 设置随机种子&#xff0c;确保结果可重现% 正常状…

作者头像 李华
网站建设 2026/4/18 12:12:28

CSDNGreener终极指南:如何彻底净化CSDN浏览体验

还在为CSDN页面上的各种广告和弹窗困扰吗&#xff1f;CSDNGreener这款专为Tampermonkey插件设计的绿化脚本&#xff0c;已经帮助无数开发者重获纯净的技术阅读环境。经过160多个版本的持续迭代&#xff0c;这款脚本已经成为CSDN优化的标杆工具。 【免费下载链接】CSDNGreener 《…

作者头像 李华