当前位置: 首页 > news >正文

【golang长途旅行第30站】channel管道------解决线程竞争的好手

channel

为什么需要channel

使用全局变量加锁同步来解决goroutine的竞争,可以但不完美

  • 难以精确控制等待时间​(主线程无法准确知道所有 goroutine 何时完成)。

  • 全局变量容易引发竞态条件​(即使加锁,代码复杂度也会增加)。

  • 不够优雅,Go 更推荐使用 ​channel​ 进行通信。

channel基本介绍

  • ​channel(通道)​​ 是一种用于 ​goroutine(协程)之间通信和同步​ 的机制。
  • channel的本质是一个队列,遵循先进先出(FIFO)
  • channel是有类型的,一种channel只能存储类型与该channel的类型相同的数据
  • channel是线程安全的,不需要加锁
  • channel是引用类型,必须初始化make后才能写入数据,未初始化的channel是nil

channel快速入门

channel的声明

var 变量名 chan 数据类型

channel的初始化

var myChan chan int = make(chan int, 2)

发送数据到channel

myChan <- 10
myChan <- 20

此处注意:channel不像map等会自动扩容,channel接收的数据数的最大值在make函数里已经自定义完成了,容量就是一直这么大,不会改变。

从channel接收数据

num := <-myChan
fmt.Println(num)

输出结果:10
此处接收数据不能超出myChan现有的数据量,也就是myChan的长度。

channel的细节

  1. channel中只能存放指定的数据类型

  2. channle的数据放满后,就不能再放入了

  3. channel放满后,如果从channel取出数据后,可以继续放入

<-myChan也是取出了数据,只是没有被接收罢了

  1. 在没有使用协程的情况下,如果channel数据取完了,再取,就会报deadlock
易错点

如果想在channel中输入多样的数据类型,就将channel声明成空接口interface{}的类型
代码示例:
func main() {
myChan := make(chan interface{}, 3)
myChan <- 10
myChan <- “sa”
person := Person{“xxx”}
myChan <- person
<-myChan
<-myChan
person2 := <-myChan
// fmt.Printf(“person2的type%T,值%v,name%v”, person2, person2, person2.Name)
person3 := person2.(Person)
fmt.Printf(“person3的type:%T,值:%v,name:%v”, person3, person3, person3.Name)
}
输出结果:person3的typemain.Person,值{xxx},namexxx

唉,为什么要person3 := person2.(Person)呢,直接用fmt.Printf(“person2的type%T,值%v,name%v”, person2, person2, person2.Name)不好吗?
当然不好啦,使用这个代码会报错,会说person2.Name undefined
为什么呢?
因为person2是从channel中读取的interface{}类型,虽然实际值是Person类型,但编译器不知道其具体类型,因此无法直接访问Name字段。
所以要通过person3 := person2.(Person),提取一个类型断言后的值,将其转换为具体的Person类型,然后才能访问其字段

channel的关闭

发送方可以关闭 channel,表示不再发送数据
内置函数:close(ch)
•​关闭后,仍然可以接收数据​(直到 channel 为空)。
•​向已关闭的 channel 发送数据会 panic。

channel的遍历

  1. 通过 for-range 遍历
    代码示例:
    func main() {
    myChan := make(chan int, 3)
    myChan <- 10
    myChan <- 30
    myChan <- 20
    close(myChan)
    for v := range myChan {
    fmt.Printf(“%v\n”, v)
    }
    }
    输出结果:
    10
    30
    20

for range会一直从 ch接收数据,直到 ch被关闭。
如果 ch未关闭,for range会一直阻塞,可能导致死锁。

手动检查 channel 是否关闭
可以用 value, ok := <-ch的方式检查 channel 是否关闭, 如果 channel 关闭,ok 为 false

  1. 传统for循环
    func main() {
    myChan := make(chan int, 3)
    myChan <- 10
    myChan <- 30
    myChan <- 20
    len := len(myChan)
    for i := 0; i < len; i++ {
    fmt.Println(<-myChan)
    }
    }

也可以正常有序输出,输出结果与for-range一致

channel的阻塞

阻塞是指 goroutine 在 channel 操作上等待,但不会导致整个程序卡死。​

  1. 从空的 channel 接收数据
  2. 向已满的缓冲 channel 发送数据
  3. 读比写的操作慢,导致出现(2)情况
  4. 写比读的操作慢,导致出现(1)情况

channel的死锁

死锁是指所有 goroutine 都在等待对方释放资源,导致程序无法继续执行。

  1. 所有 goroutine 都在等待 channel
  2. 未关闭 channel 导致 for range死锁

使用细节

  1. channel可以声明为只读,或者只写性质

此处只读只写只是一种属性,并不会改变channel的类型,该是chan int 就还是chan int
chan<- int 是只写
<-chan int 是只读

代码示例:
package main

import (
“fmt”
“math/rand”
“time”
)

// 只写通道:用于发送订单
func orderProducer(orderChan chan<- int, doneChan chan<- struct{}) {
defer close(orderChan) // 生产结束后关闭订单通道

for i := 1; i <= 5; i++ {orderID := rand.Intn(1000) + 1000 // 模拟生成订单号fmt.Printf("📦 生成订单 #%d (ID: %d)\n", i, orderID)orderChan <- orderIDtime.Sleep(time.Second) // 模拟生产间隔
}doneChan <- struct{}{} // 发送完成信号

}

// 只读通道:用于处理订单
func orderProcessor(orderChan <-chan int, doneChan chan<- struct{}) {
for orderID := range orderChan { // 自动检测通道关闭
processTime := time.Duration(rand.Intn(1500)) * time.Millisecond
fmt.Printf(“处理订单 ID: %d (耗时: %v)\n”, orderID, processTime)
time.Sleep(processTime)
}

doneChan <- struct{}{} // 发送完成信号

}

func main() {
// 初始化通道(带缓冲)
orderChan := make(chan int, 3) // 订单通道(缓冲3个订单)
doneChan := make(chan struct{}, 2) // 控制通道(缓冲2个信号)

// 启动服务
go orderProducer(orderChan, doneChan) // 订单生产(只写)
go orderProcessor(orderChan, doneChan) // 订单处理(只读)// 等待两个服务完成
for i := 0; i < 2; i++ {<-doneChan
}
fmt.Println("所有订单处理完成")

}

这段代码中,main函数中定义的orderChan是一个chan int 类型,但他可以同时被使用在只读和只写的函数里,这就很大程度上的便于代码的管理,防止误操作。

  1. select解决 channel 阻塞问题
    日常中,难以准确判断读取/写入与关闭时机难以掌握,所以提出select,虽然select还是无法关闭channel,但是能防止防止读取/写入时的无限等待
    代码示例
    for{
    select {
    case msg := <-ch1:
    fmt.Println(“收到 ch1:”, msg)
    case msg := <-ch2:
    fmt.Println(“收到 ch2:”, msg)
    case <-time.After(3 * time.Second): // 超时控制
    fmt.Println(“读取超时”)
    return
    }
    }

如果多个 case 的 channel 同时就绪(例如多个 channel 都有数据可读),select​会随机选择一个执行​(公平调度,避免饥饿问题)
select​自动忽略未就绪的 channel​(无论是否关闭),无需手动处理。
此处的ch1哪怕没有关闭,也不会报错,而是在无法从ch1取到值后,会暂时将这个case不考虑在执行case内
​每次执行 select时都会重新检查所有 case的就绪状态
还有,最后的return不能使用break代替,因为break只能退出select不能退出for循环,所以相当于重新开始了
return其实还可以用之前提到的label来代替,就是给这个for循环一个标签,然后break label就好了 (但是这种方式并不建议,可读性较差)

  1. recover来防止出现因为一个线程的错误导致其它线程无法进行
    原错误代码:
    package main

import (
“fmt”
“time”
)

// 1. 循环打印 “hello,world”
func sayHello() {
for i := 0; i < 10; i++ {
fmt.Println(“hello,world”)
time.Sleep(1 * time.Second)
}
}

// 2. 测试未初始化的 map(会触发 panic)
func test() {
var myMap map[int]string
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
myMap[0] = “golang” // error: 未初始化的 map 赋值会导致 panic
}

// 3. 主函数(并发执行)
func main() {
go sayHello() // 启动协程
go test() // 启动协程(会崩溃)

// 主线程继续执行
for i := 0; i < 10; i++ {fmt.Printf("main() ok=%d\n", i)time.Sleep(1 * time.Second)
}

}
输出结果:
main() ok=0
hello,world
panic: assignment to entry in nil map

报了panic错误,主线程并没有正常运行

修改代码:
func test() {
var myMap map[int]string
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
myMap[0] = “golang” // error: 未初始化的 map 赋值会导致 panic
}

将错误的test函数加上错误捕获,异常处理

输出结果:
hello,world
assignment to entry in nil map
main() ok=0
main() ok=1
hello,world
hello,world
main() ok=2
hello,world
main() ok=3
hello,world
main() ok=4
hello,world
main() ok=5
main() ok=6
hello,world
hello,world
main() ok=7
main() ok=8
hello,world
hello,world
main() ok=9

即使仍是错误,也依旧不影响其它线程

http://www.dtcms.com/a/348754.html

相关文章:

  • 在WSL2-Ubuntu中安装Anaconda、CUDA13.0、cuDNN9.12及PyTorch(含完整环境验证)
  • 深度学习与自动驾驶中的一些技术
  • 51c自动驾驶~合集18
  • 点评《JMeter核心技术、性能测试与性能分析》一书
  • 使用单个连接进行数据转发的设计
  • 大数据毕业设计选题推荐-基于大数据的北京市医保药品数据分析系统-Spark-Hadoop-Bigdata
  • 1688拍立淘接口数据全面解析详细说明(item_search_img)
  • Highcharts Maps/地图 :高性能的地理数据可视化方案
  • 打工人日报#20250824
  • CTFHub技能树 git泄露3道题练习--遇到没有master如何解决!!!
  • 一文掌握 Java 键盘输入:从入门到高阶(含完整示例与避坑指南)
  • 【大模型LLM学习】Research Agent学习笔记
  • c++随笔二
  • CI/CD企业案例详解
  • 从零开始学习概念物理(第13版)(1)
  • 问卷管理系统测试报告
  • 极验demo(float)(二)
  • JAVA快速学习(一)
  • 30分钟通关二分查找:C语言实现+LeetCode真题
  • 【通识】大模型
  • AI工具:开启开发实践的新纪元
  • Qt---架构文件.pro
  • Shell 循环实战:while 与 until 的趣味编程之旅
  • 【轨物交流】轨物科技亮相“智汇日照・杭电赋能”科技合作交流会,共谋产学研用新篇章
  • MOS的导通及应用
  • 6.2 el-menu
  • 20.13 ChatGLM3 QLoRA微调实战:3步实现高效低资源训练
  • 06 - spring security角色和权限设置
  • 虚拟机蓝屏问题排查与解决
  • 小工具推荐