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。
本文来自博客园,作者:LeeJuly,转载请注明原文链接:https://www.cnblogs.com/peterleee/p/13428390.html