问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

简单易懂,解析Go语言中的Channel管道

创作时间:
作者:
@小白创作中心

简单易懂,解析Go语言中的Channel管道

引用
CSDN
1.
https://blog.csdn.net/lonely__snow/article/details/145735224

在Go语言中,Channel是一种用于在goroutine之间传递数据的机制。它不仅提供了线程安全的数据传输方式,还支持阻塞和非阻塞的读写操作。本文将从Channel的初始化、读写操作以及底层实现原理等多个维度,深入解析这一重要的并发控制机制。

Channel 管道

1. 初始化

Channel可以通过var声明为nil管道,也可以使用make函数进行初始化。len()函数用于获取缓冲区中元素的数量,而cap()函数则返回缓冲区的大小。

// 变量声明
var a chan int
// 使用make初始化
b := make(chan int)  // 不带缓冲区
c := make(chan string,2) // 带缓冲区
ch1 := make(chan int) // 0 0
ch2 := make(chan int, 2)// 1 2
ch2 <- 1
fmt.Println(len(ch1), len(ch2), cap(ch1), cap(ch2))

2. 读写操作

使用<-操作符来表示数据流向。当缓冲区满时写入或缓冲区空时读取都会阻塞,直到被其他goroutine唤醒。

a := make(chan int, 3)
a <- 1 // 数据写入管道
<-a    // 管道读出数据

Channel默认是双向可读写的,但也可以在创建函数时指定为单向读或单向写。

func write(ch chan<- int,a int)  {
    ch <- a
    // <- ch  无效运算: <- ch (从仅发送类型 chan<- int 接收)
}
func read(ch <-chan int)  {
    <- ch
    //ch <- 1  无效运算: ch <- 1 (发送到仅接收类型 <-chan int)
}

读写值为nil的管道会导致永久阻塞,触发死锁。

var ch chan int
ch <- 1  // fatal error: all goroutines are asleep - deadlock!
<-ch  	 // fatal error: all goroutines are asleep - deadlock!

读写已关闭的管道:有缓冲区时可以成功读取缓冲区内容,无缓冲区时读取零值并返回false;向已关闭的管道写入数据会触发panic。

ch1 := make(chan int)
ch2 := make(chan int, 2)
go func() {
    ch1 <- 1
}()
ch2 <- 2
close(ch1)
close(ch2)
v1, b1 := <-ch1  //0 false
v2, b2 := <-ch2  //2 true
println(v1, v2, b1, b2)
ch1 <- 1  //panic: send on closed channel
ch2 <- 1  //panic: send on closed channel

3. 实现原理

Channel底层通过环形队列实现其缓冲区功能,同时使用两个等待队列存储被阻塞的goroutine,并通过互斥锁保证并发安全。

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 保护 hchan 中的所有字段,以及阻塞在这个通道上的 sudogs 中的几个字段。
// 在持有此锁时,不要更改另一个 G 的状态(特别是不要使 G 变为可运行状态),
// 因为这可能会与栈收缩操作发生死锁。
lock mutex //互斥锁
}

环形队列通过数组实现,使用sendx和recvx两个指针分别指向写入和读取位置。等待队列遵循先进先出原则,阻塞中的goroutine会被相反的操作依次唤醒。

如果写入时等待接收队列非空(recvq),那么直接将数据给到等待的goroutine,无需经过缓冲区。

select语句可以监控单个或多个管道内是否有数据,有数据时将其读出;如果没有数据也不会阻塞,直接返回。需要注意的是,select的执行顺序是随机的。

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    go write(ch1)
    go write(ch2)
    for {
        select {
        case e := <-ch1:
            fmt.Printf("ch1:%d\n", e)
        case e := <-ch2:
            fmt.Printf("ch2:%d\n", e)
        default:
            fmt.Println("none")
            time.Sleep(1 * time.Second)
        }
    }
}
func write(ch chan<- int) {
    for {
        ch <- 1
        time.Sleep(time.Second)
    }
}

使用for-range循环读取管道时,管道关闭后不会继续读取管道内的数据;而使用普通for循环读取管道时,管道关闭后仍会继续读取管道内的数据,返回一堆零值和false。

func main() {
   ch1 := make(chan int)
   go write(ch1)
   //for e := range ch1 { // 关闭后不会再从管道读取数据
   //	fmt.Print(e)
   //}
   //1111
   
   for { // 关闭后仍在从管道读取数据。返回 零值,false
   fmt.Print(<-ch1)
   }
   //11110000000000000000000000000000000000000.....
}
func write(ch chan<- int) {
   for i := 1; i < 5; i++ {
   ch <- 1
   time.Sleep(time.Second)
   }
   close(ch)
}
© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号