前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >chan 信道

chan 信道

作者头像
酷走天涯
发布2019-06-11 16:46:53
5380
发布2019-06-11 16:46:53
举报

本节学习

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

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


如何声明信道

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

chan T 表示 T 类型的信道。

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

代码语言:javascript
复制
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)
    }
}

信道也能简短声明

代码语言:javascript
复制
a := make(chan int)

信道如何收发数据

<- 数据操作符

信道发数据 data <- chan

信道接受数据

chan <- data

下面看一个完整的例子

代码语言:javascript
复制
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 协程之间进行高效的通信,不需要用到其他编程语言常见的显式锁或条件变量。

我们用一个例子演示一下

代码语言:javascript
复制
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的圆的面积和周长

代码语言:javascript
复制
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


死锁
代码语言:javascript
复制
package main
func main() {
    ch := make(chan int)
    ch <- 5
}

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

image.png

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

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

看下面的例子

代码语言:javascript
复制
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

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

看下面的例子

代码语言:javascript
复制
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次值,如果输入第八次值,会怎么样?

就会产生死锁

我们在看一个例子

代码语言:javascript
复制
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.在主协程中,只要向协程中写数据,就必须在其它协程中读,并且读的顺序一定要在写的前面,不然对于没有缓冲的信道是写不进去了,在主协程读信道的值,必须在读信道之前,有子协程方法先调用,里面有向信道里面写值的操作,不然主协程会卡主,后面的代码也没有办法执行
单向信道

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

代码语言:javascript
复制
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

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

下面我们看一个例子

代码语言:javascript
复制
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 方法中,不会对信道类型参数,进行读的操作

下面是一个重要的要义

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


如何关闭信道

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

代码语言:javascript
复制
v, ok := <- ch

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

下面看一个实例

代码语言:javascript
复制
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 可以循环监听信道,知道信道信道关闭,才会结束循环

代码语言:javascript
复制
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)发送数据。同样,只有在缓冲为空的时候,才会阻塞从缓冲信道接收数据。

代码语言:javascript
复制
ch := make(chan type, capacity)

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

代码语言:javascript
复制
package main

import (
    "fmt"
)

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

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

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

下面演示一下这个过程

代码语言:javascript
复制
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 函数创建缓冲信道的时候会指定容量大小。

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

代码语言:javascript
复制
func main() {

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

}

image.png

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018.12.24 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是信道?
  • 如何声明信道
  • 信道如何收发数据
  • 死锁
  • 单向信道
  • 如何关闭信道
  • for range
  • 缓冲信道
  • 计算容量和长度
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档