【启程Golang之旅】协程和管道操作

欢迎来到Golang的世界!在当今快节奏的软件开发领域,选择一种高效、简洁的编程语言至关重要。而在这方面,Golang(又称Go)无疑是一个备受瞩目的选择。在本文中,带领您探索Golang的世界,一步步地了解这门语言的基础知识和实用技巧。

目录

初识协程

启动多协程

初识锁

初识管道

协程与管道协同


初识协程

在go语言中,"协程"(coroutine)通常指的是go的轻量级线程,也被称为goroutine,它使得并发编程变得更加简单和高效。这里我先对go语言中其他概念进行一个简单叙述:

1)程序(program)

是为完成特定任务、用某种语言编写的一组指令的集合,是一段静态的代码(程序是静态的)

2)进程(process)

是程序的一次执行过程,正在运行的一个程序,进程作为资源分配的单位,在内存中会为每个进程分配不同的内存区域。(进程是动态的)是一个动的过程,进程的生命周期:有它自身的产生、存在和消亡的过程

3)线程(thread)

进程可进一步细化为线程,是一个程序内部的一条执行路径,若一个进程同一时间并行执行多个线程,就是支持多线程的。

4)协程(goroutine)

又称为微线程,纤程,协程是一种用户态的轻量级线程

与传统的线程相比,goroutine非常轻量,它们的创建和销毁成本很低,因此可以大量创建而不会对系统造成太大的负担,因此它是go实现高效并发编程的核心机制之一。

协程的作用:在执行A函数的时候,可以随时中断,去执行B函数,然后中断继续执行A函数(可以自动切换),注意这一切换过程并不是函数调用(没有调用语句),过程很像多线程,然而协程中只有一个线程在执行 (协程的本质是个单线程),如下图所示:

        对于单线程下,我们不可避免程序中出现io操作,但如果我们能在自己的程序中(即用户程序级别,而非操作系统级别)控制单线程下的多个任务能在一个任务遇到io阻塞时就将寄存器上下文和栈保存到某个其他地方,然后切换到另外一个任务去计算。在任务切回来的时候,恢复先前保存的寄存器上下文和栈,这样就保证了该线程能够最大限度地处于就绪态,即随时都可以被cpu执行的状态。

        相当于我们在用户程序级别将自己的io操作最大限度地隐藏起来,从而可以迷惑操作系统,让其看到:该线程好像是一直在计算,io比较少,从而会更多的将cpu的执行权限分配给我们的线程(注意:线程是CPU控制的,而协程是程序自身控制的,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级) 

我们老说协程协程,那么协程到底如何实现呢?这里给出下面的案例进行讲解:

请编写一个程序,完成如下功能:

1)在主线程中,开启一个goroutine,该goroutine每隔1秒输出"hello golang"

2)在主线程中也每隔一秒输出"hello world",输出10次后,退出程序

3)要求主线程和goroutine同时执行

package main
import ("fmt""strconv""time"
)
func test() {for i := 0; i < 10; i++ {fmt.Println("hello golang + " + strconv.Itoa(i))// 阻塞1秒time.Sleep(1 * time.Second)}
}func main() { // 主线程go test() // 开启协程for i := 0; i < 10; i++ {fmt.Println("hello world + " + strconv.Itoa(i))// 阻塞1秒time.Sleep(1 * time.Second)}
}

开启协程之后,协程和主线程交替执行,效果如下:

主线程和协程执行流程:可以从下图所示看到:

主死协从: 在go语言中有如下概念,如果主线程退出了,则协程即使还没有执行完毕,也会退出,当然协程也可以在主线程没有退出前,就自己结束了,比如完成了自己的任务,这里我们对上面的代码进行一个简单的修改,让协程的任务量变大,如下:

当我们运行程序之后,即使协程没有执行完毕,其仍要跟随主程序的结束而结束:

启动多协程

相比于线程,协程的创建和销毁成本非常低,这里可以在同一时间启动多个协程,它们会并发执行,go运行时会自动调度协程,以便它们可以在多个操作系统线程上运行,协程之间通常通过通道进行通信,以避免共享状态导致的竞态条件。下面是一个简单的go代码示例:

package main
import ("fmt""time"
)
func main() {// 匿名函数+外部变量 = 闭包for i := 0; i <= 5; i++ {// 启动协程,使用匿名函数直接调用匿名按时go func(n int) {fmt.Println(n)}(i)}time.Sleep(time.Second * 2)
}

最终实现的效果如下所示,由于它们都在并发地运行,所以输出的顺序可能是不确定的,如果你想要控制协程的执行顺序或等待它们完成,你可以使用通道或其他同步机制,这个后面再讲:

当然如果不想使用time.Sleep的方式的话,也可以采用下面的方式进行实现:

package main
import ("fmt""sync"
)
var wg sync.WaitGroup // 只定义无需赋值
func main() {// 启动五个协程for i := 0; i < 5; i++ {wg.Add(1) // 计数器加一go func(n int) {fmt.Println(n)wg.Done() // 协程执行完成减1}(i)}// 主线程一直在阻塞,什么时候wg减为0就停止wg.Wait()
}

如果多个协程操作同一个数据的情况下,给出如下代码进行示例:

package main
import ("sync"
)
// 定义一个变量
var totalNum int
var wg sync.WaitGroup // 只定义无需赋值func add() {defer wg.Done()for i := 0; i < 100000000; i++ {totalNum++}
}
func sub() {defer wg.Done()for i := 0; i < 100000000; i++ {totalNum--}
}func main() {wg.Add(2)// 启动两个协程go add()go sub()// 等待协程结束wg.Wait()println(totalNum)
}

因为协程是交替不确定的执行,结果可能都不一样,如下图所示:

初识锁

从上文的案例可以看出,如果多个协程操作同一个数据, 因为协程会交替并发执行,所以会出现争抢资源的情况,导致最终的结果可能并不是我们想要的,那么我们如何处理这个问题呢?这里我们就需要通过一个机制,确保一个协程在执行逻辑的时候另外的协程不执行,这里我们就需要引入一个概念:“锁的机制”,即加入互斥锁,示例代码如下,最终结果为0,是我们想要的:

package main
import ("sync"
)
// 定义一个变量
var totalNum int
var wg sync.WaitGroup // 只定义无需赋值
// 加入互斥锁
var lock sync.Mutexfunc add() {defer wg.Done()for i := 0; i < 100000000; i++ {lock.Lock() // 加锁totalNum++lock.Unlock() // 解锁}
}
func sub() {defer wg.Done()for i := 0; i < 100000000; i++ {lock.Lock() // 加锁totalNum--lock.Unlock() // 解锁}
}func main() {wg.Add(2)// 启动两个协程go add()go sub()// 等待协程结束wg.Wait()println(totalNum)
}

当然golang中sync包实现了两种锁Mutex(互斥锁)和RWMutex(读写锁),具体如下:

1)互斥锁:其中Mutex为互斥锁,Lock(加锁,Unlock()解锁,使用Lock()加锁后,便不能再次对其进行加锁,直到利用Unlock(解锁对其解锁后,才能再次加锁.适用于读写不确定场景,即读写次数没有明显的区别,其性能和效率相对较低。

2)读写锁:RWMutex是一个读写锁,其经常用于读次数远远多于写次数的场景,在读的时候数据之间不产生影响,写和读之间才会产生影响。

package main
import ("fmt""sync""time"
)
// 定义一个变量
var totalNum int
var wg sync.WaitGroup // 只定义无需赋值
// 加入读写锁
var lock sync.RWMutexfunc read() {defer wg.Done()lock.RLock() // 读锁,如果只是读数据那么这个锁不产生任何影响,但是读写同时发生时,就会有影响fmt.Println("开始读取数据")time.Sleep(time.Second)fmt.Println("读取数据完毕")lock.RUnlock() // 关锁
}
func write() {defer wg.Done()lock.Lock() // 写锁fmt.Println("开始写入数据")time.Sleep(time.Second)fmt.Println("写入数据完毕")lock.Unlock() // 关锁
}func main() {wg.Add(6)// 启动两个协程,读多写少for i := 0; i < 5; i++ {go read()}go write()// 等待协程结束wg.Wait()
}

初识管道

在go语言中,管道(Channel)是一种特殊的类型,用于在协程(goroutine)之间进行通信,它允许一个协程将数据发送到管道,并由另一个协程从管道中接收数据。这种机制可以实现协程之间的同步和数据交换,以下是管道相关特点介绍:

1)管道本质就是一个数据结构-队列

2)数据是先进先出

3)自身线程安全,多协程访问时,不需要加锁,channel本身就是线程安全的

4)管道有类型的,一个string的管道只能存放string类型数据

管道的定义: var 变量名chan 数据类型

chan是管道关键字;数据类型指的是管道的类型,里面放入数据的类型,管道是有类型的,intChan只能写入整数int;管道是引用类型,必须初始化才能写入数据,即make后才能使用。

package mainimport "fmt"func main() {// 定义一个int类型管道var intChan chan int// 通过make初始化,管道可以存放3个int类型数据intChan = make(chan int, 3)// 证明管道是引用类型fmt.Printf("intChan的值:%v \n", intChan) // intChan的值:0xc000018200// 向管道存放数据,注意存放的数据不能超出管道的容量intChan <- 1 // 往管道中存放数据num := 20intChan <- num // 往管道中存放数据// 取出管道数据(队列先进先出)fmt.Printf("intChan的值:%v \n", <-intChan) // intChan的值:1fmt.Printf("intChan的值:%v \n", <-intChan) // intChan的值:20// 输出管道的长度fmt.Printf("intChan的长度:%d \n", len(intChan)) // intChan的长度:2
}

管道的关闭使用内置函数close可以关闭管道,当管道关闭后,就不能再向管道写数据了,但是仍然可以从该管道读取数据。

package mainimport "fmt"func main() {// 定义管道var intChan chan int// 通过make初始化管道,可以存放3个int类型数据intChan = make(chan int, 3)// 往管道中写入数据intChan <- 1intChan <- 2// 关闭管道close(intChan)// 再次写入数据会报错//intChan <- 3 // panic: send on closed channel// 当管道关闭后,再读取数据是可以的num := <-intChanfmt.Println(num) // 1
}

管道的遍历:管道支持for-range的方式进行遍历,请注意两个细节:

1)在遍历时,如果管道没有关闭,则会出现deadlock的错误

2)在遍历时,如果管道已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。

package mainimport "fmt"func main() {// 定义管道var intChan chan int// 通过make初始化管道,可以存放100个int类型数据intChan = make(chan int, 100)// 往管道中写入数据for i := 0; i < 100; i++ {intChan <- i}// 在遍历前,如果没有关闭管道,就会出现deedlock的错误// 遍历管道中的数据for v := range intChan {fmt.Println("value = ", v)}
}

如上代码由于没有关闭管道,导致出现如下问题:

所以我们需要在遍历管道前,需要进行管道的关闭,如下:

管道的只读只写: 可以在初始化管道的时候,通过代码设置只读只写属性,如下:

func main() { // 主线程// 默认情况下,管道是双向的,可读可写var intChan chan int// 声明为只写管道var intChan1 chan<- int// 声明为只读管道var intChan2 <-chan int
}

select功能:解决多个管道的选择问题,也可以叫做多路复用,可以从多个管道中随机公平地选择一个来执行,如下代码输出的就是hello

1)case后面必须进行的是io操作,不能是等值,随机去选择一个io操作

2)default防止select被阻塞住,加入default 

package mainimport ("fmt""time"
)func main() { // 主线程// 定义一个int类型管道intChan := make(chan int, 1)go func() {time.Sleep(time.Second * 5)intChan <- 1}()// 定义一个string类型管道stringChan := make(chan string, 1)go func() {time.Sleep(time.Second * 2)stringChan <- "hello"}()//fmt.Println(<-intChan) // 阻塞等待,本身取数据就是阻塞的select {case v := <-intChan:fmt.Println("intChan: ", v)case v := <-stringChan:fmt.Println("stringChan: ", v)default:fmt.Println("防止select被阻塞")}
}

异常错误捕获:多个协程工作,其中一个协程出现panic,导致程序崩溃,这里利用refer+recover捕获panic进行处理,即使协程出现问题,主线程仍然不受影响可以继续执行,示例代码如下:

package mainimport ("fmt""time"
)// 输出数字
func printNum() {for i := 0; i < 10; i++ {fmt.Println(i)}
}// 除法操作
func devide() {defer func() {err := recover()if err != nil {fmt.Println("程序异常退出")}}()num1 := 10num2 := 0result := num1 / num2fmt.Println(result)
}func main() {// 启动两个协程go printNum()go devide()time.Sleep(time.Second * 5)
}

得到的结果如下所示:

协程与管道协同

接下来我们通过一个案例来实现协程与管道的共同操作,案例需求如下:

请完成协程和管道协同工作的案例,具体要求:

1)开启一个writeData协程,向管道中写入50个整数。

2)开启一个readData协程,从管道中读取writeData写入的数据。

3)注意:writeData和readDate操作的是同一个管道。

4)主线程需要等待writeData和readDate协程都完成工作才能退出

其对应的原理图如下所示:

package mainimport ("fmt""strconv""sync""time"
)var wg sync.WaitGroup// 写数据
func writeData(intChan chan int) {defer wg.Done()for i := 0; i < 50; i++ {intChan <- ifmt.Println("写入的数据为:" + strconv.Itoa(i))time.Sleep(time.Second)}// 关闭通道close(intChan)
}// 读数据
func readData(intChan chan int) {defer wg.Done()for i := range intChan {fmt.Println("读取的数据为:" + strconv.Itoa(i))time.Sleep(time.Second)}
}
func main() { // 主线程// 写协程和读协程共同操作同一个通道intChan := make(chan int, 50)wg.Add(2)// 开启读和写的协程go writeData(intChan)go readData(intChan)// 主线程一直在阻塞,什么时候wg.Done()减为0,主线程才会结束wg.Wait()
}

最终实现的效果如下所示:

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

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

相关文章

驱动开发之设备树语法

目录 0.设备树由来 1.设备树概念 1.1.DTS、DTB 和 DTC 和 dtsi 概念 2.设备树语法 2.1.例子 2.2.设备节点 2.2.1.节点命名 2.2.2节点数据类型 2.2.3.根节点 2.2.4.属性介绍 2.2.4.1.compatible属性 2.2.4.2.name属性 2.2.4.3.status 属性 2.2.4.5.unit-address属性…

2024050302-重学 Java 设计模式《实战享元模式》

重学 Java 设计模式&#xff1a;实战享元模式「基于Redis秒杀&#xff0c;提供活动与库存信息查询场景」 一、前言 程序员&#x1f468;‍&#x1f4bb;‍的上下文是什么&#xff1f; 很多时候一大部分编程开发的人员都只是关注于功能的实现&#xff0c;只要自己把这部分需求…

apifox 生成签名

目录 前言准备编写签名脚本签名说明捋清思路编码获取签名所需的参数生成签名将签名放到合适的位置完整代码 在apifox中配置脚本新增公共脚本引用公共脚本添加环境变量 参考 前言 略 准备 查看apifox提供的最佳实践文章&#xff1a;接口签名如何处理 编写签名脚本 签名说明…

【遗传算法】【机器学习】【Python】常见交叉方法(二)、多点交叉和均匀交叉

往期遗传算法文章见&#xff1a; 【遗传算法】【机器学习】【Python】常见交叉方法&#xff08;一&#xff09;、单点交叉和两点交叉 一、遗传算法流程图 交叉过程即存在于上图的”交叉“&#xff08;crossover&#xff09;步骤中。 二、多点交叉 多点交叉的原理就是&#x…

腾讯云centos上安装docker

下面的操作是在root用户下操作的,如果非root用户在命令行前加上sudo 1. 系统及内核查看 操作系统&#xff1a;64位的CentOS 7或更新版本。内核版本&#xff1a;最低要求是3.10&#xff0c;推荐使用3.10或更高版本。 #查看内核版本 (base) [klfwjfweaVM-0-6-centos ~]$ uname…

ARM服务器在云手机中可以提供哪些支持

ARM服务器作为云手机的底层支撑&#xff0c;在很多社媒APP或者电商APP平台都有着很多看不见的功劳&#xff0c;可以说ARM扮演着至关重要的底层支持角色&#xff1b; 首先&#xff0c;ARM 服务器为云手机提供了强大的计算能力基础。云手机需要处理大量的数据和复杂的运算&#x…

uniapp自定义的下面导航

uniapp自定义的下面导航 看看效果图片吧 文章目录 uniapp自定义的下面导航 看看效果图片吧 ![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/6aa0e964741d4dd3a58f4e86c4bf3247.png) 前言一、写组件、我这里就没有写组件了直接写了一个页面&#xff1f;总结 前言 在…

一文掌握Vue3:深度解读Vue3新特性、Vue2与Vue3核心差异以及Vue2到Vue3转型迭代迁移重点梳理与实战

每次技术革新均推动着应用性能与开发体验的提升。Vue3 的迭代进步体现在性能优化、API重构与增强型TypeScript支持等方面&#xff0c;从而实现更高效开发、更优运行表现&#xff0c;促使升级成为保持竞争力与跟进现代前端趋势的必然选择。本文深度解读Vue3 响应式数据data、生命…

Java Web学习笔记27——对话框、表单组件

常见组件对话框&#xff1a; Dialog对话框&#xff1a;在保留当前页面状态下&#xff0c;告知用户并承载相关操作。 dialogTableVisible: false 默认是不可见的。 在按钮属性中设置为true的意思&#xff0c;点击按钮的时候&#xff0c;才会true&#xff0c;对话框才会显示。 …

idm2024最新完美破解版免费下载 idm绿色直装版注册机免费分享 idm永久激活码工具

IDM 2024破解版重新开发了调度程序和MMS协议支持、重新设计和增强的下载引擎、与所有最新浏览器的独特高级集成、改进的工具栏以及大量其他改进和新功能&#xff0c;这一全新的更新&#xff0c;使得IDM下载器更加完美。值得一提的是&#xff0c;它可以借助油猴浏览器的脚本&…

Linux编译器-gcc或g++的使用

一.安装gcc/g 在linux中是不会自带gcc/g的&#xff0c;我们需要编译程序就自己需要安装gcc/g。 很简单我们使用简单的命令安装gcc&#xff1a;sudo yum install -y gcc。 g安装&#xff1a;sudo yum install -y gcc-c。 我们知道Windows上区分文件&#xff0c;都是使用文件…

ssm610学生社团管理系统+vue【已测试】

前言&#xff1a;&#x1f469;‍&#x1f4bb; 计算机行业的同仁们&#xff0c;大家好&#xff01;作为专注于Java领域多年的开发者&#xff0c;我非常理解实践案例的重要性。以下是一些我认为有助于提升你们技能的资源&#xff1a; &#x1f469;‍&#x1f4bb; SpringBoot…

小熊家务帮day15-day17 预约下单模块(预约下单,熔断降级,支付功能,退款功能)

目录 1 预约下单1.1 需求分析1.1.1 业务流程1.1.2 订单状态 1.2 系统设计1.2.1 订单表设计1.2.2 表结构的设置 1.3 开发远程调用接口1.3.0 复习下远程调用的开发1.3.1 查询地址簿远程接口jzo2o-api工程定义接口Customer服务实现接口 1.3.2 查询服务&服务项远程接口jzo2o-ap…

运维 之 DNS域名解析

前言 我们每天打开的网站&#xff0c;他是如何来解析&#xff0c;并且我们怎么能得到网站的内容反馈的界面呢&#xff1f;那什么是DNS呢&#xff08;DNS&#xff08;DomainNameservice&#xff0c;域名服务&#xff0c;主要用于因特网上作为域名和IP地址相互映射&#xff09;那…

【iOS】MRC下的单例模式批量创建单例

单例模式的介绍和ARC下的单例请见这篇&#xff1a;【iOS】单例模式 目录 关闭ARC环境MRC下的单例ARC下的单例批量创建单例Demo 关闭ARC环境 首先关闭ARC环境&#xff0c;即打开MRC&#xff1a; 或是指定某特定目标文件为非ARC环境&#xff1a; 双击某个类文件&#xff0c;指定…

SpringBoot2+Vue3开发课程审核流程系统

SpringBoot2Vue3开发课程审核流程系统 简介 此系统实现了课程审核全流程功能并使用了Activiti7工作流技术&#xff0c;功能包含&#xff1a;课程管理、用户管理、流程定义、课程审核&#xff08;我的申请、我的代办、我的已办&#xff09; 功能介绍 课程管理 对课程信息的管…

C++的STL 中 set.map multiset.multimap 学习使用详细讲解(含配套OJ题练习使用详细解答)

目录 一、set 1.set的介绍 2.set的使用 2.1 set的模板参数列表 2.2 set的构造 2.3 set的迭代器 2.4 set的容量 2.5 set的修改操作 2.6 set的使用举例 二、map 1.map的介绍 2.map的使用 2.1 map的模板参数说明 2.2 map的构造 2.3 map的迭代器 2.4 map的容量与元…

API测试工具

apifox 微信扫描登录 不推荐&#xff1a; Download Postman

QT creator c动态链接库的创建与调用

QT creator c动态链接库的创建与调用 QT5.15.2 1.创建dll项目 确保两类型选择正确 2.选择MinGW 64-bit 3.点击完成 pro文件参考&#xff1a; QT - guiTEMPLATE lib DEFINES QT_DLL_DEMO_LIBRARYCONFIG c17# You can make your code fail to compile if it uses deprecat…

Flutter 使用ffigen生成ffmpeg的dart接口

Flutter视频渲染系列 第一章 Android使用Texture渲染视频 第二章 Windows使用Texture渲染视频 第三章 Linux使用Texture渲染视频 第四章 全平台FFICustomPainter渲染视频 第五章 Windows使用Native窗口渲染视频 第六章 桌面端使用texture_rgba_renderer渲染视频 第七章 使用ff…