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
}

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。

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

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

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

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

Channel接收数据

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

  1. 如果无缓冲区,那么直接从sendq接收数据;
  2. 如果缓冲区已满,从buf队列的头部接收数据,并把数据加到buf队列的尾部;
  3. 最后调用goready函数将等待发送数据的Goroutine的状态从_Gwaiting置为_Grunnable,等待下一次调度。

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

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

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

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的简约设计则为构建清晰、可靠、易于推理的并发系统提供了优雅的范式。

最终,无论是显式的同步约束,还是隐式的因果传递,它们都通向并发编程的圣杯——在多核时代,构建出可预测、可维护且高性能的软件系统。这两种思想的碰撞与融合,正持续推动着现代并发编程的演进。

很高兴与你相遇!如果你喜欢本文内容,记得关注哦

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

  1. Go语言中的管道(Channel)总结

    管道(Channel)是Go语言中比较重要的部分,经常在Go中的并发中使用.今天尝试对Go语言的管道来做以下总结.总结的形式采用问答式的方法,让答案更有目的性. Q1.管道是什么? 管道是Go语言在语 ...

  2. Go语言的管道Channel用法

    本文实例讲述了Go语言的管道Channel用法.分享给大家供大家参考.具体分析如下: channel 是有类型的管道,可以用 channel 操作符 <- 对其发送或者接收值. ch <- ...

  3. Java 线程间通讯(管道流方式)

    一.管道流是JAVA中线程通讯的常用方式之一,基本流程如下: 1)创建管道输出流PipedOutputStream pos和管道输入流PipedInputStream pis 2)将pos和pis匹配 ...

  4. Golang的channel使用以及并发同步技巧

    在学习<The Go Programming Language>第八章并发单元的时候还是遭遇了不少问题,和值得总结思考和记录的地方. 做一个类似于unix du命令的工具.但是阉割了一些功 ...

  5. 一个I/O线程可以并发处理N个客户端连接和读写操作 I/O复用模型 基于Buf操作NIO可以读取任意位置的数据 Channel中读取数据到Buffer中或将数据 Buffer 中写入到 Channel 事件驱动消息通知观察者模式

    Tomcat那些事儿 https://mp.weixin.qq.com/s?__biz=MzI3MTEwODc5Ng==&mid=2650860016&idx=2&sn=549 ...

  6. Oracle 10g通过创建物化视图实现不同数据库间表级别的数据同步

    摘自:http://blog.csdn.net/javaee_sunny/article/details/53439980 目录(?)[-] Oracle 10g 物化视图语法如下 实例演示 主要步骤 ...

  7. angular组件间的通信(父子、不同组件的数据、方法的传递和调用)

    angular组件间的通信(父子.不同组件的数据.方法的传递和调用) 一.不同组件的传值(使用服务解决) 1.创建服务组件 不同组件相互传递,使用服务组件,比较方便,简单,容易.先将公共组件写在服务的 ...

  8. (四十四)golang--协程(goroutine)和管道(channel)相结合实例

    统计1-8000之间的素数. 整体框架: 说明:有五个协程,三个管道.其中一个协程用于写入数字到intChan管道中,另外四个用于取出intChan管道中的数字并判断是否是素数,然后将素数写入到pri ...

  9. goroutine间的同步&协作

    Go语言中的同步工具 基础概念 竞态条件(race condition) 一份数据被多个线程共享,可能会产生争用和冲突的情况.这种情况被称为竞态条件,竞态条件会破坏共享数据的一致性,影响一些线程中代码 ...

  10. 有缓存区的管道channel

    package main import ( "fmt" "time" ) func main() { //创建一个有缓存区的管道 ch := make(chan ...

随机推荐

  1. 微服务架构PaaS平台,iPaaS平台支撑底座

    RestCloud所有产品均基于本微服务架构PaaS平台研发而来,底层PaaS平台是RestCloud所有产品的技术底座,基于本技术底座RestCloud快速研发了所有产品线,通过不断迭代PaaS平台 ...

  2. ETL数据集成丨使用ETLCloud实现MySQL与Greenplum数据同步

    我们在进行数据集成时,MySQL和Greenplum是比较常见的两个数据库,我们可以通过ETLCloud数据集成平台,可以快速实现MySQL数据库与数仓数据库(Greenplum)的数据同步. MyS ...

  3. 获奖公告|RestCloud应用场景分享征稿大赛

  4. POLIR-Laws-《消费者权益保护法》:商家制假售假 与 拼多多+中国人寿财险 的欺诈行为不正当竞争得利 赔偿: 全国人大: 建议完善惩罚性赔偿制度

    <消费者权益保护法>规定: 经营者如果存在欺诈行为,需要按照消费者的要求增加赔偿其受到的损失, 增加赔偿的金额为消费者购买商品的价款或者接受服务的费用的三倍: 增加赔偿的金额不足五百元的, ...

  5. Society-Business-ICEE+BigDataAIML-Sensor-产业升级换代: 数字化、自动化、 智慧化: 温度/湿度/气/液体压强/+

    高科技企业 Methodology方法论 是设施产业: 数字化.自动化.智慧化领军企业,例如: 商业:CRM.ERP.够.存.产.销.企业IM. 农业:种植.养殖.育种. 工业:决策.规划.生产.运营 ...

  6. JobSystem的使用场合

    1. 大规模行为更新,比如什么呢,上万个AI移动 2.大数量级的for循环 上面代码是for循环1000万次开方运算所需要的时间 对大数量级的for循环优化非常明显!

  7. 明日解决PCB开槽的事

    工作: 明日解决PCB开槽的事 兼职: 小论文数据处理的事情 健康 减肥八块腹肌 锻炼眼睛 万象: 观察一件事物,然后详细的用自己的土话描述,然后反复修改,发表出来 八杯水: 拉屎 吃饭 睡觉

  8. 第二章 创建第一个Web API项目

    2.1 项目模板选择 控制器型Web API与最小API ASP.NET Core提供了两种主要的Web API开发方式: 控制器型Web API (Controller-based Web API) ...

  9. 03Gin中间件开发与鉴权实践

    Gin中间件开发与鉴权实践 1. 中间件执行流程 [客户端请求] ↓ [Logger中间件] → 记录请求开始时间 ↓ [CORS中间件] → 处理跨域请求 ↓ [JWT鉴权] → 验证访问令牌 ↓ ...

  10. ansible-playbook基础

    主机与用户 你可以为 playbook 中的每一个 play,个别地选择操作的目标机器是哪些,以哪个用户身份去完成要执行的步骤(called tasks). hosts 行的内容是一个或多个组或主机的 ...