目录
1.分类
channel的三种状态
channel的两种类型——有缓冲,无缓冲
无缓冲
有缓冲
2.操作
1.创建
2.发送
3.接收
4.关闭
3.使用场景
4.channel底层
5.channel线程安全
为什么是线程安全的
如何实现线程安全
6.channel控制goroutine并发执行顺序
7.channel共享内存的优缺点
8.channel发送和接收什么时候会死锁
groutine的通信机制,每个channel都有类型
ch:=make(chan int)
//ch为make创建的底层数据结构的引用,零值为nil
- 2种类型:无缓冲、有缓冲
- 3种模式:写操作模式(单向通道)、读操作模式(单向通道)、读写操作模式(双向通道)
写操作模式
读操作模式
读写操作模式
创建
make(chan<-int)
make(<-chan int)
make(chan int)
未初始化
关闭
正常
关闭
panic
panic
正常关闭
发送
永远阻塞导致死锁
panic
阻塞/成功发送
接收
永远阻塞导致死锁
缓冲区为空则为零值,否则可以继续读且返回true
阻塞/成功接收
无缓冲 有缓冲 创建方式 make(chan TYPE) make(chan TYPE) 发送阻塞 数据接收前发送阻塞 缓冲区满时发送阻塞 接收阻塞 数据发送前接收阻塞 缓冲区空时接收阻塞 无缓冲
时刻都是同步状态
无缓冲chan必须有发送G/接收G的同时有接收G/发送G,否则就会阻塞,
报错:fatal error: all goroutines are asleep - deadlock!
package main import ( "fmt" "time" ) func loop(ch chan int) { for { select { case i := <-ch: fmt.Println("this is value of unbuffer channel", i) } } } func main() { ch := make(chan int) ch <- 1 go loop(ch) time.Sleep(1 * time.Millisecond) }
但如果把ch <-放到go loop(ch)下面,程序就会正常运行
有缓冲
package main import ( "fmt" "time" ) func loop(ch chan int) { for { select { case i := <-ch: fmt.Println("this is value of unbuffer channel", i) } } } func main() { ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3 ch <- 4 go loop(ch) time.Sleep(1 * time.Millisecond) }
也会报错:fatal error: all goroutines are asleep - deadlock!
是因为channel的大小为3,而要往里面塞4个数据,所以会阻塞住
解决方法:
1.把channel的长度调大(不是很好的解决方案)
2.把发送者ch<-1等代码移动到go loop(ch)下面,让channel实时消费就不会导致阻塞了
注意点:
一个chan不能多次关闭,会导致panic
如果多个goroutine都监听同一个channel,那么channel上的数据都可能随即被某一个goroutine取走进行消费
如果多个goroutine都监听同一个channel,如果这个channel被关闭,则所有goroutine都能收到退出信号
1.创建
带缓冲 ch := make(chan int,3) 不带缓冲 ch := make(chan int)
创建时会做一些检查:
- 元素大小不能超过64K
- 元素对齐大小不能超过maxAlign(8字节)
- 计算出来的内存是否超过限制
创建时的策略:
- 无缓冲的channel——会直接给hchan分配内存
- 有缓冲的channel并且元素不包含指针(buf指针,指向底层数组)——会为hchan和底层数组分配一段连续的地址
- 有缓冲的channel并且元素包含指针——会为hchan和底层数组分别分配地址
2.发送
包括检查和数据发送两个步骤
数据发送步骤
1.如果channel的读等待队列存在接收者goroutine(有发送者goroutine阻塞)
将数据直接发送给第一个等待的goroutine,唤醒接收的goroutine
2.如果channel的读等待队列不存在接收者goroutine(无有发送者goroutine阻塞)
如果buf指向的循环数组未满,会把数据发送到循环数组的队尾
如果buf指向的循环数组已满,就会阻塞,将当前goroutine加入写等待队列,并挂起等待唤醒
func chansend(c *hchan,ep unsafe.Pointer,block bool,callerpc uintptr)bool
阻塞式:
调用chansend函数,且block=true
ch <- 10
非阻塞式:
调用chansend函数,且block=false
通过select让其在将阻塞时直接返回
select { case ch <- 10: ... default }
3.接收
包括检查和数据接收两个步骤
数据接收步骤
1.如果channel的写等待队列存在发送者goroutine(有发送者goroutine阻塞)
如果是无缓冲channel,直接从第一个发送者goroutine那里把数据拷贝给接收变量,唤醒发送的goroutine
如果是有缓冲channel(已满),将循环数组buf的队首元素拷贝给接收变量,将第一个发送者goroutine的数据拷贝到buf指向的循环数组队尾,唤醒发送的goroutine
2.如果channel的写等待队列不存在发送者goroutine(没有发送者goroutine阻塞)
如果buf指向的循环数组非空,将循环数组的队首元素拷贝给接收变量
如果buf指向的循环数组为空,这个时候就会阻塞,将当前goroutine加入读等待队列,并挂起等待唤醒
func chanrecv(c *hchan,ep unsafe.Pointer,block bool)(selected,received bool)
阻塞式:
调用chanrecv函数,且block=true
有4种方式
<-ch v:= <-ch v,ok := <-ch //当channel关闭时,for循环会自动退出,无需主动监测channel是否关闭,可以防止读取已经关闭的channel,造成读到数据为通道所存储的数据类型的零值 for i := range ch { fmt.Println(i) }
非阻塞式:
调用chanrecv函数,且block=false
select { case <- ch: ... default }
4.关闭
调用closechan函数
func closechan(c *hchan)
close(ch)
package main
import (
"fmt"
"time"
"unsafe"
)
//G1是发送者
//当G1向ch发送数据时,首先会对buf加锁,然后将task存储的数据copy到buf中,然后sendx++,然后释放对buf的锁
func sendTask(ch chan string) {
taskList := []string{"this", "is", "a", "demo"}
for _, task := range taskList {
ch <- task
}
}
//G2是接收者
//当G2消费数据时,会首先对buf加锁,然后将buf中的数据copy到task变量对应的内存里,然后recvx++,并释放锁
func recvTask(ch chan string) {
for {
task := <-ch //接收任务
fmt.Println("received", task) //处理任务
}
}
func main() {
//chan是带缓冲,缓冲大小为4的channel
//初始hchan结构体的buf为空,sendx和recvx均为0
ch := make(chan string, 4)
fmt.Println(ch, unsafe.Sizeof(ch))
go sendTask(ch)
go recvTask(ch)
time.Sleep(1 * time.Second)
}
0xc000058060 8
received this
received is
received a
received demo
- 停止信号监听
- 定时任务
- 生产方和消费方解耦
- 控制并发数
Go channel是一个队列,遵循FIFO的原则,负责协程之间的通信(Go语言不提倡通过共享内存而通信,而是通过通信来实现内存共享),CSP(Communicating Sequential Process)并发模型,就是通过goroutine和channel来实现的
通过var声明/make函数创建的channel变量是一个存储在函数栈上的指针,占用8个字节,指向堆上的hchan结构体
buf循环数组好处:消费数据后不需要移动,只用移动sendx,recvx下标
有锁,保证线程安全
type hchan struct { closed uint32 //channel是否关闭的标志 elemtype *_type //channel中的元素类型 //channel中分为有缓冲和无缓冲两种 //对于有缓冲的channel存储数据,使用了ring buffer(环形缓冲区)来缓存写入的数据 //本质是循环数组 //普通数组容量固定更适合指定的空间,弹出元素时,普通数组需要全部前移 buf unsafe.Pointer //指向底层循环数组的指针(环形缓冲区) qcount uint //循环数组中的元素数量 dataqsiz uint //循环数组的长度 elemsize uint16 //元素的大小 sendx uint //下一次写下标的位置 recvx uint //下一次读下标的位置 //尝试读取channel或向channel写入数据时被阻塞的goroutine recvq waitq //下一次写下标的位置 sendq waitq //下一次读下标的位置 lock mutex //互斥锁,保证读写channel时不存在并发竞争问题 }
hchan结构体的主要组成部分:
- 用来保存goroutine之间传递数据的循环数组:buf
- 用来记录此循环数组当前发送或接收数据的下标值:sendx和recvx
- 用于保存向该chan发送和从该chan接收数据被阻塞的goroutine队列:sendq和recvq
- 保证channel写入和读取数据时线程安全的锁:lock
等待队列:
双向链表,包含一个头结点和一个尾结点
每个节点是一个sudog结构体变量,记录哪个协程在等待,等待的是哪个channel,等待发送/接收的数据在哪里
type waitq struct{ first *sudog last *sudog } type sudog struct{ g *g next *sudog prev *sudog elem unsafe.Pointer c * hchan //等待的是哪个channel ... }
为什么是线程安全的
不同协程通过channel进行通信,本身的使用场景就是多线程,为了保证数据的一致性,必须实现线程安全
如何实现线程安全
channel的底层实现中,hchan结构体中采用Mutex锁保证读写安全
在对循环数组buf中的数据进行入队和出队操作时,必须优先获取互斥锁,才能操作channel数据
多个goroutine并发执行时,每个goroutine抢到处理器的时间点不一致,goroutine的执行本身不能保证顺序——代码中先写的goroutine并不能保证先执行
思路:使用channel进行通知,用channel去传递信息,从而控制并发执行顺序
从第x个协程中拿数据,通知第x+1个协程
package main import ( "fmt" "sync" "time" ) var wg sync.WaitGroup func print(goroutine string, inchan chan struct{}, outchan chan struct{}) { time.Sleep(1 * time.Second) //模仿内部操作耗时 select { case <-inchan://从第x个协程中拿数据 fmt.Printf("%s\n", goroutine)//通知第x+1个协程 outchan <- struct{}{} } wg.Done() } func main() { ch1 := make(chan struct{}, 1) ch2 := make(chan struct{}, 1) ch3 := make(chan struct{}, 1) ch1 <- struct{}{} wg.Add(3) var start = time.Now().Unix() go print("goroutine1", ch1, ch2) go print("goroutine2", ch2, ch3) go print("goroutine3", ch3, ch1) wg.Wait() end := time.Now().Unix() fmt.Printf("during:%d", end-start) }
goroutine1 goroutine2 goroutine3 during:1
串行耗时3s,而这里并行耗时1s
go设计思想:不要通过共享内存来通信,要通过通信来共享内存
通过发送消息进行同步常见例子:goCSP模型(Communication Sequential Process)
->process1
process->channel ->process2
->process3
大部分语言采用的都是通过共享内存进行通信,然后通过互斥锁,CAS等操作保证并发安全
go引入了channel和goroutine实现CSP模型,将生产者和消费者进行了解耦,channel和消息队列很相似
优点:
将生产者和消费者进行了解耦,降低并发当中的耦合
缺点:
容易出现死锁
死锁:
1.单个协程永久阻塞
2.两个或两个以上协程的执行过程中,由于竞争资源或由于彼此通信而造成的一种阻塞的现象
channel死锁场景:
1.非缓存channel读写不能同时
2.缓存channel缓存满时写/缓存空时读
3.多个channel相互等待
1.非缓存channel读写不能同时
package main import "fmt" func main() { ch := make(chan int) ans := <-ch //ch <- 1 fmt.Println(ans) }
package main func main() { ch := make(chan int) //ans := <-ch ch <- 1 //fmt.Println(ans) }
package main import "fmt" func main() { ch := make(chan int) ans := <-ch ch <- 1 fmt.Println(ans) }
package main import "fmt" func main() { ch := make(chan int) ch <- 1 ans := <-ch fmt.Println(ans) }
2.缓存channel缓存满时写/缓存空时读
package main func main() { ch := make(chan int, 1) ch <- 1 ch <- 2 }
package main import "fmt" func main() { ch := make(chan int, 1) fmt.Println(<-ch) }
3.多个channel相互等待
主协程和子协程相互等待导致死锁
package main import "fmt" func main() { ch1 := make(chan int) ch2 := make(chan int) //互相等对方造成死锁 go func() { for { select { case num := <-ch1: fmt.Println("num=", num) ch2 <- 100 } } }() for { select { case num := <-ch2: fmt.Println("num=", num) ch1 <- 200 } } }