Golang 进阶3—— 协程管道

ops/2024/10/10 23:50:13/

Golang 进阶3—— 协程&管道

注意,该文档只适合有编程基础的同学,这里的go教程只给出有区别的知识点

注意 程序 & 进程 & 协程区别

1. 协程

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

作用: 在执行 A 函数的时候, 可以随时中断, 去执行B函数,然后中断继续执行A函数(可以自由切换), 注意这一切换并不是函数调用(没有调用语句), 过程很像多线程,然而协程中只有一个线程在执行 (协程的本质是个单线程)。线程是CPU控制的,而协程是程序自身控制的,属于程序级别的切换,操作系统完全感知不到,从而更加轻量级。

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

1.1 main文件
import ("fmt""time""strconv"
)func test () {for i := 1; i <= 10; i++ {fmt.Println("test, golang", strconv.Itoa(i))time.Sleep(time.Second)}
}
func main() {// 主线程go test() // 启动一个协程, 在语句前面加上 gofor i := 1; i <= 9; i++ {fmt.Println("main, golang", strconv.Itoa(i))// 阻塞 1 秒time.Sleep(time.Second)}
}
1.2 输出结果
(base) PS E:\Goproject\src\gocode\testproject01> go run .\main\main.go
main, golang 1
test, golang 1
test, golang 2
main, golang 2
main, golang 3
test, golang 3
test, golang 4
main, golang 4
main, golang 5
test, golang 5
test, golang 6
main, golang 6
main, golang 7
test, golang 7
test, golang 8
main, golang 8
main, golang 9
test, golang 9
// 从结果可以看出,当主线程死掉之后,协程会直接死掉,不会继续执行。(本来还要继续输出: test, golang 10)—— 主死从随// 不加 go 的情况
(base) PS E:\Goproject\src\gocode\testproject01> go run .\main\main.go
test, golang 1
test, golang 2
test, golang 3
test, golang 4
test, golang 5
test, golang 6
test, golang 7
test, golang 8
test, golang 9
test, golang 10
main, golang 1
main, golang 2
main, golang 3
main, golang 4
main, golang 5
main, golang 6
main, golang 7
main, golang 8
main, golang 9
1.3 启动多个协程
import ("fmt""time"
)func main() {// 主线程for i := 0; i < 10; i++ {go func (i int) {fmt.Println(i)}(i)}time.Sleep(time.Second * 2)
}
1.4 输出结果
(base) PS E:\Goproject\src\gocode\testproject01> go run .\main\main.go
0
1
9
5
6
3
7
4
8
2
2. WaitGroup

WaitGroup 用于等待一组线程结束的结束。 父线程调用 Add 方法来设定应等待的线程的数量。每个被等待的线程在结束时应调用Done方法。同时,主线程里可以调用Wait方法阻塞至所有线程结束。 ——> 解决主线程在子协程结束之后自动结束。(详细可以查看api中的sync 包)

2.1 main文件
import ("fmt""sync"
)var wg sync.WaitGroup
func main() {// 主线程// 启动 5 个协程for i := 0; i < 5; i++ {wg.Add(1) // 计数器加一go func (i int) {fmt.Println(i)defer wg.Done() // 协程执行完毕,计数器减一}(i)}wg.Wait() // 等待协程执行结束, 当计数器达到 0 的时候才结束
}
2.2 输出结果
(base) PS E:\Goproject\src\gocode\testproject01> go run .\main\main.go
0
1
3
4
2 
2.3 多线程对同一个数进行操作 —— 要加锁 (互斥锁)
// 竞争条件:sum 变量被多个 goroutine 并发访问,而没有适当的同步机制(如互斥锁)。这会导致数据的竞争条件,使得 sum 的值可能计算不正确。
package mainimport ("fmt""sync"
)var wg sync.WaitGroup
var mu sync.Mutex // 互斥锁
var sum int 
func add () {for i := 0; i < 1000000; i++ {wg.Add(1)go func (i int) {mu.Lock() // 加锁sum += imu.Unlock() // 解锁defer wg.Done()}(i)}
}func sub () {for i := 0; i < 1000000; i++ {wg.Add(1)go func (i int) {mu.Lock() // 加锁sum -= imu.Unlock() // 解锁defer wg.Done()}(i)}
}
func main() {// 主线程add()sub()wg.Wait() // 等待协程执行结束, 当计数器达到 0 的时候才结束fmt.Println(sum)
}
2.4 输出结果
(base) PS E:\Goproject\src\gocode\testproject01> go run .\main\main.go
0
2.5 另外一种锁

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

package mainimport ("fmt""sync""time"
)var wg sync.WaitGroup
var lock sync.RWMutex // 读写锁func read () {wg.Add(1)defer wg.Done()lock.RLock() // 读锁fmt.Println("read")time.Sleep(time.Second)lock.RUnlock() // 解锁
}func write () { wg.Add(1)defer wg.Done()lock.Lock() // 写锁fmt.Println("write")time.Sleep(time.Second * 3)fmt.Println("write over")defer lock.Unlock() // 解锁}
func main() {// 主线程for i := 0; i < 10; i++ {go read()}go write()wg.Wait() // 等待协程执行结束, 当计数器达到 0 的时候才结束
}
2.6 输出结果
(base) PS E:\Goproject\src\gocode\testproject01> go run .\main\main.go
read
read
write
write over
read
read
read
read
read
read
read
read
3. 管道

管道的本质就是一个数据结构——队列(先进先出)。 自身线程安全,不需要加锁,channel本身就是线程安全的,多个协程操作同一个管道时候,不会发生资源争抢问题。管道有类型,一个string的管道只能存放string类型的数据

3.1 main文件
import ("fmt"
)/*管道chan管道关键字数据类型是指管道的类型,里面放入数据的类型, 管道是有类型的, int类型的管道只能写入int管道是引用类型,必须初始化才能写入数据,即make之后才能使用
*/
func main() { // 主线程// 定义管道var inChan chan int// 通过make初始化, 管道可以存放3个int类型的变量inChan = make(chan int, 3)// 证明是引用类型fmt.Println(inChan) // 输出管道地址// 存放数据inChan <- 1inChan <- 2inChan <- 3// 超过定义的长度,会报错 fatal error: all goroutines are asleep - deadlock!// inChan <- 4fmt.Println("管道的长度:", len(inChan))// 取数据num1 := <-inChanfmt.Println(num1)num2 := <-inChanfmt.Println(num2)num3 := <-inChanfmt.Println(num3)// 关闭管道, 关闭之后,还可以读数据,但是写不了数据了close(inChan)fmt.Println("--------------------------------------------------")// 管道遍历for v := range inChan { // 没有索引fmt.Println("value = ", v)}// 重新初始化管道inChan = make(chan int, 3)inChan <- 4inChan <- 5inChan <- 6close(inChan)// 管道遍历 在遍历前如果没有加入 close(inChan) 会报错, fatal error: all goroutines are asleep - deadlock!// 所以在遍历前要把管道关闭for v := range inChan { // 没有索引fmt.Println("value = ", v)}}
3.2 输出结果
(base) PS E:\Goproject\src\gocode\testproject01> go run .\main\main.go
0xc000080100
管道的长度: 3
1
2
3
--------------------------------------------------
value =  4
value =  5
value =  6
3.3 协程 + 管道
import ("fmt""time""sync"
)var wg sync.WaitGroup// 写入数据
func writeFunc (ch chan int) {defer wg.Done()for i := 1; i <= 50; i++ {fmt.Println("write:", i)ch <- itime.Sleep(time.Second)}close(ch)
}// 读数据func readFunc (ch chan int) {defer wg.Done()for n := range ch {fmt.Println("read data:", n)time.Sleep(time.Second)}
}func main() { // 主线程fmt.Println("rw")wg.Add(2)ch := make(chan int, 50) // 创建管道,容量为50go writeFunc(ch)go readFunc(ch)wg.Wait()	
}
3.4 输出结果
(base) PS E:\Goproject\src\gocode\testproject01> go run .\main\main.go
rw
write: 1
read data: 1
write: 2
read data: 2
write: 3
read data: 3
write: 4
read data: 4
write: 5
read data: 5
write: 6
read data: 6
write: 7
read data: 7
...
3.5 声明只读只写管道
func main() { // 主线程//ch := make(chan int, 10) // 默认情况下,管道可读可写// 声明只写// ch2 := make(chan<- int, 10)// ch2 <- 1// num1 := <-ch2// fmt.Println(num1) // invalid operation: cannot receive from send-only channel ch2 (variable of type chan<- int)// 声明只读ch3 := make(chan int, 10)ch3 <- 1ch3 <- 2close(ch3)if ch3 == nil {fmt.Println("ch3 is nil")} else {num2 := <-ch3fmt.Println(num2)}}
3.6 输出结果
(base) PS E:\Goproject\src\gocode\testproject01> go run .\main\main.go
1
3.7 管道阻塞

当管道只写入数据,没有读出数据就会进入阻塞状态

var wg sync.WaitGroup
func main() { // 主线程ch := make(chan int, 10) // 默认情况下,管道可读可写//  值写入不读 fatal error: all goroutines are asleep - deadlock!for i := 1; i <= 10; i++ {wg.Add(1)defer wg.Done()go func(i int) {ch <- i}(i)}wg.Wait()fmt.Println("over")
}
3.8 输出结果
(base) PS E:\Goproject\src\gocode\testproject01> go run .\main\main.go
fatal error: all goroutines are asleep - deadlock!goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc000008108?)D:/Go/src/runtime/sema.go:71 +0x25
sync.(*WaitGroup).Wait(0xc0000281c0?)D:/Go/src/sync/waitgroup.go:118 +0x48
main.main()E:/Goproject/src/gocode/testproject01/main/main.go:20 +0x12e
exit status 2
4. select

功能: 解决多个管道的选择问题, 也可以叫做多路复用,可以从多个管道中随机公平地选择一个来执行;

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

default 防止select被阻塞, 加入default

4.1 main
import ("fmt""time"
)func main() { // 主线程intCh := make(chan int, 10) stringCh := make(chan string, 10)go func () {time.Sleep(time.Second * 2)intCh <- 1}()go func () {time.Sleep(time.Second * 3)stringCh <- "hello"}()select {case data := <-intCh:fmt.Println("intChan :", data)case data := <-stringCh:fmt.Println("stringChan :", data)default:fmt.Println("防止select被阻塞")}}
4.2 输出结果
(base) PS E:\Goproject\src\gocode\testproject01> go run .\main\main.go
防止select被阻塞
5. refer + recover 解决错误
5.1 main
import ("fmt""sync"
)var wg sync.WaitGroup
// 输出操作
func printNum () {defer wg.Done()for i := 0; i < 10; i++ {fmt.Println(i)}
}// 除法操作
func devide () {defer wg.Done()defer func() {err := recover()if err != nil {fmt.Println("程序异常: ", err)}}()num1 := 10num2 := 0res := num1 / num2fmt.Println("结果: ", res)
}func main() { // 主线程wg.Add(2)go printNum()go devide()wg.Wait()}
5.2 输出结果
(base) PS E:\Goproject\src\gocode\testproject01> go run .\main\main.go
程序异常:  runtime error: integer divide by zero
0
1
2
3
4
5
6
7
8
9

http://www.ppmy.cn/ops/123710.html

相关文章

【C++篇】虚境探微:多态的流动诗篇,解锁动态的艺术密码

文章目录 C 多态详解&#xff08;进阶篇&#xff09;前言第一章&#xff1a;多态的原理1.1 虚函数表的概念1.1.1 虚函数表的生成过程 1.2 虚表的存储位置 第二章&#xff1a;动态绑定与静态绑定2.1 静态绑定2.1.1 静态绑定的实现机制&#xff1a;2.1.2 示例代码&#xff1a; 2.…

手动降级wsl中的numpy

下载完pytorch之后想验证一下cuda好不好使&#xff0c;在测试的时候发现一个warning python中报错如下 我下载的pytorch版本比较低&#xff0c;numpy太高&#xff0c;所以需要手动给numpy降级 pip install numpy\<2 降级后再进到python验证cuda就没有warning和报错了&…

Spring Cloud Netflix Ribbon 负载均衡详解和案例示范

1. 引言 在传统的集中式架构中&#xff0c;负载均衡器一般是放置在服务器端的&#xff0c;例如 Nginx等。随着微服务架构的兴起&#xff0c;服务实例的数量和部署地点变得更加动态和分布式&#xff0c;这使得在客户端进行负载均衡成为了一种可行且更灵活的方案。Netflix Ribbo…

kubeadm部署k8s1.28.0主从集群(cri-dockerd)

1. kubernetes集群规划 主机IP主机名主机配置角色192.168.100.3master12C/4G管理节点192.168.100.4node12C/4G工作节点192.168.100.5node22C/4G工作节点 2. 集群前期环境准备 &#xff08;1&#xff09;初始化脚本 #!/bin/bash echo "——>>> 关闭防火墙与SE…

专栏简介:Java 17 深入剖析:从入门到精通

Java 17 深入剖析&#xff1a;从入门到精通 专栏简介 在信息技术迅猛发展的今天&#xff0c;Java 语言凭借其跨平台的特性、强大的生态系统以及丰富的社区支持&#xff0c;依然稳居开发者的首选。随着 Java 17 的发布&#xff0c;Java 语言引入了众多创新特性和改进&#xff…

SM2无证书及隐式证书公钥机制签名和加密过程详解(二)

前面对非显式证书公钥机制&#xff08;无证书和隐式证书&#xff09;的密钥生成过程进行了描述&#xff08;SM2无证书及隐式证书公钥机制签名和加密过程详解(一)_sm2加密解密过程-CSDN博客&#xff09;&#xff0c;这里接着对隐式证书ASN.1模板和生成过程进行说明。 &#xff…

await的作用(举例)

问&#xff1a; 当方法a中又三个方法a1、a2、a3、a4都是异步函数&#xff0c;现在在a2、a4追加await&#xff0c;方法执行顺序是什么&#xff1f;主进程顺序是什么&#xff1f; 答&#xff1a; 通过一个具体的例子来说明当方法 a 中有四个异步方法 a1、a2、a3 和 a4&#xff…

【内存池】——Nginx 内存池结构设计

目录 实现思路——分而治之 Nginx 的内存池结构图 结构体设计 内存池设计&#xff1a; 数据区属性设计&#xff1a; 大块内存区设计&#xff1a; 伪代码解释&#xff1a; 数据结构实现 实现思路——分而治之 算法结构&#xff1a;链表顺序表 1、对于每个请求或者连接都会建…