Golang--协程和管道

1、概念

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

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

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

协程:

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

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

对于单线程下,我们不可避免程序中出现io操作,但如果我们能在自己的程序中(即用户程序级别,而非操作系统级别)控制单线程下的多个任务能在一个任务遇到io阻塞时就将寄存器上下文和栈保存到某个其他地方,然后切换到另外一个任务去计算。在任务切回来的时候,恢复先前保存的寄存器上下文和栈,这样就保证了该线程能够最大限度地处于就绪态,即随时都可以被cpu执行的状态,相当于我们在用户程序级别将自己的io操作最大限度地隐藏起来,从而可以迷惑操作系统,让其看到:该线程好像是一直在计算,io比较少,从而会更多得将cpu执行权限分配给我们的线程(线程是cpu控制的,而协程是程序自身控制的,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级

2、启动一个协程 

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

  1. 在主线程中,开启一个goroutine(协程),该goroutine每隔1秒输出"hello golang"
  2. 在主线程中也每隔一秒输出"hello world",输出10次后退出程序
  3. 要求主线程和goroutine同时执行
package main
import ("fmt""strconv""time"
)func test(){for i := 1; i <= 10; i++{fmt.Println("hello golang" + strconv.Itoa(i))//阻塞一秒time.Sleep(time.Second * 1)// 1s}
}func main(){   //主线程//开启一个协程go test()for i := 1; i <= 10; i++{fmt.Println("hello world" + strconv.Itoa(i))//阻塞一秒time.Sleep(time.Second * 1)// 1s}
}

开启协程:使用go关键字,例如:go test()

执行流程:
注意:如果主线程退出,协程还没执行结束,协程也会提前结束(主死从随)

3、启动多个协程

package main
import ("fmt""strconv""time"
)func test1(){for i := 1; i <= 10; i++{fmt.Println("hello golang" + strconv.Itoa(i))//阻塞一秒time.Sleep(time.Second * 1)// 1s}
}func main(){   //主线程//开启一个协程--使用匿名函数//匿名函数 + 外部变量 = 闭包for i := 1; i <= 5; i++{go func(n int){fmt.Println(n)}(i)}//开启一个协程--使用普通函数go test1()for i := 1; i <= 10; i++{fmt.Println("hello world" + strconv.Itoa(i))//阻塞一秒time.Sleep(time.Second * 1)// 1s}
}

4、使用WaitGroup控制协程退出

WaitGroup用于等待一组线程的结束。父线程调用Add方法来设定应等待的线程的数量,每个被等待的线程在结束时应调用Done方法。同时主线程里可以调用Wait方法阻塞至所有线程结束。用于解决主线程在子协程结束后自动结束,防止主线程退出导致协程被迫退出,


package main
import ("fmt""sync" // 并发包"time""strconv"
)var wg sync.WaitGroup // 并发包的变量, 只定义无需初始化--类似计数器func test1(){//协程执行完毕,协程数量-1defer wg.Done()for i := 1; i <= 10; i++{fmt.Println("hello golang" + strconv.Itoa(i))//阻塞一秒time.Sleep(time.Second * 2)// 2s}// //协程执行完毕,协程数量-1// wg.Done()
}func main(){   //主线程//开启一个协程--使用普通函数wg.Add(1)  // 协程数量+1go test1() // 开启协程//主线程一直阻塞,等待协程执行完毕--直到协程执行完毕才会继续执行主线程wg.Wait()for i := 1; i <= 10; i++{fmt.Println("hello world" + strconv.Itoa(i))//阻塞一秒time.Sleep(time.Second * 1)// 1s}
}

5、多个协程操纵同一数据案例(使用互斥锁同步协程

错误案例:

package main
import ("fmt""sync" // 并发包
)var wg sync.WaitGroup // 并发包的变量, 只定义无需初始化//定义一个变量:
var cnt intfunc iAdd(){defer wg.Done()for i := 1; i <= 10000; i++{cnt = cnt + 1}
}
func iSub(){defer wg.Done()for i := 1; i <= 10000; i++{cnt = cnt - 1}
}func main(){   //主线程//开启一个协程--使用普通函数wg.Add(2)  // 协程数量+2go iAdd() // 开启协程go iSub()wg.Wait()fmt.Println(cnt) //结果:理论上cnt是0,但实际上不是
}

两个协程进行的顺序是不一定的,比如取cnt这个值时,两个协程进行的取值和运算操作顺序的关系,可能导致对原本正确的结果进行了覆盖,导致iAdd函数的偏移量和iSub函数的偏移量之和不等于0,实际结果是个不确定的值。

5.1 互斥锁 

正确案例:使用互斥锁同步协程

解决上面问题的方案:
确保一个协程在执行逻辑的时候另外的协程不执行---->利用锁的机制---->互斥锁

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

package main
import ("fmt""sync" // 并发包
)var wg sync.WaitGroup // 并发包的变量, 只定义无需初始化
var lock sync.Mutex //加入互斥锁//定义一个变量:
var cnt intfunc iAdd(){defer wg.Done()for i := 1; i <= 10000; i++{lock.Lock() // 加锁cnt = cnt + 1lock.Unlock() // 解锁}
}
func iSub(){defer wg.Done()for i := 1; i <= 10000; i++{lock.Lock() // 加锁cnt = cnt - 1lock.Unlock() // 解锁}
}func main(){   //主线程wg.Add(2) go iAdd()go iSub()wg.Wait()fmt.Println(cnt) //结果:理论上cnt是0,但实际上不是
}


5.2 读写锁 

读写锁:

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

package main
import ("fmt""sync" // 并发包"time"
)var wg sync.WaitGroup // 并发包的变量, 只定义无需初始化
var lock sync.RWMutex //加入读写锁func 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 * 2)fmt.Println("修改数据成功!")lock.Unlock()
}func main(){   //主线程//场景:读多写少wg.Add(1)go write()for i := 1; i < 20; i++{wg.Add(1)go read()}wg.Wait()
}


的过程中,锁生效,但是读可以并发读,锁没有影响

6、管道

6.1 管道概念

管道:

  • 管道本质就是一个数据结构--队列
  • 数据特性:先进先出
  • 自身线程安全,多协程访问时,不需要加锁,channel本身就是线程安全的
  • 管道有类型的,如:一个string类型的管道只能存放string类型数据

6.2 管道的定义

定义:

var 变量名 chan 数据类型

  • chan是管道关键字
  • 数据类型指的是管道的类型,指里面放入数据的类型,int类型的管道只能写入整数int
  • 管道是引用类型,必须初始化才能写入数据,即make后才能使用
package main
import ("fmt"
)func main(){//定义管道、声明管道 ---> 定义一个int类型的管道var intChan chan int//通过make初始化:管道可以存放3个int类型的数据intChan = make(chan int,3)//证明管道是引用类型fmt.Printf("intChan的值:%v\n",intChan) //intChan的值:0xc0000ac080//向管道中写入数据intChan <- 10intChan <- 20//输出管道的长度fmt.Printf("管道的实际长度:%v,管道的容量:%v\n",len(intChan),cap(intChan)) // 管道的实际长度:2,管道的容量:3//从管道中读取数据num := <-intChanfmt.Println(num) // 10//关闭管道---> 不能向关闭的管道中写入数据close(intChan)// intChan <- 100 // 报错:panic: send on closed channelnum,flag := <-intChanfmt.Println(num,flag) // 20 true
}

6.3 管道的关闭

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


例子:上面代码的

6.4 管道的遍历

管道的遍历:

管道支持for-range的方式进行遍历,注意:

  • 在遍历时,如果管道没有关闭,则会出现deadlock(死锁)的错误
  • 在遍历时,如果管道已经关闭,则会正常遍历数据,遍历完后,就会退出遍历 
package main
import ("fmt"
)func main(){//定义管道、声明管道 ---> 定义一个int类型的管道var intChan chan int = make(chan int,100)for i := 1; i <= 100; i++{intChan <- i}//遍历前,如果没有关闭管道,那么会出现死锁 -- deadlockclose(intChan)//for-range读取管道中的数据for v := range intChan{fmt.Printf("%v ",v)}
}

6.5 协程和管道协同工作案例

案例需求:

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

  1. 开启一个writeData协程,向管道中写入50个整数
  2. 开启一个readData协程,从管道中读取writeData写入的数据
  3. 注意:writeData和readData操作的是同一个管道
  4. 主线程需要等待writeData和readData协程都完成工作才能退出
package main
import ("fmt""sync""time"
)var wg sync.WaitGroup // 并发包的变量, 只定义无需初始化func writeData(intChan chan int){defer wg.Done() // 协程执行完毕,协程数量-1for i := 1; i <= 50; i++{fmt.Printf("写入数据:%v\n",i)intChan<- i		time.Sleep(time.Second * 1)}close(intChan)
}func readData(intChan chan int){defer wg.Done() // 协程执行完毕,协程数量-1for v := range intChan{fmt.Printf("读取数据:%v\n",v)time.Sleep(time.Second * 1)}
}func main(){//initvar intChan chan int = make(chan int,100)//开启线程wg.Add(2)go writeData(intChan)go readData(intChan)wg.Wait()
}

6.6 声明只读只写管道

管道可以声明为只读或者只写性质:

package mainimport ("fmt"
)func main(){//默认情况下,管道是双向的 --> 可读可写var intChan1 chan int = make(chan int, 10)intChan1 <- 10fmt.Println(intChan1)//声明为只写 --> 只能写,不能读var intChan2 chan <- int = make(chan int, 5)intChan2 <- 10fmt.Println(intChan2)//fmt.Println(<- intChan2) // 报错:invalid operation: <-intChan2 (receive from send-only type chan<- int)//声明为只读--> 只能读,不能写var intChan3 <- chan int = make(chan int, 5)fmt.Println(intChan3)//intChan3 <- 10 // 报错:invalid operation: intChan3 <- 10 (send to receive-only type <-chan int)
}

6.7 管道的阻塞

阻塞的情况:

package main
import ("fmt""sync"//"time"
)var wg sync.WaitGroup // 并发包的变量, 只定义无需初始化func writeData(intChan chan int){defer wg.Done() // 协程执行完毕,协程数量-1for i := 1; i <= 10; i++{fmt.Printf("写入数据:%v\n",i)intChan<- i		//time.Sleep(time.Second * 1)}close(intChan)
}func readData(intChan chan int){defer wg.Done() // 协程执行完毕,协程数量-1for v := range intChan{fmt.Printf("读取数据:%v\n",v)//time.Sleep(time.Second * 1)}
}func main(){//initvar intChan chan int = make(chan int,5)//开启线程wg.Add(1)go writeData(intChan)//go readData(intChan)wg.Wait()
}
  • 在Go语言中,如果一个管道(channel)只被写入数据而没有被读取,那么最终会导致管道阻塞。这是因为Go语言中的管道是同步的,即写入操作会等待读取操作完成后才能继续如果没有读取操作来接收数据,写入操作就会一直等待,从而导致程序阻塞
  • 在代码片段中,如果writeData函数一直向intChan管道写入数据,而没有readData函数来读取这些数据,那么writeData函数中的intChan<- i操作会在管道缓冲区满后阻塞。如果缓冲区大小是5,那么在写入5个数据后,writeData函数就会阻塞,直到有读取操作来接收数据。
  • 如果程序中没有其他地方读取这个管道,那么writeData函数会一直阻塞,这可能会导致程序死锁。为了避免这种情况,通常应该确保在创建管道时,有相应的读取操作来处理写入的数据。
  • 在实际编程中,应该根据程序的逻辑和需求来设计管道的读写操作,确保数据能够被正确处理,避免出现阻塞或死锁的情况。如果确实需要一个只写而不读的管道,那么应该考虑使用其他机制来处理数据,或者在程序中添加适当的逻辑来处理这种情况。

确保有相应的读取操作(不管读写速度是否一致(例如读的速度小于写的速度),只要有读取操作就不会阻塞):

package main
import ("fmt""sync""time"
)var wg sync.WaitGroup // 并发包的变量, 只定义无需初始化func writeData(intChan chan int){defer wg.Done() // 协程执行完毕,协程数量-1for i := 1; i <= 10; i++{fmt.Printf("写入数据:%v\n",i)intChan<- i		time.Sleep(time.Second * 1)}close(intChan)
}func readData(intChan chan int){defer wg.Done() // 协程执行完毕,协程数量-1for v := range intChan{fmt.Printf("读取数据:%v\n",v)time.Sleep(time.Second * 5)}
}func main(){//initvar intChan chan int = make(chan int,5)//开启线程wg.Add(2)go writeData(intChan)go readData(intChan)wg.Wait()
}

6.8 select功能

select功能:在Go语言中,select语句用于处理多个通道(channel)的操作。它类似于switch语句,但是专门用于处理通道的发送和接收操作select语句会监听多个通道的操作,一旦其中一个通道准备好进行发送或接收操作,select就会执行相应的分支。

在这个语法中,每个case分支对应一个通道操作。如果多个case分支同时满足条件,Go语言会随机选择一个分支执行如果没有任何分支满足条件,并且存在default分支,那么就会执行default分支。如果没有default分支,select语句会阻塞,直到有某个分支满足条件
 

  • case后面必须进行的是io操作,不能是等值,随机去选择一个io操作
  • default防止select被阻塞住,加入default
package main
import ("fmt""time"
)func main(){//chan intvar intChan chan int = make(chan int, 5)go func(){time.Sleep(time.Second * 4)intChan <- 10}()//chan stringvar strChan chan string = make(chan string, 5)go func(){time.Sleep(time.Second * 2)strChan <- "hello world"}()//chan float32var floChan chan float32 = make(chan float32, 5)go func(){time.Sleep(time.Second * 1)floChan <- 1.1111}()//select--> 多路复用 --> 哪个管道有数据就执行哪个管道select{case v := <- intChan:fmt.Printf("select:读取intChan数据:%v\n",v)case v := <- strChan:fmt.Printf("select:读取strChan数据:%v\n",v)case <- floChan:fmt.Printf("floChan管道") //执行// default:// 	fmt.Println("防止select阻塞\n")}
}

6.9  defer + recover机制处理错误

问题原因:多个协程工作,其中一个协程出现panic,导致程序奔溃

解决方法:利用defer + recover捕获panic进行处理,即使协程出现问题,主线程仍然不受影响可以继续执行

package main
import ("fmt""time"
)//输出数字
func printNum(){for i := 1; i <= 10; i++{fmt.Println(i)//time.Sleep(time.Second * 1)}
}//除法
func devide(){defer func(){err := recover()if err != nil{fmt.Println("devide()发生错误:",err)}}()num1 := 100000num2 := 0result := num1/num2fmt.Println(result)
}func main(){go printNum()go devide()time.Sleep(time.Second * 5)
}

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

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

相关文章

动力商城-02 环境搭建

1.父工程必须满足&#xff1a;1.1删除src目录 1.2pom 2.依赖继承 //里面的依赖&#xff0c;后代无条件继承<dependencies></dependencies>//里面的依赖&#xff0c;后代想要继承&#xff0c;得自己声明需要使用&#xff0c;可以不写版本号&#xff0c;自动继承&l…

JavaWeb开发9

ResponseBody 类型&#xff1a;方法注解、类注解 位置&#xff1a;Controller方法上/类上 作用&#xff1a;将方法返回值直接响应&#xff0c;如果返回值类型是实体对象/集合&#xff0c;将会转换为JSON格式响应 说明&#xff1a;RestControllerControllerResponseBody; 统…

夜天之书 #103 开源嘉年华纪实

上周在北京参与了开源社主办的 2024 中国开源年会。其实相比于有点明显班味的“年会”&#xff0c;我的参会体验更像是经历了一场中国开源的年度嘉年华。这也是在会场和其他参会朋友交流时共同的体验&#xff1a;在开源社的 COSCon 活动上&#xff0c;能够最大限度地一次性见到…

06 Oracle性能优化秘籍:AWR、ASH、SQL trace与实时监控的实战指南

文章目录 Oracle性能优化秘籍&#xff1a;AWR、ASH、SQL trace与实时监控的实战指南一、AWR&#xff08;Automatic Workload Repository&#xff09;1.1 理论部分1.2 实践部分1.2.1 使用方式1.2.2 分析方式 二、ASH&#xff08;Active Session History&#xff09;2.1 理论部分…

JS实现,防抖节流 + 闭包

防抖&#xff08;Debounce&#xff09; 防抖是指短时间内大量触发同一事件&#xff0c;只会在最后一次事件完成后延迟执行一次函数。 防抖的典型应用场景是输入框的搜索建议功能&#xff0c;用户输入时不需要每次输入都去查询&#xff0c;而是在用户停止输入一段时间后才进行…

1.每日SQL----2024/11/7

题目&#xff1a; 计算用户次日留存率,即用户第二天继续登录的概率 表&#xff1a; iddevice_iddate121382024-05-03232142024-05-09332142024-06-15465432024-08-13523152024-08-13623152024-08-14723152024-08-15832142024-05-09932142024-08-151065432024-08-131123152024-…

WPF中如何简单的使用MvvmLight创建一个项目并进行 增删改查

目录 第一步&#xff1a;创建项目后下载如下两个NuGet程序包&#xff0c;然后删除删掉using Microsoft.Practices.ServiceLocation; 并且引入using CommonServiceLocator; 第二步&#xff1a;删除原来的XAML文件并创建如下的包结构然后创建一个在View文件夹中创建一个Main窗体 …

网页版五子棋——匹配模块(客户端开发)

前一篇文章&#xff1a;网页版五子棋——用户模块&#xff08;客户端开发&#xff09;-CSDN博客 目录 前言 一、前后端交互接口设计 二、游戏大厅页面 1.页面代码编写 2.前后端交互代码编写 3.测试获取用户信息功能 结尾 前言 前面文章介绍完了五子棋项目用户模块的代码…

elasticSearch 7.12.1 Docker 安装ik分词

一、下载 https://github.com/infinilabs/analysis-ik/releases/tag/v7.12.1 将文件解压&#xff0c;复制到docker挂载的目录 docker ps#重启docker docker restart f7ec58e91f1f 测试 GET _analyze?pretty {"analyzer": "ik_max_word","text&qu…

在JS中, 0 == [0] 吗

在不知道答案的情况下, 你觉得这段代码的输出是什么 我当时觉得是false, 结果我错了–^^– 那为什么输出是true呢 因为的隐式类型转换, 运算符会尝试将两个操作数转换为相同的类型&#xff0c;然后再进行比较。 在这个例子中&#xff0c;0 是一个数字&#xff0c;而 [0] 是…

【学习AI-相关路程-mnist手写数字分类-win-硬件:windows-自我学习AI-实验步骤-全连接神经网络(BPnetwork)-操作流程(3) 】

【学习AI-相关路程-mnist手写数字分类-win-硬件&#xff1a;windows-自我学习AI-实验步骤-全连接神经网络&#xff08;BPnetwork&#xff09;-操作流程&#xff08;3&#xff09; 】 1、前言2、前置学习&#xff08;1&#xff09;window和Linux中python寻找目录的方式。&#x…

RabbitMQ客户端应用开发实战

这一章节我们将快速完成RabbitMQ客户端基础功能的开发实战。 一、回顾RabbitMQ基础概念 这个RabbitMQ的核心组件&#xff0c;是进行应用开发的基础。 二、RabbitMQ基础编程模型 RabbitMQ提供了很多种主流编程语言的客户端支持。这里我们只分析Java语言的客户端。 上一章节提…

一文了解Android SELinux

在Android系统中&#xff0c;SELinux&#xff08;Security-Enhanced Linux&#xff09;是一个增强的安全机制&#xff0c;用于对系统进行强制访问控制&#xff08;Mandatory Access Control&#xff0c;MAC&#xff09;。它限制了应用程序和进程的访问权限&#xff0c;提供了更…

python画图|hist()函数深层体验

【1】引言 前述学习已经掌握hist()函数的基本运用技巧&#xff0c;可通过下述链接直达&#xff1a; python画图|hist()函数画直方图初探-CSDN博客 python画图|hist()函数画直方图进阶-CSDN博客 我们已经理解hist()函数本质上画的是概率分布图&#xff0c;相关知识属于数理统…

火狐浏览器同源策略禁止解决方案

前言 火狐浏览器同源策略禁止解决方案_同源策略禁止读取远程资源怎么办-CSDN博客 在使用Firefox火狐浏览器进行Web开发时&#xff0c;有时会遇到因为同源策略&#xff08;Same-Origin Policy&#xff09;导致的跨域请求被拦截的问题。例如&#xff0c;控制台可能会显示如下错…

计算机网络——TCP篇

TCP篇 基本认知 TCP和UDP的区别? TCP 和 UDP 可以使用同一个端口吗&#xff1f; 可以的 传输层中 TCP 和 UDP在内核中是两个完全独立的软件模块。可以根据协议字段来选择不同的模块来处理。 TCP 连接建立 TCP 三次握手过程是怎样的&#xff1f; 一次握手:客户端发送带有 …

解决ImportError: DLL load failed while importing _message: 找不到指定的程序。

C:\software\Anoconda\envs\yolov5_train\python.exe C:\Project\13_yolov5-master\train.py C:\software\Anoconda\envs\yolov5_train\lib\site-packages\torchvision\io\image.py:13: UserWarning: Failed to load image Python extension: [WinError 127] 找不到指定的程序…

AOSP沙盒android 11

这里介绍一下aosp装系统 什么是aosp AOSP&#xff08;Android Open Source Project&#xff09;是Android操作系统的开源版本。 它由Google主导&#xff0c;提供了Android的源代码和相关工具&#xff0c;供开发者使用和修改。 AOSP包含了Android的核心组件和API&#xff0c;使…

git提交冲突的原因及解决方案

一、场景一 1.冲突原因 提交者的版本库 < 远程库 要保障提交者的版本库信息和远程仓库是一致的 2.解决方案 实现本地同步git pull,再提交代码&#xff08;最好每次git push之前都git pull一下&#xff0c;防止这种情况的出现&#xff09; 场景二 1.冲突原因 别人跟你…

第十五届蓝桥杯C/C++B组题解——数字接龙

题目描述 小蓝最近迷上了一款名为《数字接龙》的迷宫游戏&#xff0c;游戏在一个大小为N N 的格子棋盘上展开&#xff0c;其中每一个格子处都有着一个 0 . . . K − 1 之间的整数。游戏规则如下&#xff1a; 从左上角 (0, 0) 处出发&#xff0c;目标是到达右下角 (N − 1, N …