channel用于携程中的通信
go中的channel是一个队列,遵循先进先出的原则,负责携程之间的通信(go提倡不要通过共享内存来通信,而是要通过通信来实现内存共享),CSP并发模型就是通过goroutine和channel来实现的
使用场景
停止信号监听
定时任务
生产方和消费方解耦
控制并发数
底层数据结构
通过var声明或者make函数创建的channel变量是一个存储在函数栈帧上的指针,占用8个字节,指向堆上的hchan结构体,该结构体在src/runtime/chan.go中
buf就是一个缓冲的数组,如果是一个有缓冲的数据,就会使用这个数组,使用循环数组的时候就是消费数据时,其他的元素不需要一定,通过两个下标就可以搞定了
sendq和recvq就是通道在读取或者写入数据的时候,会发生堵塞,所以需要两个队列
lock锁,因为线程是安全的,所以需要一把锁
为什么设计成线程安全?
不同协程通过channel进行通信,本身的使用场景就是多线程,为了保证数据的一致性,必须实现线程安全
如何实现线程安全的?
channel的底层实现中,hchan结构体中采用Mutex锁来保证数据读写安全。在对循环数组buf中的数据进行入队和出队操作时,必须先获取互斥锁,才能操作channel数据
channel如何控制goroutine并发执行顺序
多个goroutine并发执行时,每一个goroutine抢到处理器的时间点不一致,gorouine的执行本身不能保证顺序。即代码中先写的gorouine并不能保证先执行
思路:使用channel进行通信通知,用channel去传递信息,从而控制并发执行顺序
channel共享内存有什么优劣势
优点:使用 channel 可以帮助我们解耦生产者和消费者,可以降低并发当中的耦合
缺点:容易出现死锁的情况
Go channel发送和接收什么情况下会死锁
死锁
- 单个协程永久阻塞
- 两个或两个以上的协程的执行过程中,由于竞争资源或由于彼此通信而造成的一种阻塞的现象
channel死锁场景
- 非缓存channel只写不读
- 非缓存channel读在写后面
- 缓存channel写入超过缓冲区数量
- 空读
- 多个协程互相等待
创建时的策略
- 如果是无缓冲的channel,会直接给hchan分配内存
- 如果是有缓冲的channel,并且元素不包含指针,那么会为hchan和底层数组分配一段连续的地址
- 如果是有缓冲的channel,并且元素包含指针,那么会为hchan和底层数组分别分配地址
向channel中发送数据时
- 如果channel的读等队列存在接受者goroutine
将数据直接发送给第一个等待的goroutine,唤醒接收的goroutine - 如果channel的读等队列不存在接受者goroutine
- 如果循环数组的buf未满,那么将数据发送到循环数组的队尾
- 如果循环数组的buf已满,将当前的goroutine加入写等待对列,并挂起等待唤醒接收
向channel中接收数据时
-
如果channel的写等待队列存在发送者goroutine
- 如果是无缓冲channel,直接从第一个发送者goroutine那里把数据拷贝给接收变量,唤醒发送的gorontine
- 如果是有缓冲channel(已满),将循环数组buf的队首元素拷贝给接受变量,将第一个发送者goroutine的数据拷贝到循环数组队尾,唤醒发送到goroutine
-
如果channel的写等待队列不存在发送者goroutine
- 如果循环数组buf非空,将循环数据buf的队首元素拷贝给接受变量
- 如果循环数组buf为空,这个时候就会走阻塞接收的流程,将当前goroutine加入读等队列,并挂起等待唤醒
无缓冲channel的定义与使用
package main
import (
"fmt"
)
func main() {
// 定义一个无缓冲的channel
c := make(chan int)
// 定义一个G1
go func(){
defer fmt.Println("G1 结束")
fmt.Println("G1正在运行")
// 将666 发送给c
c <- 666
}()
// 从c中将666取出并赋值给num
num := <-c
fmt.Println("num == ", num)
fmt.Println("main goroutine结束。。。。。。")
}
有缓冲channel的定义与使用
package main
import (
"fmt"
"time"
)
func main() {
// 定义一个有缓冲的channel
c := make(chan int, 3)
fmt.Println("len(c) = ", len(c), "cap(c) = ", cap(c))
// 定义一个G1
go func(){
defer fmt.Println("G1 结束")
for i := 0; i < 3; i++ {
c <- i
fmt.Println("G1正在运行", "len(c) = ", len(c), "cap(c) = ", cap(c))
}
}()
// 确保元素全部放入通道
time.Sleep(time.Second*2)
for i := 0; i < 3; i++ {
num := <-c
fmt.Println("num is ", num)
}
fmt.Println("main goroutine结束。。。。。。")
}
通道的关闭close
package main
import (
"fmt"
)
func main() {
// 定义一个无缓冲的channel
c := make(chan int)
// 定义一个G1
go func(){
defer fmt.Println("G1 结束")
for i := 0; i < 3; i++ {
c <- i
}
// close可以关闭一个channel
close(c)
}()
for {
// ok如果为true表示channel没有关闭,反之则表示关闭
if data, ok := <-c; ok {
// 如果上面没有close则会死锁
fmt.Println(data)
}else{
break
}
}
fmt.Println("main goroutine结束。。。。。。")
}
使用range来取数据
package main
import (
"fmt"
)
func main() {
// 定义一个无缓冲的channel
c := make(chan int)
// 定义一个G1
go func(){
defer fmt.Println("G1 结束")
for i := 0; i < 3; i++ {
c <- i
}
// close可以关闭一个channel
close(c)
}()
// 可以使用range来迭代不断操作channel
for data := range c{
fmt.Println(data)
}
fmt.Println("main goroutine结束。。。。。。")
}
select监控多个channel的状态
package main
import (
"fmt"
)
func fibonacii(c,quit chan int) {
x, y := 1, 1
for{
select {
// 如果c通道能够放入数据则进入
case c <- x:
x = y
y = x +y
// 如果quit通道能够取出数据
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
// sub go
go func() {
for i := 0; i < 10; i++ {
// 从c通道中读取数据
fmt.Println(<-c)
}
// 往quit通道中放入数据
quit <-0
}()
// main go
fibonacii(c,quit)
fmt.Println("主携程结束。。。。。。")
}