chan 信道

本节学习

  • 什么是信道?
  • 如何声明信道?
  • 信道如何收发数据?
  • 什么是死锁?
  • 什么是单向信道?
  • 如何关闭信道?
  • 使用 for range 遍历信道
  • 如何缓冲信道
  • 计算信道的容量和长度

什么是信道?

信道是实现 Go 协程间的通信的桥梁,信道可以想像成 Go 协程之间通信的管道。如同管道中的水会从一端流到另一端,通过使用信道,数据也可以从一端发送,在另一端接收。


如何声明信道

所有信道都关联了一个类型。信道只能运输这种类型的数据,而运输其他类型的数据都是非法的。

chan T 表示 T 类型的信道。

信道的零值为 nil。信道的零值没有什么用,应该像对 map 和切片所做的那样,用 make 来定义信道

package main

import "fmt"

func main() {  
    var a chan int // 声明信道 零值
    if a == nil {
        fmt.Println("channel a is nil, going to define it")
        a = make(chan int) 
        fmt.Printf("Type of a is %T", a)
    }
}

信道也能简短声明

a := make(chan int)

信道如何收发数据

<- 数据操作符

信道发数据
data <- chan

信道接受数据

chan <- data

下面看一个完整的例子

package main

import "fmt"

func main() {

  a := 12
  b := make(chan int,1)
  b <- a
  c :=<- b
  fmt.Println(c)
  fmt.Println(b)

}
image.png
  • 发送与接收默认是阻塞的。

这是什么意思?当把数据发送到信道时,程序控制会在发送数据的语句处发生阻塞,直到有其它 Go 协程从信道读取到数据,才会解除阻塞。与此类似,当读取信道的数据时,如果没有其它的协程把数据写入到这个信道,那么读取过程就会一直阻塞着。

信道的这种特性能够帮助 Go 协程之间进行高效的通信,不需要用到其他编程语言常见的显式锁或条件变量。

我们用一个例子演示一下

package main

import (
    "fmt"

)

func read(num chan int){
    fmt.Println(num) // 2
    num <- 12
    time.Sleep(100 * time.Millisecond)
    fmt.Println("可能不会执行") // 4
}

func main() {

    done := make(chan int)
    go read(done)
    fmt.Println("马上要开始等待了") // 1
    b := <-done
    fmt.Printf("阻塞接受了值 %d",b) // 3

}

代码执行顺序 1 - 2 - 3 - 4(已经不执行了)


image.png

注意以上日志输出顺序 1出为甚么先执行,由于go 是并发的,所以1处不会等待read执行完毕就已经开始执行了,但是 b := <-done 是阻塞接受的,当1执行完毕时,当前的协程就卡主了,当read中的num <- 12 只要执行完毕,不管后面有没有语句,b :<- done 立马就开始接受数据,此时3处就已经执行了,4处的代码就不会值了

注意如果我们 将 b := <-done 改为 <-done 是完全合法的 此操作 是从信道中取出值

下面演示一个协程的核心用法,多协程协同工作

求半径为r的圆的面积和周长

package main

import (
    "math"
    "fmt"
)

func calculateArea(r float64,area chan float64) {
    area <- r * r * math.Pi

}

func calculateLength(r float64, length chan float64) {
    length <- r * 2 * math.Pi

}

func main() {
area := make(chan float64)
length := make(chan float64)

// 多协程并发计算
go calculateArea(3.0,area)
go calculateLength(3.0,length)

// 等待计算结果
 a := <- area
 b := <- length
 fmt.Println(a)
 fmt.Println(b)
}
image.png

死锁

package main
func main() {
    ch := make(chan int)
    ch <- 5
}

由于没有其他协程接受数据,所以就产生了死锁

image.png
import "fmt"
func read(data *chan int){
    b := <- *data
    fmt.Println(b)
}
func main() {
    ch := make(chan int)
    go read(&ch) // 这个协程在等待接受数据
    ch <- 5
}

由于 read 协程等待接受数据,所以就不会产生死锁

看下面的例子

package main

import (
    "fmt"
    "time"
)

func read(data *chan int,num int){
    b := <- *data
    fmt.Println(b)
    fmt.Println(num)
}

func main() {
    ch := make(chan int)
    go read(&ch,1) // 这个协程在等待接受数据
    go read(&ch,2) // 这个协程在等待接受数据
    go read(&ch,3) // 这个协程在等待接受数据
    ch <- 5
    time.Sleep(1000 * time.Millisecond)
}
image.png

等待的协程会产生竞争,夺取这个数据,一旦有协程拿到这个数了,信道中的数据就会被销毁,其他协程会继续在哪里等待,知道有新的数据到信道里面

看下面的例子

package main

import (
    "fmt"
    "time"
)

func read(data *chan int,num int){
    b := <- *data
    fmt.Printf("%d-%d\n",num,b)
}

func main() {
    ch := make(chan int)
    go read(&ch,0) // 这个协程在等待接受数据
    go read(&ch,1) // 这个协程在等待接受数据
    go read(&ch,2) // 这个协程在等待接受数据
    go read(&ch,3) // 这个协程在等待接受数据
    go read(&ch,4) // 这个协程在等待接受数据
    go read(&ch,5) // 这个协程在等待接受数据
    go read(&ch,6) // 这个协程在等待接受数据
    
    ch <- 5
    ch <- 4
    ch <- 3
    ch <- 2
    ch <- 1
    ch <- 0
    ch <- 10
    
    time.Sleep(1000 * time.Millisecond)
}
image.png

注意 注意 我们向通道输送数据的顺序和其它协程接受数据的循序,发送数据肯定是顺序发送,因为在一个协程中,但是接受数据的顺序是在不同协程,所以这个我们没法控制

7 个子协程在等待数据,我们给信道输入了7次值,如果输入第八次值,会怎么样?

就会产生死锁

我们在看一个例子

package main

import (
    "fmt"

    "time"
)

func read(data *chan int,num int){
    b := <- *data
    fmt.Println(num)
    fmt.Printf("%d-%d\n",num,b)
}

func main() {
    ch := make(chan int)
    go read(&ch,0) // 这个协程在等待接受数据
    go read(&ch,1) // 这个协程在等待接受数据
    go read(&ch,2) // 这个协程在等待接受数据
    go read(&ch,3) // 这个协程在等待接受数据
    go read(&ch,4) // 这个协程在等待接受数据
    go read(&ch,5) // 这个协程在等待接受数据
    go read(&ch,6) // 这个协程在等待接受数据

    ch <- 6
    time.Sleep(300)
    ch <- 5
    time.Sleep(300)
    ch <- 4
    time.Sleep(300)
    ch <- 3
    time.Sleep(300)
    ch <- 2
    time.Sleep(300)
    ch <- 1
    time.Sleep(300)
    ch <- 0
    time.Sleep(1000 * time.Millisecond)

}
image.png

从这张图中可以总结如下规律

  • 1.读取信道数据的顺序,与协程等待的顺序没有关系
  • 2.协程就算优先获取信道里的数据,但是由于go的并发性,它处理数据也可能落后于其他协程
  • 3.在主协程中,只要向协程中写数据,就必须在其它协程中读,并且读的顺序一定要在写的前面,不然对于没有缓冲的信道是写不进去了,在主协程读信道的值,必须在读信道之前,有子协程方法先调用,里面有向信道里面写值的操作,不然主协程会卡主,后面的代码也没有办法执行

单向信道

我们目前讨论的信道都是双向信道,即通过信道既能发送数据,又能接收数据。其实也可以创建单向信道,这种信道只能发送或者接收数据

package main

import "fmt"

func sendData(sendch chan<- int) {  
    sendch <- 10
}

func main() {  
    sendch := make(chan<- int) // 定义一个只能写的信道
    go sendData(sendch)
    fmt.Println(<-sendch)
}
image.png

这里你会疑问,如果只定义一个只能写的信道有什么意义

下面我们看一个例子

package main

import "fmt"

func sendData(sendch chan<- int) {
    sendch <- 10
}

func main() {
    ch := make(chan int) // 定义一个只能写的信道
    go sendData(ch)
    a := <- ch
    fmt.Println(a)
}

sendData 的参数是一个只能写的信道类型,这样就能限制sendData 方法中,不会对信道类型参数,进行读的操作

下面是一个重要的要义

不管是读通道 还是写 通道,都不能转换成双向通道,但是双向通到是可以转换成单向通道的


如何关闭信道

当从信道接收数据时,接收方可以多用一个变量来检查信道是否已经关闭

v, ok := <- ch

如果成功接收信道所发送的数据,那么 ok 等于 true。而如果 ok 等于 false,说明我们试图读取一个关闭的通道。从关闭的信道读取到的值会是该信道类型的零值。

下面看一个实例

package main

import (
    "fmt"
    "time"
)

func read(num <-chan bool) {
   v,ok := <- num
   if(ok){
       fmt.Println(v)
   }else{
       fmt.Println("信道关闭了")
   }
}

func main() {
    ch := make(chan bool) // 定义一个只能写的信道
    go read(ch)
    close(ch)
    ch <- true

    time.Sleep(time.Millisecond * 1000)
}
image.png

注意 信道关闭后,就不能向信道里面输送值了,不然出抛出一个panic


for range

for range 可以循环监听信道,知道信道信道关闭,才会结束循环

package main

import (
    "fmt"
    )


func getLess5(num chan int) {
  for i := 0 ;i < 5; i++{
      num <- i
  }
  close(num)
}


func main() {
    ch := make(chan int)

    // 在一个协程中 获取小于5的数字
    go getLess5(ch)
    count := 0

    // 循环接受 信道里面的新值 直到close 关闭
    for v := range ch{
        count += v
    }
    fmt.Println(count)
}
image.png

缓冲信道

无缓冲信道的发送和接收过程是阻塞的,

我们还可以创建一个有缓冲(Buffer)的信道。只在缓冲已满的情况,才会阻塞向缓冲信道(Buffered Channel)发送数据。同样,只有在缓冲为空的时候,才会阻塞从缓冲信道接收数据。

ch := make(chan type, capacity)

缓冲信道capacity 表示缓冲信道的容量

package main

import (
    "fmt"
)

func main() {
    ch := make(chan string, 2)
    ch <- "naveen"
    ch <- "paul"
    fmt.Println(<- ch)
    fmt.Println(<- ch)
}

代码不会发生任何阻塞,什么时候会阻塞呢?

写入的时候,当缓冲区满的时候会阻塞,读取的时候,当缓冲区为空的时候,会阻塞

下面演示一下这个过程

package main

import (
    "fmt"
    "time"
)

func write(ch chan  int){
    for i := 0; i < 5 ;i++{
        ch <- i
        fmt.Printf("写入数据-%d\n",i)
    }
    close(ch)
}

func main() {
    ch := make(chan int, 2)
    go write(ch)
    time.Sleep(time.Second) // 1 延时函数
    for v:= range  ch{
        fmt.Printf("读取数据-%d\n",v)
        time.Sleep(time.Second)
    }
}
image.png

为什么加延时函数,不加延时函数,由于系统是并发的整个过程没法看清楚,

代码执行顺序

1.并发执行 go write(ch) 和 延时函数,延时函数,没有执行完毕,之前 write函数,向信道写入两次数据,之后由于信道的容量已经满了,所以不再向信道写入数据了

  1. 延时函数执行完毕后,for range 开始执行,这个时候,开始从信道读取数据,当读取一个数据后,信道的缓冲有多了1个单元
    3.write 函数,可以向信道里面写入数据了,写入完成后,信道缓冲又满了, 此时继续等待
    4.rang中的延时结束之后,就可以继续读取信道里面的值了,此过程循环,直到程序结束

计算容量和长度

缓冲信道的容量是指信道可以存储的值的数量。我们在使用 make 函数创建缓冲信道的时候会指定容量大小。

缓冲信道的长度是指信道中当前排队的元素个数。

func main() {

    ch := make(chan int,3)
    ch <- 1
    fmt.Println(len(ch))
    fmt.Println(cap(ch))

}
image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 224,383评论 6 522
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 96,028评论 3 402
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 171,486评论 0 366
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 60,786评论 1 300
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 69,793评论 6 399
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 53,297评论 1 314
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 41,685评论 3 428
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 40,655评论 0 279
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 47,192评论 1 324
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 39,223评论 3 345
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 41,343评论 1 354
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,966评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,656评论 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 33,130评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 34,257评论 1 275
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,869评论 3 381
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 46,407评论 2 365

推荐阅读更多精彩内容

  • 你的第一个协程 输出结果 从本质上讲,协同程序是轻量级的线程。它们是与发布 协同程序构建器一起启动的。您可以实现相...
    十方天仪君阅读 2,525评论 0 2
  • Go语言并发 Go 是并发式语言,而不是并行式语言。 并发是指立即处理多个任务的能力。 Go 编程语言原生支持并发...
    kakarotto阅读 1,894评论 0 7
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,290评论 11 349
  • 第一章 我是彼岸花 “盛开的彼岸花,请你带他们回家。” 声断,只见一朵红色小花化作一位红衣女子,这位红衣女子走过...
    拾郴阅读 392评论 2 2
  • 数据分析 | 易观数据,能提供哪些数据? 这些数据,有哪些用途? 打开易观千帆的官网:http://qianfan...
    张云钱阅读 7,141评论 1 2