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结构体的组成部分主要有四个:
- buf --> 保存goroutine之间传递数据的循环链表
- sendx和recvx --> 记录循环链表当前发送或接收数据的下标值
- sendq和recvq --> 保存向chan发送和从chan接受数据的goroutine的队列
- 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的几个规则
- select中的多个case的表达式必须都是channel的读写操作,不能是其他的数据类型;
- 如果不满足任何case语句,同时没有default,那么当前的goroutine阻塞(没有case时,所在的goroutine永久阻塞,发生panic)
- Go自带死锁检测机制,当发现当前协程再也没有机会被唤醒时,则发生panic
- select中满足多个case,随机选择一个满足的case下的语句去执行
- 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)
- 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接口的读写锁 |
读写锁的优点:
使用读写互斥锁在读多写少的场景下能够极大地提高程序的性能
注:对于锁而言,不应该将其作为值传递和存储,应该使用指针
死锁
当两个或两个以上的进程在执行过程中,因争夺资源而处理一种互相等待的状态,如果没有外部干涉无法继续下去,这时我们称系统处于死锁或产生了死锁。
死锁主要有以下几种场景。
- 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()
- 锁被拷贝使用
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也无法执行,所以永远获得不了这个锁,这时候就发生了死锁
- 交叉锁
下面这样一段代码
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释放
两者都在等对方释放锁,形成死锁