Go36-10,11-通道

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go36-10,11-通道相关的知识,希望对你有一定的参考价值。

通道

通道(channel)是Go语言的并发编程模式中重要的一员。通道类型的值本身就是并发安全的,这也是Go语言自带的、唯一一个可以满足并发安全性的类型。

使用通道

声明一个通道类型变量,需要确定该通道类型的元素类型,这决定了可以通过这个通道传递什么类型的数据。
初始化通道,需要用到Go语言的内建函数make。make函数除了必须接收类型字面量作为参数,还可以接收一个int类型的参数。第二个参数是可选的,表示通道的容量。
通道的容量,指通道最多可以缓存多少个元素:

  • 当容量为0时,未设置第二个参数也是0,通道为非缓冲通道
  • 当容量大于0时,通过第二个参数指定了容量,通道为缓冲通道

一个通道相当于一个先进先出(FIFO)的队列。
通道中的各个元素值都是严格地按照发送的顺序排列的,先被发送通道的元素值一定会先被接收。
元素值的发送和接收都需要用到操作符<-。可以叫它接送操作符。一个左尖括号紧接着一个减号形象地代表了元素值的传输方向。

package main

import "fmt"

func main() {
    ch1 := make(chan int, 3)
    ch1 <- 1
    ch1 <- 2
    ch1 <- 3
    tmp := <- ch1
    fmt.Println(tmp)
    fmt.Println(<- ch1)
    fmt.Println(<- ch1)
}

通道的特性

通道的基本特性:

  1. 发送操作之间是互斥的,接收操作之间也是互斥的
  2. 发送操作和接收操作中对元素值的处理都是不可分割的
  3. 发送操作在完全完成之前会被阻塞,接收操作也是这样

特性一
在同一时刻,在运行时,系统只会执行对同一个通道的任意个发送操作中的某一个。知道这个元素值被完全的赋值进通道后,其他针对该通道的发送操作才可能被执行。接收操作也是这样。

元素值的复制
元素值从外界进入通道时会被复制。就是说进入通道的并不是那个元素值,而是它的副本。
元素值从通道进入外界是会被移动。这个移动包含2步,先是生成元素值的副本给接收方,然后删除在通道中的这个元素值。

特性二
不可分割的意思,如果是发送操作,就是要复制元素值,一旦开始执行,一定会复制完毕。 不会出现值赋值了一部分的情况。
如果是接收操作,这里有2步,在生成副本后一定会删除掉通道中的元素值。不会出现通道中有残留原来的副本的情况。
这是为了保证通道中元素值的完整性,也是为了保证通道操作的唯一性。

特性三
发送操作包括了“复制元素值”和“放置副本到通道内部”这两个步骤。在这两个步骤完成之前,那句代码会一直阻塞在那里。在通道完成发送操作之后,系统会通知这句代码所在的goroutine,使它可以去争取继续运行代码的机会。
接收操作包括了“复制通道内的元素值”、“放置副本到接收方”和“删掉原值”这三个步骤。完成全部操作执行,同样也是阻塞的。
如此阻塞代码,其实就是为了实现操作的互斥(特性一)和元素值的完整(特性二)。

阻塞的问题

这里分别讲 非缓冲通道 和 缓冲通道 的情况。

缓冲通道
如果通道已满,那么对它的所有发送操作都会被阻塞,直到通道中有元素值被接收走。
对于有多个阻塞的发送操作,会优先通知最早的那个因为通道满了而等待的、那个发送操作所在的goroutinr,于是会在收到通知后再次执行发送操作。由于发送操作在阻塞后,所在的goroutine会顺序进入通道内部的发送等待队列,所以通知的顺序总是公平的。
如果通道已空,那么对它的所有接受操作都会被阻塞,知道通道中有新的元素值出现。这里通道内部也有个接受等待队列,保证通知执行接收操作的顺序。

非缓冲通道
情况简单一些,无论是发送操作还是接收操作,一开始执行就会被阻塞,直到配对的操作也开始执行。

非缓冲通道是在用同步的方式传递数据。就是只有收发双方对接上,数据才会被传送。并且数据是直接从发送方赋值到接收方的,中间不会用非缓冲通道做中转。
缓冲通道则是在用异步的方式传递数据。缓冲通道会作为收发双方的中间件,元素值是先从发送方赋值的缓冲通道,之后再由缓冲通道赋值给接收方。但是,当发送操作在执行的时候发泄空的通道中,正好有等待的接收操作是,会直接把元素复制给接收方。

值为nil的通道
就是未做初始化的通道,对它的发送或接收操作都会永久的处于阻塞状态。这是个错误使用通道而造成阻塞的情况。所以不要忘记初始化通道。

关闭通道

上面说了不要忘记初始化通道。对于已经初始化的通道,收发操作一定不会引发Panic。除非通道关闭,通道一旦关闭,再对它接你发送操作,就会引发Panic。
关闭通道的操作只能执行一次,尝试关闭一个已经关闭了的通道,也会引发Panic。
接收操作,关闭通道后,不会影响接收通道内还未取出的值,也就是说是可以继续取值的。即使值取完了,接收操作也还能一直取到值,不会阻塞,此时取到的是通道内类型的零值:

package main

import "fmt"

func main() {
    ch1 := make(chan int, 3)
    ch1 <- 1
    ch1 <- 2
    ch1 <- 3
    fmt.Println(<- ch1)
    close(ch1)  // 关闭通道不影响取值
    // ch1 <- 4  // 关闭后不能再发送值了
    fmt.Println(<- ch1)
    fmt.Println(<- ch1)
    fmt.Println(<- ch1)  // 取完了,就一直返回零值
    fmt.Println(<- ch1)
    fmt.Println(<- ch1)
}

接收操作,是可以感知到通道关闭的,并能够安全退出。接收表达式可以返回两个变量。第二个变量是bool类型,一般用ok命名。如果返回值为false就说明通道已经关闭了,并且再没有元素值可取了。注意是并且,也就是如果通道关闭,但是里面还有未取出的元素,返回的还是true。可以参考一下如下示例的效果:

package main

import "fmt"

func main() {
    ch1 := make(chan int, 2)
    // 发送方
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println("发送方发送值:", i)
            ch1 <- i
        }
        fmt.Println("发送完毕,关闭通道。")
        close(ch1)
    }()
    // 接收方
    for {
        elem, ok := <- ch1
        if !ok{
            fmt.Println("感知到通道已经关闭")
            break
        }
        fmt.Println("接收方接收值:", elem)
    }
    fmt.Println("结束...")
}

最好不要让接收方来关闭通道,而应当让发送方来关闭。

单向通道

之前说的通道,指的都是双向通道,就是既能发也能收。
单向通道,只能发不能收,或者只能收不能发的通道。定义单向通道的方法:

var c1 = make(chan<- int, 1)  // 只能发不能收
var c2 = make(<-chan int, 1)  // 只能收不能发

只能发不能收的通道,可以简称为发送通道
只能收不能发的通道,可以简称为接收通道

单向通道约束函数行为

通过单向通道可以约束其他代码的行为。下面是对之前的一个例子稍加修改:

package main

import "fmt"

func sender(ch chan<- int) {
    for i := 0; i < 10; i++ {
        fmt.Println("发送方发送值:", i)
        ch <- i
    }
    fmt.Println("发送完毕,关闭通道...")
    close(ch)
}

func reveiver(ch <-chan int) {
    for {
        elem, ok := <- ch
        if !ok {
            fmt.Println("感知到通道已经关闭")
            break
        }
        fmt.Println("接收方接收值:", elem)
    }
}

func main() {
    intChan := make(chan int, 2)
    // 发送方
    go sender(intChan)
    // 接收方
    reveiver(intChan)
    fmt.Println("结束...")
}

这里把之前主函数里的发送方和接收方的代码都封装了一个函数中去了。在函数内部只接收单向通道,这样在封装函数内部,就只能对该通道进行定义的单向操作。在调用函数的时候,仍然是把双向通道传给它。Go语言会自动把双向通道转换为函数所需的单向通道。这样在函数内部该通道就是单向的,但是在函数外部就没有限制。

单向通道约束调用方

下面的例子中定义了函数getIntChan,该函数会返回一个 <-chan int 类型的通道。得到该通道的程序只能从通道中接收元素。这是对函数调用方的一种约束:

package main

import "fmt"

func getIntChan() <-chan int {
    num := 5
    ch := make(chan int, num)
    for i := 0; i < num; i++ {
        ch <- i
    }
    close(ch)
    return ch
}

func main() {
    intChan := getIntChan()
    for elem := range intChan {
        fmt.Println(elem)
    }
}

上面用了for range来遍历通道中的所有元素。

select语句

我们可以使用带range子句的for语句从通道中获取数据,也可以通过select语句操纵通道。

示例

select语句只能与通道联用,它一般由若干个分支组成。由于select语句是专为通道而设计的,所以每个case表达式中都只能包含操作通道的表达式,比如接收表达式。如果还需要把接收表达式赋值给变量的话,可以写成赋值语句或短变量声明:

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func init() {
    rand.Seed(time.Now().UnixNano())
}

func main() {
    // 生成了包括3个通道的数组
    intChannels := [3]chan int{
        make(chan int, 1),
        make(chan int, 1),
        make(chan int, 1),
    }
    // 生成返范围在[0,2]的随机数,效果就是随机选择一个通道
    index := rand.Intn(3)
    fmt.Println("index:", index)
    intChannels[index] <- index
    // 哪个通道中远元素可以取出,那个对应的分支就会被执行
    select {
    case <-intChannels[0]:
        fmt.Println("选中了第一个通道")
    case <-intChannels[1]:
        fmt.Println("选中了第二个通道")
    case elem := <- intChannels[2]:
        fmt.Println("选中了第三个通道,值:", elem)
    default:  // 这个默认分支不会被选中
        fmt.Println("没有选中任何通道")
    }
}

使用select语句的注意点

使用select语句,需要注意一下几点:

  1. 如果加入了默认分支,select语句就不会被阻塞。如果所有通道的表达式都阻塞了,那么就会执行默认分支
  2. 如果没有加入默认分支,那一旦所有的表达式都没有满足求职条件,select就会阻塞。知道至少有一个case表达式满足条件
  3. 可能会因为通道关闭了,而直接从通道接到到一个元素类型的零值。这时候就需要通过表达式的第二个返回值来判断通道是否关闭,一旦发现某个通道关闭了,就应该及时屏蔽掉对应的分支或者采取别的措施。
  4. select语句只能对其中的每一个case表达式各求值一次。如果想连续的或是定时的操作其中的通道,需要通过for循环嵌入select来实现。这样的话要主要,简单的在select里使用break,只能结束当前的select,不会退出for循环。

下面是通过表达式的第二个返回值判断通道关闭的示例:

package main

import (
    "fmt"
    "time"
)

func main() {
    intChan := make(chan int, 1)
    // 3秒后关闭通道
    time.AfterFunc(time.Second * 3, func() {
        close(intChan)
    })
    select {
    case _, ok := <- intChan:
        if !ok {
            fmt.Println("通道已经关闭")
            break
        }
        fmt.Println("通道里有值传入")
    }
}

select语句的分支选择的规则

规则如下所示:

  1. 当case表达式被求值是,如果包含多个表达式,总会按照从左到有的顺序被求职
  2. 所有case表达式都会被求职,并且是按从上到下的顺序。结合上面一条,就是先从左到右,再从上到下对所有的表达式求职
  3. 如果是发送表达式或者接受表达式,在被求值时处于阻塞状态就认为是求职不成功,也就是case表达式所在的分支不满足选择条件
  4. 仅当select语句里所有的case表达式都被求职完毕后,才会开始选择候选分支。如果所有分支都不满足,那就选默认分支。如果没有默认分支,就阻塞,直到至少有一个候选分支返回条件为止。
  5. 如果有多个满足条件的分支,那么会有一种伪随机算法选择其中一个分支并执行。
  6. 一条select语句只能由一个默认分支。默认分支只在无候选分支是才会被执行,与它的编写位置无关。
  7. 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]
}
/* 执行结果
PS H:\Go\src\Go36\article11\example05> go run main.go
channels[0]
numbers[0]
channels[1]
numbers[1]
channels[2]
numbers[2]
No candidate case is selected!
PS H:\Go\src\Go36\article11\example05>
*/

以上是关于Go36-10,11-通道的主要内容,如果未能解决你的问题,请参考以下文章

Go 通道 vs Java BlockingQueue

Go语言之通道

023_go语言中的通道

024_go语言中的缓冲通道

Go 通道:从通道中消费数据,但不向通道推送任何内容

如何解决 Go 通道死锁?