go并发编程| channel入门
channel
介绍
channel 是在 Go 的并发编程中使用的,这个工具的作用之一是 goroutine 之间通信(线程通信指的是多个线程之间通过共享数据或协作机制来协调操作,通常需要借助锁来保证同步)。Go 中推荐使用 channel(不同语言有不同的并发模型,例如 Java 使用共享内存并发模型,通过锁机制和条件变量实现线程间通信)。为什么是推荐呢?因为不用 channel,也可以用共享内存的方式进行通信,但这并不推荐(例如使用 sync.Mutex、sync.Map 等进行并发控制,虽然也能实现功能,但更容易出错、调试困难,违背了 Go “通过通信来共享内存”的设计理念),这里不展开。
- 共享内存模式 就像是多人在同一个白板上写字,需要规矩(锁)来约定谁能写。
- channel 模式 像是每人写好纸条,通过信封(channel)传递,彼此之间不直接碰同一个白板,避免冲突。
一句话总结:没有那么复杂,channel就是一个官方写好的数据结构,用来线程通信,很安全,放心用。
它的底层已经帮你封装好了:
- 带缓冲的 channel:用环形队列存数据;
- 不带缓冲的 channel:发送和接收必须同步进行;
- 内部用 锁(Mutex)和等待队列 保证并发安全。
使用
使用make初始化,eg.make(chan int)
,后面要跟一个类型,就是channel中放置的数据类型。
<-ch
是只读管道,ch<-
是只进管道,ch
是双端管道
ch := make(chan int, 2)go func() {for i := 0; i < 3; i++ {ch <- i + 1}close(ch)}()for i := range ch {// for中i就是管道中的值fmt.Println(i)}
channel具有阻塞的特点,也就是如果在取ch时无数据,当前goroutine会阻塞,如果当前ch满了还往里面放数据,当前goroutine也会阻塞在里面。
利用这个阻塞的特点,可以实现并发线程数量的控制,并发顺序的控制:
ch := make(chan int)// 这样goroutine只有结束后,主goroutine才会继续执行go func() {for i := 0; i < 3; i++ {fmt.Println(i)}ch <- 1}()<-ch
控制并发数量:
func TestName(t *testing.T) {ch := make(chan int, 5)wg := sync.WaitGroup{}for i := 0; i < 30; i++ {wg.Add(1)go func(i int) {ch <- 1defer wg.Done()defer func() { <-ch }() // defer必须调用一个函数fmt.Println(i)time.Sleep(1 * time.Second)}(i)}wg.Wait()
}
ch可以作为全局变量也可以作为参数传导,不过网上很多都推荐作为参数传递:
func doWork(i int, ch chan int, wg *sync.WaitGroup) {ch <- 1 // 获取令牌defer func() {<-ch // 释放令牌wg.Done()}()fmt.Println(i)time.Sleep(1 * time.Second)
}func TestName(t *testing.T) {ch := make(chan int, 5)var wg sync.WaitGroupfor i := 0; i < 30; i++ {wg.Add(1)// wg是值类型,所以要取指针,ch本身就是指针,所以不用再取指针了go doWork(i, ch, &wg)}wg.Wait()
}
解释一下为什么不用传指针:Go 中的 channel
(chan int
这种类型)在语义上是引用类型,其底层是一个指向 hchan
结构体的指针,所以在函数间传递 channel 实际上传的是指针的副本,多个函数操作的都是同一个 channel 实例。
原理
一句话总结:Go 的 channel
本质是一个线程安全的通信结构,底层是 hchan
结构体,借助环形缓冲区 + 双向队列 + 互斥锁 + 原子操作实现 协程间的通信与同步。
解释:
chan T
在底层对应的是 runtime.hchan
结构体,源码在 runtime/chan.go
。
type hchan struct {qcount uint // 队列中当前元素个数dataqsiz uint // 队列容量buf unsafe.Pointer // 环形队列缓冲区起始地址elemsize uint16 // 元素大小closed uint32 // 是否关闭sendx uint // 发送索引(环形队列位置)recvx uint // 接收索引(环形队列位置)recvq waitq // 等待接收的 goroutine 队列sendq waitq // 等待发送的 goroutine 队列lock mutex // 保证并发安全的锁
}
channel 在底层都是一个 hchan
结构体。当是带缓冲的 channel 时,它内部会维护一个环形队列用于缓存数据;当是无缓冲的 channel 时,数据只能在发送方和接收方同时存在时直接传递,不经过缓冲区。在多 goroutine 并发使用时,channel 的所有操作都由互斥锁(mutex)保护,以确保线程安全。
缓冲时的环形结构图:
buf: [v0, v1, v2, v3, v4]↑ ↑recvx sendx
非缓冲时:
对于无缓冲 channel
(make(chan T)
):
- 没有
buf
。 - 发送方必须等待接收方同时准备好,才能发送成功,真正的同步通信。
- 否则发送方会被挂起,进入
sendq
等待队列。
发送操作的伪代码:
func send(ch *hchan, val T) {lock(ch.lock) // ① 加锁,保证并发安全if ch.closed {panic("send on closed channel") // ② 关闭的 channel 不允许发送}if receiver := ch.recvq.dequeue(); receiver != nil {// ③ 有接收者在等,直接将值拷贝给接收者,唤醒它receiver.value = valready(receiver)} else if ch.qcount < ch.dataqsiz {// ④ 缓冲区未满,放入缓冲区ch.buf[ch.sendx] = valch.sendx = (ch.sendx + 1) % ch.dataqsizch.qcount++} else {// ⑤ 缓冲区已满,无接收者 —— 当前 sender 入队并阻塞enqueue(ch.sendq, currentGoroutine)park() // 将当前 goroutine 挂起}unlock(ch.lock) // ⑥ 解锁
}
接收操作的伪代码:
func recv(ch *hchan) (val T, ok bool) {lock(ch.lock) // ① 加锁if ch.qcount > 0 {// ② 缓冲区有数据,从缓冲区读取val = ch.buf[ch.recvx]ch.recvx = (ch.recvx + 1) % ch.dataqsizch.qcount--unlock(ch.lock)return val, true}if sender := ch.sendq.dequeue(); sender != nil {// ③ 有发送者在等,直接从 sender 拿值,唤醒 senderval = sender.valueready(sender)unlock(ch.lock)return val, true}if ch.closed {// ④ channel 已关闭,返回零值,ok = falseunlock(ch.lock)return zeroValue(T), false}// ⑤ 没人发,缓冲区也空,当前 receiver 入队并阻塞enqueue(ch.recvq, currentGoroutine)park() // 挂起当前 goroutine,等待唤醒unlock(ch.lock)// 被唤醒时,会从其他 sender 处获取 valreturn val, true
}
小问题
问题 | 解答 |
---|---|
channel 是值类型还是引用类型? | 引用类型,传的是底层 hchan 指针 |
无缓冲 channel 怎么通信? | 发送和接收必须同时准备好(同步通信) |
关闭 channel 会怎样? | 再次关闭会 panic;读取会读到零值且 ok==false |
发送到关闭的 channel? | panic |
从关闭的 channel 读取? | 如果还有数据没读完:正常返回数据。如果已经读完了:返回该类型的零值 + ok == false 。 |
是否可以判断 channel 是否满? | 不行,channel 没有 len() 和 cap() 外的并发判断能力 |
channel 会内存泄漏吗? | 如果 goroutine 被卡在 send/recv 而没人唤醒,会泄漏 |
相关文章:
go并发编程| channel入门
channel 介绍 channel 是在 Go 的并发编程中使用的,这个工具的作用之一是 goroutine 之间通信(线程通信指的是多个线程之间通过共享数据或协作机制来协调操作,通常需要借助锁来保证同步)。Go 中推荐使用 channel(不同…...

PH热榜 | 2025-05-29
1. Tapflow 2.0 标语:将你的文档转化为可销售的指导手册、操作手册和工作流程。 介绍:Tapflow 2.0将各类知识(包括人工智能、设计、开发、营销等)转化为有条理且可销售的产品。现在你可以导入文件,让人工智能快速为你…...
详解GPU
详解GPU GPU(图形处理器)就像电脑里的 “图形小能手”,原本主要用来画画(渲染图形),现在还能帮忙干很多杂活(并行计算) 一、先认识 GPU 的 “钥匙”:驱动和开发工具 装驱…...
WPF【11_10】WPF实战-重构与美化(配置Material UI框架)
11-16 【UI美化】配置Material UI框架 三种比较主流的 UI 设计规范,分别是: 苹果的扁平化 UI 设计、安卓或者说谷歌 的 Material Design 以及微软的 Metro 风格。 这三种风格都极具特色,不过我们接下来将会使用的是 Material Design 。在 W…...
(自用)Java学习-5.16(取消收藏,批量操作,修改密码,用户更新,上传头像)
1. 取消收藏功能 前端实现: 用户点击“取消收藏”按钮时,前端通过变量status判断当前状态(0为未收藏,1为已收藏)。 发送AJAX请求到后端接口: 添加收藏:/favoriteise/addFavoriteise?pid商品ID…...

【Node.js】部署与运维
个人主页:Guiat 归属专栏:node.js 文章目录 1. Node.js 部署概述1.1 部署的核心要素1.2 Node.js 部署架构全景 2. 传统服务器部署2.1 Linux 服务器环境准备系统更新与基础软件安装创建应用用户 2.2 应用部署脚本2.3 环境变量管理2.4 Nginx 反向代理配置2…...

【Java Web】速通JavaScript
参考笔记:JavaWeb 速通JavaScript_javascript 速通-CSDN博客 目录 一、JavaScript快速入门 1. 基本介绍 2. JavaScript特点 3. JavaScript的引入方式(重要) 3.1 写在script标签中 3.2 以外部文件方式引入 二、JS的数据类型 1. 变量 2. 常用数据类型 3.特殊值 三、…...

TDengine 运维——巡检工具(安装前预配置)
背景 TDengine 的安装部署对环境系统有一定的依赖和要求,安装部署前需要进行环境预配置操作,本文档旨在说明安装前预配置工具在安装 TDengine 前对环境的预配置内容和工具的使用方法。 预配置工具使用方法 工具支持通过 help 参数查看支持的语法 Usa…...
C#索引器详解:让对象像数组一样被访问
索引器是C#中一个强大而实用的特性,它允许我们像访问数组一样访问类的成员。本文将全面介绍索引器的概念、语法、实现方式以及实际应用场景。 索引器基础概念 索引器(Indexer)是一组get和set访问器,与属性类似,但有以…...
机器学习课设
🎓 图像处理课程设计任务书 课程名称: 图像处理与模式识别 课设题目: 基于手工特征提取与传统机器学习方法的图像分类系统实现 一、课设目的 本课程设计旨在加深对图像处理与分类算法的理解,提升图像特征提取、传统机器学习模…...
vue 如何对 div 标签 设置assets内本地背景图片
在 Vue 中为 <div> 设置 assets 目录下的本地背景图片,需要通过 Webpack 或 Vite 等构建工具 处理路径引用。以下是详细实现方法: 一、项目结构说明 假设你的项目结构如下: src/assets/images/bg.jpg # 背景图片components/…...
wsl2 docker重启后没了
参考这篇文章:wsl2 docker重启后没了_mob64ca12f55920的技术博客_51CTO博客...
ubuntu 22.04 配置静态IP、网关、DNS
1、打开配置文件 vi /etc/netplan/00-installer-config.yaml 2、修改文件内容 # This is the network config written by subiquity network:ethernets:ens33:dhcp4: false # 禁用 dhcpaddresses:- 192.168.12.15/24 # 静态IProutes:- to: defaultvia: 192.168.12.254 …...

RDS PostgreSQL手动删除副本集群副本的步骤
由于PostgreSQL不支持直接删除副本集群,而是需要先将副本集群升级到主实例(区域集群),然后在逐一将写入器实例删除,然后才可以删除副本集群 查看现有的主从实例集群 将副本集群提升到区域集群 选择副本集群–>操作–>提升 提升只读副本…...

MySQL 自增主键重置详解:保持 ID 连续性
目录 前言正文 前言 爬虫神器,无代码爬取,就来:bright.cn Java基本知识: java框架 零基础从入门到精通的学习路线 附开源项目面经等(超全)【Java项目】实战CRUD的功能整理(持续更新)…...
Vue Hook Store 设计模式最佳实践指南
Vue Hook Store 设计模式最佳实践指南 一、引言 在 Vue 3 组合式 API 与 TypeScript 普及的背景下,Hook Store 设计模式应运而生,它结合了 Vue 组合式 API 的灵活性与状态管理的最佳实践,为开发者提供了一种轻量级、可测试且易于维护的状态…...

国产化Word处理控件Spire.Doc教程:通过Java简单快速的将 HTML 转换为 PDF
在处理 HTML 文件时,你可能会发现它们在不同的浏览器和屏幕尺寸下的显示效果并不一致。而将 HTML 转换为 PDF 则可以有效地保留其布局和格式,从而确保内容在不同设备和平台上的呈现保持一致。本文将介绍如何在 Spire.Doc for Java 的帮助下通过 Java 将 …...
Spring AI 1.0 GA深度解析与最佳实践
随着人工智能技术的快速发展,Spring AI 1.0 GA 的发布标志着 Spring 生态在 AI 领域迈出了重要一步。本文将从原理、全景架构设计、最佳实践、性能测试对比等维度,全面解析如何基于 Spring AI 构建企业级 AI 应用,并以接入 DeepSeek 大模型为…...
Java求职面试:从Spring到微服务的技术挑战
Java求职面试:从Spring到微服务的技术挑战 在这个故事中,我们将进入一个模拟的互联网大厂Java求职者面试现场。面试官严肃而专业,而求职者谢飞机则以其幽默和捉摸不透的回答,让面试过程充满了趣味。 第一轮:基础框架…...
鸿蒙OSUniApp 开发的图文混排展示组件#三方框架 #Uniapp
使用 UniApp 开发的图文混排展示组件 在移动应用开发中,图文混排展示是资讯、社区、电商、教育等场景中极为常见的需求。一个灵活、美观的图文混排组件,不仅能提升内容的可读性,还能增强用户的视觉体验。随着 HarmonyOS(鸿蒙&…...
WHAT - 学习 WebSocket 实时 Web 开发
文章目录 一、基础知识了解1. WebSocket 是什么?2. 它的优势: 二、基本工作流程三、快速体验:使用原生 WebSocket客户端(浏览器端 JS):服务端(Node.js 示例,使用 ws 库) …...

5G NTN卫星通信发展现状(截止2025年3月)
今天咱们用实实在在的数据唠唠卫星通信这事儿—这些数字可比科幻片还刺激,直接告诉你这玩意儿现在有多火,未来能有多野! 先甩个大数字:截至2025年3月,全球已经有143个运营商和卫星厂商的合作项目,覆盖53个国…...

【计算机网络】第2章:应用层—DNS
目录 一、PPT 二、总结 DNS(域名系统)详解 (一)DNS核心概念 (二)DNS查询过程(重点❗) (三)DNS资源记录(RR)类型…...

[Linux]虚拟地址到物理地址的转化
[Linux]虚拟地址到物理地址的转化 水墨不写bug 文章目录 一、再次认识地址空间二、页表1、页表的结构设计2、页表节省了空间,省在哪里?3、页表的物理实现 一、再次认识地址空间 OS和磁盘交互的内存基本单位是4KB,这4KB通常被称为内存块。OS对…...

Linux线程入门
目录 Linux线程概念 什么是线程 重新理解进程 线程的优点 线程的缺点 线程的异常 线程用途 Linux线程概念 什么是线程 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”。一切进程至…...

Kubernetes超详细教程,一篇文章帮助你从零开始学习k8s,从入门到实战
k8s 概述 k8s github地址:https://github.com/kubernetes/kubernetes 官方文档:https://kubernetes.io/zh-cn/docs/home/ k8s,全程是 kubernetes,这个名字源于希腊语,意为"舵手"或"飞行员” k8s 这…...
Docker基础 -- Ubuntu 22.04 AArch64 交叉编译 Docker 镜像构建指南
Ubuntu 22.04 AArch64 交叉编译 Docker 镜像构建指南 作者: (填写作者) 发布日期: 2025‑05‑26 1 背景与目标 在企业内网(需要代理)环境下,我们需要一套可靠、可复用的 Ubuntu 22.04 交叉编…...
【Elasticsearch】使用脚本删除索引中的某个字段
在 Elasticsearch 中,删除索引中的某个字段可以通过以下几种方式实现,具体取决于你的需求和场景。以下是几种常见的方法: 方法 1:使用 _update_by_query API 删除字段 _update_by_query API 可以对索引中的文档执行批量更新操作&…...

OpenHarmony平台驱动使用(二),CLOCK
OpenHarmony平台驱动使用(二) CLOCK 概述 功能简介 CLOCK,时钟是系统各个部件运行的基础,以CPU时钟举例,CPU 时钟是指 CPU 内部的时钟发生器,它以频率的形式工作,用来同步和控制 CPU 内部的各…...

我们是如何为 ES|QL 重建自动补全功能的
作者:来自 Elastic Drew Tate Elasticsearch 拥有许多新功能,可以帮助你根据使用场景构建最佳搜索方案。浏览我们的示例笔记本了解更多内容,开始免费试用云服务,或者立即在本地机器上尝试 Elastic。 对于我们开发者来说࿰…...