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

golang -- 认识channel底层结构

channel

channel是golang中用来实现多个goroutine通信的管道(goroutine之间的通信机制),底层是一个叫做hchan的结构体,定义在runtime包中

type hchan struct {qcount   uint           // 循环数组中的元素个数(通道中元素个数)dataqsiz uint           // 循环数组的长度buf      unsafe.Pointer // 数组指针elemsize uint16          能够收发的元素的大小synctest bool // true if created in a synctest bubbleclosed   uint32        channel是否关闭的标志timer    *timer // timer feeding this chanelemtype *_type // element typesendx    uint   // 下一次发送数据的下标位置recvx    uint   // 下一次接收数据的下标位置recvq    waitq  // 读等待队列sendq    waitq  // 写等待队列// lock protects all fields in hchan, as well as several// fields in sudogs blocked on this channel.//// Do not change another G's status while holding this lock// (in particular, do not ready a G), as this can deadlock// with stack shrinking.lock mutex      //互斥锁,保证读写channel时不存在并发竞争问题
}

hchan结构体的组成部分主要有四个:

  1. buf --> 保存goroutine之间传递数据的循环链表
  2. sendx和recvx --> 记录循环链表当前发送或接收数据的下标值
  3. sendq和recvq --> 保存向chan发送和从chan接受数据的goroutine的队列
  4. lock --> 保证channel写入和读取数据时线程安全的锁

🔗有关channel的基本学习

select

select定义在runtime包中

在Linux系统下,select 是一种IO多路复用的解决方案,IO多路复用就是用一个线程处理多个IO请求。
在Golang中,select 是 在一个goroutine协程监听多个channel(代表多个goroutine)的读写事件,提高从多个channel获取信息的效率

select的使用方法与switch相似,这是一个使用select 语句的例子

select {
case <- chan1://语句1
case chan2 <- 1://语句2
default://语句3
}

对上面这段代码的解释:

第一个case:如果成功接收到chan1中的数据,执行语句1
第二个case:如果成功发送数据到chan2,执行语句2
default:如果上面case都不满足,执行语句3

select底层是一个 在runtime包中定义的 scase结构体

type scase struct {c    *hchan         // case中使用的chanelem unsafe.Pointer // 指向case包含的数据的指针
}

在case语句中(除default),包含的是对channel的读写操作,所以scase结构体中包含这两个要素:使用的channel 和指向数据的指针

select的几个规则

  1. select中的多个case的表达式必须都是channel的读写操作,不能是其他的数据类型;
  2. 如果不满足任何case语句,同时没有default,那么当前的goroutine阻塞(没有case时,所在的goroutine永久阻塞,发生panic)
  3. Go自带死锁检测机制,当发现当前协程再也没有机会被唤醒时,则发生panic
  4. select中满足多个case,随机选择一个满足的case下的语句去执行
  5. select 中只有一个case时(不是default),实际会被编译器转换为对该channel的读写操作,和实际调用data:=<-ch 或 ch<-data 没有什么区别
    例如这样的一个代码
ch := make(chan struct{})
select {
case data <- ch:fmt.Printf("ch data: %v\n", data)
}

会被编译器转换为

data := <- ch
fmt.Printf("ch data: %v\n", data)
  1. select 中有一个case + 一个 default
package main
import ("fmt"
)
func main() {ch := make(chan int)select {case ch <- 1:fmt.Println("case")default:fmt.Println("default")}
}

编译器会转换为

if selectnbsend(ch, 1) {fmt.Println("case")
} else {fmt.Println("default")
}
func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) {return chansend(c, elem, false, getcallerpc())
}

**runtime.selectnbsend()**函数调用runtime.chansend()函数,传入这个函数的第三个参数是false,该参数是 block,为false代表非阻塞,即每次尝试从channel读写值,如果不成功则直接返回,不会阻塞。

锁与死锁

数据竞争

多个goroutine同时对一个变量进行处理时,会造成数据竞争,某个goroutine执行的结果可能会覆盖掉其他goroutine中的操作,导致结果与预期不符
比如这样一个代码

var (x int64wg sync.WaitGroup // 等待组
)// add 对全局变量x执行5000次加1操作
func add() {for i := 0; i < 5000; i++ {x = x + 1}wg.Done()
}func main() {wg.Add(2)go add()go add()wg.Wait()fmt.Println(x)
}

预期的结果时输出10000,但是每次执行都会输出不同的结果

要想程序正确执行,可以给goroutine上锁(这个例子中是互斥锁),从而保证同一时间只有一个goroutine可以访问共享资源。

Go语言中的锁分为两种:互斥锁和读写锁

互斥锁

互斥锁只有一种锁,即sync.Mutex,是绝对锁,同一时刻一段代码只能被一个线程运行,使用方法Lock(加锁)和Unlock(解锁)即可实现

方法功能
func lock(l *mutex)获取互斥锁
func unlock(l *mutex)获取互斥锁

上面代码使用互斥锁

var (x int64wg sync.WaitGroup // 等待组m sync.Mutex // 互斥锁
)// add 对全局变量x执行5000次加1操作
func add() {for i := 0; i < 5000; i++ {m.Lock() // 修改x前加锁x = x + 1m.Unlock() // 改完解锁}wg.Done()
}func main() {wg.Add(2)go add()go add()wg.Wait()fmt.Println(x)
}

输出

10000

在Lock()和Unlock()之间的代码段称为资源的临界区(critical section),临界区的代码是要执行的代码

使用互斥锁能够保证同一时间有且只有一个 goroutine 进入临界区,其他的 goroutine 则在等待;当互斥锁释放后,等待的 goroutine 才可以获取锁进入临界区
多个 goroutine 同时等待一个锁时,唤醒的策略是随机的

读写锁

互斥锁保证了同一时间一段代码只能被一个线程运行,但是当不涉及资源的修改,只是获取资源时,使用互斥锁就没必要了,这种场景下读写锁是种更好的选择

读写锁有两种锁,即读锁和写锁。

读锁(RLock),不是绝对锁,允许多个读者同时读取;
写锁(Lock),是绝对锁,同一时刻一段代码只能被一个线程运行

当已经有读锁时,还可以任意加读锁,不可以加写锁(直到读锁全部释放)
当已经有写锁时,不可以再加读锁,也不可以再加写锁

读写锁的方法

方法功能
func (rw *RWMutex) RLock()获取读锁
func (rw *RWMutex) RUnlock()释放读锁
func (rw *RWMutex) Lock()获取写锁
func (rw *RWMutex) Unlock()释放写锁
func (rw *RWMutex) RLocker() Locker返回一个实现Locker接口的读写锁

读写锁的优点:
使用读写互斥锁在读多写少的场景下能够极大地提高程序的性能

注:对于锁而言,不应该将其作为值传递和存储,应该使用指针

死锁

当两个或两个以上的进程在执行过程中,因争夺资源而处理一种互相等待的状态,如果没有外部干涉无法继续下去,这时我们称系统处于死锁或产生了死锁

死锁主要有以下几种场景。

  1. Lock/Unlock不是成对出现时

没有成对出现容易会出现死锁的情况
例如下面只有Unlock 没有Lock 的情况

var m sync.Mutex        //锁
var wait sync.WaitGroup //等待组变量func hello() {fmt.Println("hello")
}
func main() {hello()m.Unlock()
}

报错
go fatal error: sync: unlock of unlocked mutex

使用defer ,使 lock 和unlock 紧凑出现可以增加容错

m.Lock()
defer m.Unlock()
  1. 锁被拷贝使用
func main(){m.Lock()defer m.Unlock()copyTest(m)
}func copyTest(m sync.Mutex) {  //值传递m.Lock()   //defer m.Unlock()fmt.Println("ok")
}

在函数外,加了一个Lock,在拷贝的时候又执行了一次Lock,这时候发生堵塞,而函数外层的Unlock也无法执行,所以永远获得不了这个锁,这时候就发生了死锁

  1. 交叉锁

下面这样一段代码

func main() {var mA, mB sync.Mutexvar wg sync.WaitGroupwg.Add(2)go func() {defer wg.Done()mA.Lock()defer mA.Unlock()mB.Lock()defer mB.Lock()}()go func() {defer wg.Done()mB.Lock()defer mB.Lock()mA.Lock()defer mA.Unlock()}()wg.Wait()
}

执行后
go fatal error: all goroutines are asleep - deadlock!

执行过程:
goroutine1获取mA
goroutine2获取mB
goroutine1尝试获取mB,但是已经被goroutine2获取,等待mB释放
goroutine2尝试获取mA,但是已经被goroutine1获取,等待mA释放
两者都在等对方释放锁,形成死锁

相关文章:

  • AI软件汇总与功能解析:赋能未来的智能工具库
  • 以项目的方式学QT开发(二)——超详细讲解(120000多字详细讲解,涵盖qt大量知识)逐步更新!
  • mysql 基础复习-安装部署、增删改查 、视图、触发器、存储过程、索引、备份恢复迁移、分库分表
  • 8、SpringBoot集成MinIO
  • 鸿蒙OSUniApp 制作简洁高效的标签云组件#三方框架 #Uniapp
  • 插槽(Slot)的使用方法
  • GPUGeek云平台实战:DeepSeek-R1-70B大语言模型一站式部署
  • 应用BERT-GCN跨模态情绪分析:贸易缓和与金价波动的AI归因
  • buildroot使用外部编译链编译bluez蓝牙工具
  • MySQL-数据库分布式XA事务
  • 连接指定数据库时提示not currently accepting connections
  • Golang基础知识—cond
  • LM2902:一款高性能四运算放大器的解析
  • 蓝桥杯 2024 C++国 B最小字符串
  • 论文学习_Directed Greybox Fuzzing
  • 《MySQL:MySQL视图特性》
  • rsync入门笔记
  • 第30节:现代CNN架构-轻量级架构EfficientNet
  • 【YOLO 系列】基于YOLO的道路坑洞检测识别系统【python源码+Pyqt5界面+数据集+训练代码】
  • 各个历史版本mysql/tomcat/Redis/Jdk/Apache下载地址
  • 新修订的《餐饮业促进和经营管理办法》公布,商务部解读
  • 跨越三十年友情,61岁余隆和60岁齐默尔曼在上海再度合作
  • 美国将与阿联酋合作建立海外最大的人工智能数据中心
  • 埃尔多安:愿在土耳其促成俄乌领导人会晤
  • 宜昌谱写新叙事:长江大保护与高质量发展如何相互成就
  • 制造四十余年血腥冲突后,库尔德工人党为何自行解散?