golang -- 认识channel底层结构

发布于:2025-05-16 ⋅ 阅读:(22) ⋅ 点赞:(0)

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 bubble
	closed   uint32        channel是否关闭的标志
	timer    *timer // timer feeding this chan
	elemtype *_type // element type
	sendx    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中使用的chan
	elem 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 int64

	wg 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 int64

	wg sync.WaitGroup // 等待组

	m sync.Mutex // 互斥锁
)

// add 对全局变量x执行5000次加1操作
func add() {
	for i := 0; i < 5000; i++ {
		m.Lock() // 修改x前加锁
		x = x + 1
		m.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.Mutex
	var wg sync.WaitGroup

	wg.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释放
两者都在等对方释放锁,形成死锁


网站公告

今日签到

点亮在社区的每一天
去签到