1 协程
线程是操作系统的内核对象,有 cpu时间片 的概念,线程之间进行抢占式调度。在 java 中,尽管 jvm 是系统的一个进程,但 cpu 的最小调度单位是线程而非进程,所以 java 中多线程仍然可能会被 OS 调度到多个 cpu中执行。
协程也被称作“用户态线程”,操作系统不会感知到协程的存在,由 go 进程对协程进行分配,所以协程切换不会存在线程上下文切换(用户态和内核态)的损耗。
协程之间是协作式调度,将 goroutine 合理地分配到每个 CPU 中,最大限度地使用多核CPU性能。一个协程在哪个线程上执行是不确定的,由调度器决定。
特点:
1. 轻量级,所有的消耗几乎只有栈空间的分配,并且开始时栈非常小,仅在需要时才会分配堆空间。
2. 同一个线程内部最多有一个协程正在运行。当协程睡眠或结束后,线程的运行权会转让给其他协程。
3. 子协程的异常退出会传播到主协程,直接导致主协程也跟着挂掉。
2 通道
共享内存和通道是程序中最常用的协程通信方式。
锁属于共享内存中的一种方式,至于共享内存是如何进行通信的,详见博客:juejin.cn/post/698101…
事实上 go 主要使用通道作为通信模型。
创建通道
ch := make(chan int, len) // len 表示缓冲区大小,FIFO
ch <- 1 // 写入
<- ch // 读取
注意区分无缓冲区通道和缓冲区为 1 的通道。缓冲通道的发送和接收不会互相依赖,用异步的方式传递数据。
无缓冲区通道会造成协程阻塞,阻塞 channel 的发送或接收可能会引起 goroutines 的内存泄漏:即使被阻塞的 channel 无法访问,垃圾收集器也不会终止 goroutine。
关闭通道
通过 close() 方法能够关闭通道,读取一个已经关闭的通道会立即返回通道类型的零值(如果缓冲区中还有数据,会读出来),而写入一个已经关闭的通道会造成 panic。
通过 for-range 读通道,能够避免读取已关闭通道的零值,当通道为空时,循环会阻塞;当通道关闭,循环会停止。
var c = make(chan int, 3)
//子协程写
go func() {
c <- 1
defer close(c)
}()
//直接读取通道,存在不知道子协程是否已关闭的情况
//fmt.Println(<-c)
//主协程读取:使用for...range安全的读取
for value := range c {
fmt.Println(value)
}
如何安全的写通道,确保不会写入已经关闭的通道而导致异常呢?
确保通道写安全的最好方式是由负责写通道的协程自己来关闭通道,读通道的协程不要去关闭通道。
但这种方式只能解决单个写通道的情形,我们可以使用内置的 sync.WaitGroup,它使用计数来等待指定事件完成,需要额外一个专门的通道做这件事。
func main() {
var ch = make(chan int, 8)
var wg = new(sync.WaitGroup)
//写协程
for i := 1; i <= 4; i++ {
wg.Add(1)
go func(num int, ch chan int, wg *sync.WaitGroup) {
defer wg.Done()
ch <- num
ch <- num * 10
}(i, ch, wg)
}
//读
go func(ch chan int) {
for num := range ch {
fmt.Println(num)
}
}(ch)
//Wait阻塞等待所有的写通道协程结束,待计数值变成零,Wait才会返回
wg.Wait()
//安全的关闭通道
close(ch)
//防止读取通道的协程还没有完毕
time.Sleep(time.Second)
fmt.Println("finish")
}
3 协程和通道的常见场景
消息队列
type message struct {
num int
}
func main() {
ch := make(chan message, 10)
go connsumer(ch)
go produce1(ch)
go produce2(ch)
time.Sleep(time.Second)
}
func producer1(c chan<- message) {
for i := 0; i < 10; i++ {
c <- message{i}
}
}
func producer2(c chan<- message) {
for i := 10; i < 20; i++ {
c <- message{i}
}
}
func connsumer(c <-chan message) {
for m := range c{
fmt.Println(m)
}
}
替代锁的功能
首先需要声明的是,chan 的设计目的不是为了替代(取代)锁的功能,具体情况到底是用 chan 还是直接用锁,需要具体情况具体分析,大的决策点是,哪种简单方便更易理解更具有可读性就用哪种。(显然,使用chan实现共享变量在协程之间的安全性,不如直接使用锁可读性高)
func main() {
x := 0
ch := make(chan int, 1)
for i := 0; i < 1000; i++{
go func() {
ch <- i
x ++
<- ch
}()
go func() {
ch <- i
x --
<- ch
}()
}
time.Sleep(5 * time.Second)
fmt.Println(x)
}
4. channel 底层原理
channel 底层数据结构
type hchan struct {
qcount uint
dataqsiz uint
buf unsafe.Pointer // 缓冲数据队列
elemsize uint16
closed uint32
elemtype *_type
sendx uint // 后续写数据的位置
recvx uint // 后续读数据的位置
recvq waitq // 等待读消息的goroutine队列
sendq waitq // 等待写消息的goroutine队列
lock mutex // 互斥锁,为每个读写操作锁定通道,因为发送和接收必须是互斥操作
}
发送数据:
读取数据: