当前位置: 首页 > 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释放
两者都在等对方释放锁,形成死锁

http://www.xdnf.cn/news/462709.html

相关文章:

  • LLM Text2SQL NL2SQL 实战总结
  • set, multiset ,unordered_set; map, multimap, unordered_map
  • 【向量维度如何选择?】
  • 深入探索向量数据库:构建智能应用的新基础
  • linux dbus
  • print()函数详解:输出文字、变量与格式化
  • Windows 安装 Redis 的几种方式
  • 设计模式(基于Python3)
  • Python课程及开源项目推荐
  • 宣纸阁项目测试报告
  • 流程编辑器Bpmn与LogicFlow学习
  • 2025长三角数学建模C题完整思路
  • Python多线程
  • 计算机网络:什么是电磁波以及有什么危害?
  • 谷歌量子计算机:开启计算新纪元
  • C# 活动窗体截图:基于 Win32 API 的实现
  • 有效的括号
  • 【蓝桥杯省赛真题49】python偶数 第十五届蓝桥杯青少组Python编程省赛真题解析
  • ROS--NAVI DWA
  • 【c语言】动态内存分配
  • MySQL 迁移至 Doris 最佳实践方案
  • 低功耗实现方法思路总结
  • 策略模式-枚举实现
  • 如何判断一个网站后端是用什么语言写的
  • 7.Pyecharts:全局配置项1
  • Python 翻译词典小程序
  • 平替BioLegend品牌-Elabscience FITC Anti-Mouse CD8a抗体(53-6.7)精准标记T细胞表面抗原
  • 断点续传使用场景,完整前后端实现示例,包括上传,下载,验证
  • 麒麟系统ARM64架构部署mysql、jdk和java项目
  • 牛客网刷题:NC208813求逆序数