Golang channel底层原理及 select 和range 操作channel用法

Golang通过通信来实现共享内存,而不是通过共享内存而实现通信,通信实际就是借用channel来实现的

channel底层数据结构

type hchan struct {
    qcount   uint           
    dataqsiz uint           
    buf      unsafe.Pointer #是有缓冲的channel所特有的结构,用来存储缓存数据。是个循环链表
    elemsize uint16
    closed   uint32
    elemtype *_type 
    sendx    uint          #buf这个循环链表中的发送或接收的index
    recvx    uint         
    recvq    waitq        #接收(<-channel)或者发送(channel <- xxx)的goroutine抽象出来的结构体(sudog)的队列。是个双向链表
    sendq    waitq
    lock mutex
}

整体结构图:

 

 

channel 发送send(ch <- xxx) 和 接收recv(<- ch)的逻辑

如果要让元素先进先出的顺序进入到buf循环链表中,就需要加锁,hchan结构中本身就携带了一个互斥锁mutex。

当使用send (ch <- xx)或者recv ( <-ch)的时候,先要锁住hchan这个结构体。

具体过程:

加锁->传递数据->解锁

在channel阻塞时,goroutine如何调度

G1
ch := make(chan int,1)
ch <-1
#阻塞
ch <-1

当阻塞的channel再次send数据时,会主动让出调度器,让G1等待,让出M,由其他Groutine去使用,此时G1会被抽象成含有G1指针和send元素的sudog结构体保存到hchan的sendq中等待被唤醒。

当G2执行了recv操作p := <-ch时

G2从缓存队列中取出数据,channel会将等待队列中的G1推出,将G1当时send的数据推到缓存中,然后调用Go的scheduler,唤醒G1,并把G1放到可运行的Goroutine队列中

channel结合select简单使用

所有的case都阻塞时,默认执行default中的值,多个case可以执行时候,随便选择一个case

ch := make(chan int, 1)
for i := 0; i < 10; i++ {
    select {
    case x := <-ch:
        fmt.Println(x) 
    case ch <- i:
    }
}

输出0,2,4,6,8

某个请求时间过程,通过default释放资源

select {
    case <-ch:
        // ch1所在的发送端goroutine正在处理请求
    case <-time.After(2*time.Second):
        // 释放资源,返回请求处理失败的数据,或者先通知用户已处理成功,最终一致性可以保证。
        // 最重要的是快速响应,免得用户看着页面没反应,过多的点击按钮发送请求,会过多消耗服务端的系统资源
}

 Go提供了range关键字,将其使用在channel上时,会自动等待channel的动作一直到channel被关闭,多个goroutine可以借助range操作一个channel

package main

import (
    "fmt"
)

// 开启5个goroutine对channel中的每个值求立方
func oprate(task chan int, exitch chan bool) {
    for t := range task { //  处理任务
        fmt.Println("ret:", t*t*t)
    }
    exitch <- true
}
func main() {
    task := make(chan int, 1000) //任务管道
    exitch := make(chan bool, 5) //退出管道
    go func() {
        for i := 0; i < 1000; i++ {
            fmt.Println("in:", i)
            task <- i
        }
        close(task)
    }()
    for i := 0; i < 5; i++ { //启动5个goroutine做任务
        go oprate(task, exitch)
    }

    for i := 0; i < 5; i++ {
        <-exitch
    }
    close(exitch) //关闭退出管道
}

5个goroutine并发对任务管道的1000个值进行处理,in是按顺序的,ret不一定按顺序

只读或者只写channel并不是存在的,但只是作为一种形式存在着,例如可以在函数的参数里定义参数只读或者只写

package main

import (
    "fmt"
    "time"
)

//只能向chan里写数据
func send(c chan<- int) {
    for i := 0; i < 10; i++ {
        c <- i
    }
}
//只能取channel中的数据
func get(c <-chan int) {
    for i := range c {
        fmt.Println(i)
    }
}
func main() {
    c := make(chan int)
    go send(c)
    go get(c)
    time.Sleep(time.Second*1)
}

对channel读写频率的控制,借助time.Tick(time)实现

复制代码
package main

import (
    "time"
    "fmt"
)

func main(){
    requests:= make(chan int ,5)
    for i:=1;i<5;i++{
        requests<-i
    }
    close(requests)
    limiter := time.Tick(time.Second*1)
    for req:=range requests{
        <-limiter
        fmt.Println("requets",req,time.Now()) //执行到这里,需要隔1秒才继续往下执行,time.Tick(timer)上面已定义
    }
}
//结果:
requets 1 2018-07-06 10:17:35.98056403 +0800 CST m=+1.004248763
requets 2 2018-07-06 10:17:36.978123472 +0800 CST m=+2.001798205
requets 3 2018-07-06 10:17:37.980869517 +0800 CST m=+3.004544250
requets 4 2018-07-06 10:17:38.976868836 +0800 CST m=+4.000533569

如何正确关闭channel?

channel 关闭了接着 send 数据会发生panic,关闭一个已经关闭的 channel 会发生panic

The Channel Closing Principle:在使用Go channel的时候,一个适用的原则是不要从接收端关闭channel,也不要关闭有多个并发发送者的channel。换句话说,如果sender(发送者)只是唯一的sender或者是channel最后一个活跃的sender,那么你应该在sender的goroutine关闭channel,从而通知receiver(s)(接收者们)已经没有值可以读了。维持这条原则将保证永远不会发生向一个已经关闭的channel发送值或者关闭一个已经关闭的channel。

posted @ 2020-08-03 20:12  LeeJuly  阅读(1069)  评论(0编辑  收藏  举报