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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • select 信道好帮手

    select 语句用于在多个发送/接收信道操作中进行选择。select 语句会一直阻塞,直到发送/接收操作准备就绪。如果有多个信道操作准备完毕,select 会...

    酷走天涯
  • 字符串

    注意 len是按照字节取值的,一个中文字占三个字节,所以这里得到的值是6 ,如果我们想按照字符取值的话,用这个方法就麻烦了,那怎么才能解决这个问题呢?

    酷走天涯
  • 反射

    在学习反射时,所有人首先面临的疑惑就是:如果程序中每个变量都是我们自己定义的,那么在编译时就可以知道变量类型了,为什么我们还需要在运行时检查变量,求出它的类型呢

    酷走天涯
  • Golang学习笔记之并发.协程(Goroutine)、信道(Channel)

    简单的理解一下,并发就是你在跑步的时候鞋带开了,你停下来系鞋带。而并行则是,你一边听歌一边跑步。 并行并不代表比并发快,举一个例子,当文件下载完成时,应该使用弹...

    李海彬
  • Golang之并发篇

    超蛋lhy
  • Python调用Windows API函数控制光驱和系统音量

    Python小屋屋主
  • 使用Java生成比特币钱包地址的过程

    执行上述命令会生成ec-prive.pem文件,将其快速解码为可读的16进制形式。

    fengzhizi715
  • 让莫扎特“续写”披头士的音乐,OpenAI的新AI作曲能力强丨Demo可玩

    它们今天发布了新AI,名叫MuseNet,利用无监督学习的方法,可以用10种不同的乐器来制作时长4分钟的音乐。

    量子位
  • 重温计算机简史:从石头计数到计算机

    大数据文摘
  • Levenshtein Distance(编辑距离)算法与使用场景

    已经很久没深入研究过算法相关的东西,毕竟日常少用,就算死记硬背也是没有实施场景导致容易淡忘。最近在做一个脱敏数据和明文数据匹配的需求的时候,用到了一个算法叫Le...

    Throwable

扫码关注云+社区

领取腾讯云代金券