通道

作者: 天命_风流 | 来源:发表于2020-09-10 10:33 被阅读0次

通道是 Go 并发编程中重要的一员

基础知识

  • 通道是 Go 自带的,唯一一个可以满足并发安全性的类型。
  • 声明并初始化一个通道,需要使用 make 函数。
  • 通道相当于一个队列,它是先进先出的。对于通道的输入和输出,需要使用 <- 操作符。
package main

import "fmt"

func main() {
  ch1 := make(chan int, 3)
  ch1 <- 2
  ch1 <- 1
  ch1 <- 3
  elem1 := <-ch1
  fmt.Printf("The first element received from channel ch1: %v\n",
    elem1)
}
  • 在上面的代码中,声明并初始化了一个可以存储 3 个 int 类型的通道,并向通道发送了 2、1、3 三个元素。
  • 你可以使用短变量声明的方式直接为一个变量赋值。如 elem1 := <-ch1

通道的基本特性

  • 对于同一个通道,发送操作之间互斥,接收操作之间也是互斥的。
  • 发送操作和接收操作中对元素的值的处理都是不可分割的。
  • 不论是发送操作还是接收操作,它们在完全完成之间都会被阻塞。

通道之所以有这三个特性,是为了实现操作的互斥和元素值的完整。下面是这些特性中的一些细节:

  • 元素值从外界进入通道时会被复制,也就是说,通道中的元素是赋值元素的副本。
  • “不可分割”的意思是,它们处理元素值是一气呵成的,绝不会被打断。
  • 发送操作包括:复制元素值 和 放置副本到通道 这两个步骤。
  • 接收操作包括:复制通道内的元素、放置副本到接收方 和 删除原值 三个步骤。

通道何时会被阻塞

  • 通道满时,对它的所有发送操作都被阻塞,知道通道中有元素被接收。
  • 通道空的时候,接收操作会被阻塞。注意,阻塞唤醒的顺序也是由协程到来的顺序决定的。(也就是说,所有进入到通道内的元素依然保持 FIFO 的特性)
  • 对于非缓冲的通道(不设置缓冲值),则发送和接收都会被阻塞,直到对方到来。

可能会引发错误的操作

  • 通道是引用类型,所以它的零值是 nil,这种情况下任何发生和接收的操作都会造成永久阻塞,一定要记得使用 make 函数进行初始化。
  • 对已经 close 的通道进行发送操作,会引发 panic。
  • 重复 close 一个已经关闭的通道,会引发 panic。
  • 你可以接收两个值,第二个值如果为 false,代表通道已经关闭,且通道中没有元素了
elem, ok := <-ch2
  • 因为上面的原因,建议不要让接收方关闭通道,而是让发送方关闭。

通道的高级操作

单向通道

  • 你可以在 make 的时候指定通道的类型:
var uselessChan = make(chan<- int, 1)
  • 对于上面的通道,我们只能向通道中发送数据,而不能接收。
  • 单向通道的用处,是约束其他代码的行为,你可以看一下下面的代码:
func SendInt(ch chan<- int) {
  ch <- rand.Intn(1000)
}
  • 在这个函数中,函数能接受一个 chan<- int 类型的参数,这就起到了约束的作用。同样的道理,你可以使用接口来约束相关代码实现:
type Notifier interface {
  SendInt(ch chan<- int)
}
  • 注意,你可以将双向通道作为参数传入,Go 会自动将双向通道转换为函数所需要的单向通道:
intChan1 := make(chan int, 3)
SendInt(intChan1)
  • 返回值也是如此:
func getIntChan() <-chan int {
  num := 5
  ch := make(chan int, num)
  for i := 0; i < num; i++ {
    ch <- i
  }
  close(ch)
  return ch
}

for ... range

  • for 和 range 可以配合通道使用:
intChan2 := getIntChan()
for elem := range intChan2 {
  fmt.Printf("The element in intChan2: %v\n", elem)
}
  • for 会不断从通道中取出元素,直到通道已经关闭 且 通道已经没有元素
  • 如果通道中没有元素且没有被关闭,for 会阻塞。
  • 如果通道为 nil,则会永远阻塞。

select

  • select 只能与通道联合使用,下面是一个例子:
// 准备好几个通道。
intChannels := [3]chan int{
  make(chan int, 1),
  make(chan int, 1),
  make(chan int, 1),
}
// 随机选择一个通道,并向它发送元素值。
index := rand.Intn(3)
fmt.Printf("The index: %d\n", index)
intChannels[index] <- index
// 哪一个通道中有可取的元素值,哪个对应的分支就会被执行。
select {
case <-intChannels[0]:
  fmt.Println("The first candidate case is selected.")
case <-intChannels[1]:
  fmt.Println("The second candidate case is selected.")
case elem := <-intChannels[2]:
  fmt.Printf("The third candidate case is selected, the element is %d.\n", elem)
default:
  fmt.Println("No candidate case is selected!")
}

在使用 select 的时候,你需要注意以下几个事情

  • 如果你设置了 default,可以避免阻塞。
  • 如果没有 default,且所有 case 表达式都没有满足的条件,select 就会被阻塞。知道至少有一个 case 满足条件。
  • 你最好添加对通道是否关闭的判断,并在某个通道已经关闭的时候屏蔽掉它。
  • select 只能对每个 case 求值一次,如果你想持续地操作其中的通道,需要使用 for。
  • 但是,break 只能跳出 select,而不能直接跳出 for,你需要警惕 for 无限运行。

select 语句分支选择规则

  • 在 select 进行选择之前,会对所有的 case 进行扫描,扫描完成后,才会开始选择分支。
  • select 扫描的方式:从上到下,从左到右。
  • 如果一个 case 表达式(这个表达式甚至可以是由函数返回)处于阻塞,在后续的选择分支时就会屏蔽这个 case。
  • 如果一个 select 中有多个 case 符合条件,go 会使用一种伪随机的方式选择一个 case 执行。
  • 一个 select 只能有一个默认分支(default)
  • select 的执行和对 case 表达式的求值和选择都是独立的。但是,它们的执行是否并发安全,需要看一下具体的代码。
  • 下面是一个代码实例:
package main

import "fmt"

var channels = [3]chan int{
    nil,
    make(chan int),
    nil,
}

var numbers = []int{1, 2, 3}

func main() {
    select {
    case getChan(0) <- getNumber(0):
        fmt.Println("The first candidate case is selected.")
    case getChan(1) <- getNumber(1):
        fmt.Println("The second candidate case is selected.")
    case getChan(2) <- getNumber(2):
        fmt.Println("The third candidate case is selected")
    default:
        fmt.Println("No candidate case is selected!")
    }
}

func getNumber(i int) int {
    fmt.Printf("numbers[%d]\n", i)
    return numbers[i]
}

func getChan(i int) chan int {
    fmt.Printf("channels[%d]\n", i)
    return channels[i]
}
// 执行结果:
channels[0]
numbers[0]
channels[1]
numbers[1]
channels[2]
numbers[2]
No candidate case is selected!

解释:1. 在这个 select 中,会从上到下,从左到右依次执行 case 中的表达式,在这个过程中,会触发在函数中的 print 操作。
2.在扫描完成后,发现所有的 case 都是阻塞的。(nil 总是阻塞,没有缓冲区的 chan 在没有数据的时候也是被阻塞的。)
3.最终,select 选择了 default 分支进行执行。

相关文章

  • 通道的运用

    复合通道,颜色通道,专色通道,记录选区阿尔法通道,

  • 通道

    channel通道 关闭通道和通道上范围循环 缓冲通道 定向通道 time包中的通道相关函数 select语句 C...

  • 梦通道,行通道

    09年的诗呢 那时还真有心情 哪怕现在晃荡如我 也是再也没有当时的心情呢 当时那文艺的 文艺女青年是个贬义词,现在...

  • <>

    通道的高级玩法 单向通道 单向通道是只允许写或者读的通道,定义方式如下: 通道的读写 for语句读写 select...

  • 知识人脉积累好,管理也可转专业-171-90-34-1473

    【实操技巧:双通道职业发展通道中,管理怎么转专业? 我们公司有个职业双通道路径,管理通道与专业通道是...

  • Photoshop百种技巧六

    81. 使用通道处理图片中的天空 A: 复制蓝色通道 去通道工具窗口拖动蓝色通道到创建新通道的按钮上,复制它。点击...

  • 通道

    2月11号,也就是今天,我们早上起来后,大概九点左右,我们出发了! 坐上了车,开始了今天的路程,我...

  • 通道

    文/湘邵铁炉 晚上好【今日铁炉说——通道】:今天我所说的这个通道是通往成功的通道,话说“七十...

  • 通道

    又一次走过渐渐熟悉的通道。 傻子,傻子,傻子!要不要走个通道都这么伤感! 到站了,有人下了,我也应该下,可是,我没...

  • 通道

    通道的应用: 创建不可能的选区和蒙版 确定对象和背景——复制反差最强的通道副本——将对象变黑背景变白(色阶吸管、填...

网友评论

      本文标题:通道

      本文链接:https://www.haomeiwen.com/subject/ztcuektx.html