Golang语法进阶(并发概述、Goroutine、Channel)

文章目录

  • 并发概述
  • 1.进程与线程
  • 2. 协程
  • 3. 并行与并发
  • Goroutine
  • Goroutine使用
  • 主协程
  • 多协程调用
  • Channel
  • Channel是什么
  • channel初始化
  • channel操作
    • 判定读取
    • For range读取
  • 双向channel和单向channel
    • 定义单向读channel
    • 定义单向写channel
  • 解决什么问题?
    • 扩展
  • 关于channel的几点总结

并发概述

1.进程与线程

谈到并发或者并行一个绕不开的话题就是进程和线程,弄清楚进程与线程的本质是并发编程的前提,那么究竟什么是进程,什么是线程呢?

可以这样理解:

  • 进程就是运行着的程序,它是程序在操作系统的一次执行过程,是一个程序的动态概念,进程是操作系统分配资源的基本单位
  • 线程可以理解为一个进程的执行实体,它是比进程粒度更小的执行单元,也是真正运行在CPU上的执行单元线程是CPU调度资源的基本单位

进程中可以包含多个线程,需要记住进程和线程一个是操作系统分配资源的基本单位(进程),一个是操作系统调度资源的基本单位(线程)。

在同一进程中,线程的切换不会引起进程切换。在不同进程中进行线程切换,如从一个进程内的线程切换到另一个进程中的线程时,会引起进程切换

2. 协程

协程可以理解为用户态线程,是更微量级的线程。区别于线程,协程的调度在用户态进行,不需要切换到内核态,所以不由操作系统参与,由用户自己控制。在一些支持协程高级语言中,往往这些语言都实现了自己的协程调度器,比如go语言就有自己的协程调度器

协程有独立的栈空间,但是共享堆空间

一个进程上可以跑多个线程,一个线程上可以跑多个协程

3. 并行与并发

很多时候大家对于并行和并发的概念还比较模糊,其实只需要根据一点来判断即可,能不能同时运行两个任务能同时运行就是并行,不能同时运行,而是每个任务执行一小段,交叉执行,这种模式就是并发


图3.1 并行

图3.2 并发

如图3.1所示,两个任务一直运行,切实同时运行着,这就是并行模式,要注意并行的话一定要有多个核的支持,因为只有一个cpu的话,同一时间只能跑一个任务,如图3.2所示,两个任务,每次只执行一小段,这样交叉的执行,就是并发模式,并发模式在单核cpu上是可以完成的。

从根上理解用户态与内核态

Goroutine

goroutine就是go语言对于协程的支持,可以把它理解为go语言的协程。
这是一个go语言并发编程的终极杀器,它让我们的并发编程变得简单。

go语言的并发只会用到goroutine,并不需要我们去考虑用多进程或者是多线程。有过c++或者java经验的同学可能知道,线程本身是有一定大小的,一般OS线程栈大小为2MB且线程在创建和上下文切换的时候是需要消耗资源的,会带来性能损耗,所以在我们用到多线程技术的时候,我们往往会通过池化技术,即创建线程池来管理一定数量的线程

在go语言中,一个goroutine栈在其生命周期开始时占用空间很小(一般2KB)并且栈大小可以按需增大和缩小,goroutine的栈大小限制可以达到1GB,但是一般不会用到这么大。所以在Go语言中一次创建成千上万,甚至十万左右的goroutine理论上也是可以的。

在go语言中,我们用多goroutine来完成并发,在某个任务需要并发执行的时候,只需要把这个任务包裹成一个函数,开启一个goroutine去执行这个函数就可以了,并不需要我们来维护一个类似于线程池的东西,也不需要我们去关心协程是怎么切换和调度的,因为这些都已经有go语言内置的调度器帮我们做了,并且效率还非常高。

Goroutine使用

goroutine使用起来非常方便,通常我们会将需要并发的任务封装成一个函数,然后在该函数前加上go关键字就行了,这样就开启了一个goroutine。

func()gofunc()// 会并发执行这个函数

主协程

和其它语言一样,go程序的入口也是main函数。在程序开始执行的时候,Go程序会为main()函数创建一个默认的goroutine,我们称之为主协程,我们后来人为的创建的一些goroutine,都是在这个主goroutine的基础上进行的。

下面请看个例子:

packagemainimport"fmt"funcmyGroutine(){fmt.Println("myGroutine")}funcmain(){gomyGroutine()fmt.Println("end!!!")}

运行结果:

end!!!

很奇怪,明明是多协程任务,为什么只打印了主协程里的"end! ! ! “,而没有打印我们开启的协程里的输出"myGroutine”,按理不是应该都打印出来吗?

这是因为:当main()函数返回的时候该goroutine就结束了,当主协程退出的时候,其他剩余的协程不管是否运行完,都会跟着结束。所以,这里主协程打印完"end! ! ! "之后就退出了,myGroutine协程可能还没运行到fmt.Println(“myGroutine”)语句也跟着退出了。

接下来我们让主goroutine执行完fmt.Println(“end!!!”)之后不立刻退出,而是等待2s,看一下运行结果:

packagemainimport("fmt""time")funcmyGroutine(){fmt.Println("myGroutine")}funcmain(){gomyGroutine()fmt.Println("end!!!")time.Sleep(2*time.Second)}

运行结果:

end!!!myGroutine

此时打印出了我们想要的结果,这里我们通过让主协程睡眠2s来等待子协程执行完了之后再退出

多协程调用

packagemainimport("fmt""sync""time")funcmyGoroutine(namestring,wg*sync.WaitGroup){deferwg.Done()fori:=0;i<5;i++{fmt.Printf("myGoroutine %s\n",name)time.Sleep(10*time.Millisecond)}}funcmain(){varwg sync.WaitGroup wg.Add(2)gomyGoroutine("goroutine1",&wg)gomyGoroutine("goroutine2",&wg)wg.Wait()}

运行结果:

myGoroutine goroutine2 myGoroutine goroutine1 myGoroutine goroutine1 myGoroutine goroutine2 myGoroutine goroutine1 myGoroutine goroutine2 myGoroutine goroutine2 myGoroutine goroutine1 myGoroutine goroutine1 myGoroutine goroutine2
fori:=0;i<5;i++{fmt.Printf("myGoroutine %s\n",name)time.Sleep(10*time.Millisecond)}

也可以写成

forrange5{fmt.Printf("myGoroutine %s\n",name)time.Sleep(10*time.Millisecond)}

Channel

可以通过go关键字来开启一个goroutine,我们的样例代码逻辑很简单,都是在各个goroutine各自处理自己的逻辑,但有时候我们需要不同的goroutine之间能够通信,这里就要用到channel。

Channel是什么

官方定义:

Channels are a typed conduit through which you can send and receive values with the channel operator

Channel是一个可以收发数据的管道

channel初始化

channel的声明方式如下:

varchannel_namechanchannel_typevarchannel_name[size]chanchannel_type// 声明一个chan 数组,其容量大小为size

声明之后的管道,并没有进行初始化为其分配空间,其值是nil,我们要使用还要配合make函数来对其初始化,之后才可以在程序中使用该管道。

channel_name=make(chanchannel_type)channel_name=make(chanchannel_type,size)

或者我们可以直接一步完成

channel_name:=make(chanchannel_type)channel_name:=make(chanchannel_type,size)//创建带有缓存的管道,size为缓存大小

channel操作

传递数据:

ch:=make(chanint)// 创建一个管道chch<-v// 向管道ch中发送数据v。v:=<-ch// 从管道中读取数据存储到变量vclose(ch)// 关闭管道ch

执行到ch <- v 时,没有任何其他 goroutine 正在接收 <-ch,发送方会一直阻塞,根本走不到下一行 v := <-ch,也走不到 close(ch)

在这里需要注意close(ch)这个操作,管道用完了,需要对其进行关闭,避免程序一直在等待以及资源的浪费。但是关闭的管道,仍然可以从中接收数据,只是接收到的数据永远是零值。

看下面例子:

packagemainimport("fmt""time")funcmain(){ch:=make(chanint,5)ch<-1close(ch)gofunc(){fori:=0;i<5;i++{v:=<-ch fmt.Printf("v=%d\n",v)}}()time.Sleep(2*time.Second)}

运行结果:

v=1v=0v=0v=0v=0

管道中还有数据:此时可以正常地从管道中读取到实际的值。

管道中没有数据:此时从管道中读取的值将是该类型的零值,不会阻塞等待数据。

创建一个缓存为5的int类型的管道,向管道里写入一个1之后,将管道关闭,然后开启一个goroutine从管道读取数据,读取5次,可以看到即便管道关闭之后,他仍然可以读取数据,在读完数据之后,将一直读取零值。

但是,上述读取方式还有一个问题?比如我们创建一个int类型的channel,我们需要往里面写入零值,用另一个goroutine读取,此时我们就无法区分读到的是正确的零值还是数据已经读完了而读到的零值。

所以我们一般用以下两种常用的读取方式:

判定读取

还是以上面的例子来看,稍作修改

packagemainimport("fmt""time")funcmain(){ch:=make(chanint,5)ch<-1close(ch)gofunc(){fori:=0;i<5;i++{v,ok:=<-ch// 判断式读取ifok{fmt.Printf("v=%d\n",v)}else{fmt.Printf("channel数据已读完, v=%d\n",v)}}}()time.Sleep(2*time.Second)}

运行结果:

v=1channel数据已读完,v=0channel数据已读完,v=0channel数据已读完,v=0channel数据已读完,v=0

在读取channel数据的时候,用ok句式做了判断,当管道内还有数据能读取的时候,ok为true,当管道关闭后,ok为false。

For range读取

在上面例子中,我们明确了读取的次数是5次,但是我们往往在更多的时候,是不明确读取次数的,只是在Channel的一段读取数据,有数据我们就读,直到另一段关闭了这个channel,这样就可以用for range这种优雅的方式来读取channel中的数据了

packagemainimport("fmt""time")funcmain(){ch:=make(chanint,5)ch<-1ch<-2close(ch)gofunc(){forv:=rangech{fmt.Printf("v=%d\n",v)}}()time.Sleep(2*time.Second)}

运行结果:

v=1v=2

主goroutine往channel里写了两个数据1和2,然后关闭,子channel也只能读到1和2。这里在主goroutine关闭了channel之后,子goroutine里的for range循环才会结束。

for range ch 会在 channel 被关闭且把缓冲区里剩余的数据都读完之后自动退出循环。

双向channel和单向channel

channel根据其功能又可以分为双向channel和单向channel,双向channel即可发送数据又可接收数据单向channel要么只能发送数据,要么只能接收数据。

定义单向读channel

varch=make(chanint)// 给` <-chan int `取了个别名RChanneltypeRChannel=<-chanint// 定义类型varrec RChannel=ch

定义单向写channel

varch=make(chanint)typeSChannel=chan<-int// 定义类型varsend SChannel=ch

注意写channel与读channel在定义的时候只是<-的位置不同,前者在chan关键字后面,后者在chan关键字前面。

代码示例:

import("fmt""time")typeSChannel=chan<-inttypeRChannel=<-chanintfuncmain(){varch=make(chanint)// 创建channelgofunc(){varsend SChannel=ch fmt.Println("send: 100")send<-100}()gofunc(){varrec RChannel=ch num:=<-rec fmt.Printf("receive: %d",num)}()time.Sleep(2*time.Second)}

运行结果:

send:100receive:100

创建一个channel ch,分别定义两个单向channel类型SChannel 和 RChannel,根据别名类型给ch定义两个别各send和rec,一个只用于发送,一个只用于读取

解决什么问题?

Channel非常重要,Golang中有个重要思想:

不以共享内存来通信,而以通信来共享内存。

说得更直接点,协程之间可以利用Channel来传递数据,如下的例子,可以看出父子协程如何通信的,父协程通过Channel拿到了子协程执行的结果。

packagemainimport("fmt""time")funcsum(s[]int,cchanint){sum:=0for_,v:=ranges{sum+=v}c<-sum// send sum to c}funcmain(){s:=[]int{7,2,8,-9,4,0}c:=make(chanint)gofunc(){sum(s[:len(s)/2],c)//time.Sleep(1 * time.Second)}()gosum(s[len(s)/2:],c)x,y:=<-c,<-c// receive from cfmt.Println(x,y,x+y)}

运行结果:

-51712

channel 又分为两类:有缓冲 channel 和无缓冲 channel,这个在前面的代码示例中也有简单的描述了。为了协程安全,无论是有无缓冲的 channel,内部都会有一把锁来控制并发访问。

同时 channel 底层一定有一个队列,来存储数据。

无缓冲 channel 可以理解为同步模式,即写入一个,如果没有消费者在消费,写入就会阻塞。

有缓冲 channel 可以理解为异步模式。即写入消息之后,即使还没被消费,只要队列没满,就可继续写入。如图所示:

这里可能会问,如果有缓冲 channel 队列满了,那不就退化到同步了么?是的,如果队列满了,发送还是会阻塞。

但是我们来反向思考下,如果有缓冲 channel 长期都处于满队列情况,那何必用有缓冲。所以预期在正常情况下,有缓冲 channel 都是异步交互的。

扩展

上面说了当缓冲队列满了以后,继续往channel里面写数据,就会阻塞,那么利用这个特性,我们可以实现一个goroutine之间的锁。

直接看示例

packagemainimport("fmt""time")funcadd(chchanbool,num*int){ch<-true// 拿锁:往盒子里放一个 token*num=*num+1<-ch// 放锁:把 token 拿出来}funcmain(){// 创建一个size为1的channelch:=make(chanbool,1)varnumintfori:=0;i<100;i++{goadd(ch,&num)}time.Sleep(2*time.Second)fmt.Println("num 的值: ",num)}

运行结果:

num 的值:100

用“容量为 1 的缓冲 channel”当成一把互斥锁(binary semaphore),让 100 个 goroutine 对 num 的 +1 操作串行执行,从而避免数据竞争。

ch <- true和<-ch就相当于一个锁,将 *num = *num + 1这个操作锁住了。因为ch管道的容量是1,在每个add函数里都会往channel放置一个true,直到执行完+1操作之后才将channel里的true取出。由于channel的size=1,所以当一个goroutine在执行add函数的时候,其他goroutine执行add函数,执行到ch <- true的时候就会阻塞,*num = *num + 1不会成功,直到前一个+1操作完成,<-ch,读出了管道的元素,这样就实现了并发安全

关于channel的几点总结

  • 关闭一个未初始化的 channel 会产生 panic
  • channel只能被关闭一次,对同一个channel重复关闭会产生 panic
  • 向一个已关闭的 channel 发送消息会产生 panic
  • 从一个已关闭的channel读取消息不会发生panic,会一直读取所有数据,直到零值
  • channel可以读端和写端都可有多个goroutine操作,在一端关闭channel的时候,该channel读端的所有goroutine都会收到channel已关闭的消息
  • channel是并发安全的,多个goroutine同时读取channel中的数据,不会产生并发安全问题

之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1136779.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

‌移动App用户体验(UX)测试技巧:专业指南与实战策略

UX测试在移动App中的核心地位‌在2026年的数字生态中&#xff0c;移动App已成为用户交互的主要入口&#xff0c;用户体验&#xff08;UX&#xff09;质量直接决定产品成败。对于软件测试从业者而言&#xff0c;UX测试不再是可有可无的附加项&#xff0c;而是确保App竞争力、用户…

深入浅出讲解ModbusTCP报文封装与解码过程

深入拆解ModbusTCP报文&#xff1a;从封装到解析的实战全路径在工业自动化现场&#xff0c;你是否曾遇到过这样的场景&#xff1f;一台PLC明明通电正常&#xff0c;HMI却始终读不到数据&#xff1b;抓包工具里看到一串十六进制数来回传输&#xff0c;但就是不知道哪里出了问题。…

Leetcode—865. 具有所有最深节点的最小子树【中等】

2025每日刷题&#xff08;236&#xff09; Leetcode—865. 具有所有最深节点的最小子树实现代码 /*** Definition for a binary tree node.* type TreeNode struct {* Val int* Left *TreeNode* Right *TreeNode* }*/ func subtreeWithAllDeepest(root *TreeNode) …

一文说清Proteus示波器如何配合8051进行波形观测

用Proteus示波器看8051输出的波形&#xff0c;其实比你想象的简单在嵌入式开发的世界里&#xff0c;“我代码写完了&#xff0c;但信号到底出没出来&#xff1f;”是每个工程师都会遇到的灵魂拷问。真实项目中&#xff0c;我们靠示波器抓波形、逻辑分析仪看时序。可如果你是在实…

基于springboot旅游网站

基于 SpringBoot 的旅游网站是一款集旅游信息展示、产品预订、用户互动于一体的综合性在线平台&#xff0c;借助 SpringBoot 框架的高效性和稳定性&#xff0c;为用户提供目的地查询、行程规划、酒店门票预订等一站式旅游服务&#xff0c;同时为旅游商家提供产品管理和订单处理…

springboot基于微信小程序的校园租赁小程序

SpringBoot基于微信小程序的校园租赁小程序介绍 一、系统定位与背景 随着共享经济的兴起和校园租赁市场的不断扩大&#xff0c;基于微信小程序的校园租赁小程序应运而生。该系统旨在通过微信小程序这一便捷的平台&#xff0c;为校园内的学生、教师及工作人员提供高效、便捷的物…

融媒体中心巡察报告对象主要有哪些?

融媒体中心作为“统筹策划、一次采集、多种生成、多元传播”的综合性平台&#xff0c;其巡察报告的对象覆盖面非常广。它不仅包含物理层面的发布渠道&#xff0c;还包含逻辑层面的内容数据以及管理层面的制度流程。具体而言&#xff0c;巡察报告重点聚焦以下四大类对象&#xf…

Leetcode—1123. 最深叶节点的最近公共祖先【中等】

2025每日刷题&#xff08;236&#xff09; Leetcode—1123. 最深叶节点的最近公共祖先实现代码 /*** Definition for a binary tree node.* type TreeNode struct {* Val int* Left *TreeNode* Right *TreeNode* }*/ func lcaDeepestLeaves(root *TreeNode) *TreeN…

【视频优化研究】过程 记录

videoimprove - AtomGit | GitCode \\10.1.1.153\01-部门空间\系统集成部\黑光布控球和摄像机在不同光照强度下视频画面对比\video-2.rar \\10.1.1.153\01-部门空间\系统集成部\不同场景下800B对讲声音采集\DeepFilterNet3_onnx.rar D:\java\videoImprove\video-2\video-2

ModbusTCP协议报文解析图解说明

ModbusTCP报文解析&#xff1a;一张图看懂工业通信的底层逻辑在智能制造和工业自动化的浪潮中&#xff0c;设备之间的“对话”从未像今天这般频繁。而在这场无声的数据洪流里&#xff0c;有一个协议始终默默支撑着无数产线的稳定运行——ModbusTCP。它不像OPC UA那样华丽&#…

redis相关命令讲解及原理

redis相关命令讲解及原理 某一个元素没了&#xff0c;会删除key https://gitee.com/HGtz2222/classroom_code/tree/master/redis-code 通过key找到对应的value&#xff0c;而关系数据库通过b树索引。 这里的string不是字符串即‘\0\结尾&#xff0c;而是以长度定义。’ 集…

springboot校园快递仓库管理系统

基于 SpringBoot 的校园快递仓库管理系统是一款针对高校快递收发场景设计的数字化管理平台&#xff0c;借助 SpringBoot 框架的高效后端能力&#xff0c;整合快递入库、存储、出库、取件通知等全流程功能&#xff0c;旨在解决校园快递量大、取件效率低、错拿漏拿等问题&#xf…

MATLAB实现基于Sinkhorn距离的非负矩阵分解乘法更新规则

在上一篇文章中,我们介绍了SDNMF的主入口函数,今天深入其核心优化部分——乘法更新规则的实现。SDNMF(Non-negative Matrix Factorization with Sinkhorn Distance)通过将传统的Frobenius重构误差替换为带熵正则化的Sinkhorn距离(也称为熵正则化的最优传输距离),并结合图…

图解说明CANFD帧结构在汽车网络中的变化

图解CAN FD如何重塑汽车通信&#xff1a;从帧结构到实战应用你有没有遇到过这样的场景&#xff1f;一台自动驾驶测试车的摄像头源源不断传来图像数据&#xff0c;毫米波雷达也在实时上报目标信息。可总线负载却一路飙升&#xff0c;逼近90%——工程师们盯着诊断仪眉头紧锁&…

电路仿真circuits网页版一文说清:其与传统桌面工具的本质区别

电路仿真网页版&#xff1a;一场从桌面到浏览器的静默革命你有没有试过在咖啡馆用笔记本电脑打开LTspice&#xff0c;结果发现安装包下载了一半&#xff0c;VC运行库报错&#xff0c;而隔壁的学生却只用一个链接就在iPad上跑通了Arduino呼吸灯&#xff1f;这不是偶然——这是一…

替代HT6310/KP3310离线式AC-DC无感线性稳压器

概述&#xff1a;&#xff08;替代HT6310/KP3310&#xff09;PC6310 是一款紧凑型无电感设计的离线式线性稳压器。PC6310 输出电压已由内部设定为 5V/3.3V/2.7V 三个版本。PC6310 是一种简单可靠的获得偏置供电的离线式电源解决方案。PC6310 集成了 650V 功率 MOSFET&#xff0…

springboot新乡工程学院失物招领平台

基于 Spring Boot 的新乡工程学院失物招领平台介绍 在校园生活中&#xff0c;物品遗失与寻找是师生们常面临的困扰。为有效解决这一问题&#xff0c;新乡工程学院依托 Spring Boot 框架开发了失物招领平台。该平台借助 Spring Boot 强大的后端开发能力&#xff0c;整合多种功能…

出口欧盟产品合规,到底包括哪些内容?

很多企业在做出口欧盟合规时&#xff0c;都会问一句话&#xff1a;“你先告诉我&#xff0c;我到底要做哪些合规&#xff1f;”但现实往往是——刚开始以为只要做一个认证&#xff0c;做到一半发现还要补资料&#xff0c;最后才意识到&#xff1a;自己连合规“包含哪些内容”都…

图解说明aarch64异常处理机制:EL0到EL3切换逻辑

深入理解 aarch64 异常处理机制&#xff1a;从用户程序到安全监控的全路径解析你有没有想过&#xff0c;当你在手机上点击一个应用时&#xff0c;背后究竟发生了多少次“特权跃迁”&#xff1f;一条看似简单的系统调用&#xff0c;可能已经穿越了四层执行等级、触发了多次上下文…

租赁中介用什么房产中介管理系统合适

在租赁房产交易场景中&#xff0c;房源分散、客源跟进不及时、带看流程混乱、合同管理繁琐等问题&#xff0c;一直是困扰房产中介的核心痛点。选择一套适配的房产中介管理系统&#xff0c;成为提升运营效率、降低管理成本的关键。对于以租赁业务为主的中介机构而言&#xff0c;…