学习笔记-通道

Rob Pike 的至理名言:Don’t communicate by sharing memory; share memory by communicating. (不要经过共享内存来通讯,而应该经过通讯来共享内存。)缓存

容量,就是指通道最多能够缓存多少个元素值,当容量为0时,咱们能够称通道为非缓冲通道,也就是不带缓冲的通道。而当容量大于0时,咱们能够称为缓冲通道,也就是带有缓冲的通道。安全

在声明并初始化一个通道的时候,咱们须要用到 Go 语言的内建函数make,在声明一个通道类型变量的时候,咱们首先要肯定该通道类型的元素类型,好比,类型字面量chan int,其中的chan是表示通道类型的关键字,而int则说明了该通道类型的元素类型。又好比,chan string表明了一个元素类型为string的通道类型。并发

一个通道至关于一个先进先出(FIFO)的队列。也就是说,通道中的各个元素值都是严格地按照发送的顺序排列的,先被发送通道的元素值必定会先被接收。元素值的发送和接收都须要用到操做符<-。咱们也能够叫它接送操做符。一个左尖括号紧接着一个减号形象地表明了元素值的传输方向。异步

在 demo20.go 文件中,我声明并初始化了一个元素类型为int、容量为3的通道ch1,并用三条语句,向该通道前后发送了三个元素值二、1和3。这里的语句须要这样写:
依次敲入通道变量的名称(好比ch1)、接送操做符<-以及想要发送的元素值(好比2),而且这三者之间最好用空格进行分割。ide

当咱们须要从通道接收元素值的时候,一样要用接送操做符<-,只不过,这时须要把它写在变量名的左边,用于表达“要从该通道接收一个元素值”的语义。函数

好比:<-ch1,这也能够被叫作接收表达式。在通常状况下,接收表达式的结果将会是通道中的一个元素值。code

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)
}
go run demo20.go 
The first element received from channel ch1: 2

对通道的发送和接收操做都有哪些基本的特性?

它们的基本特性以下:
对于同一个通道,发送操做之间是互斥的,接收操做之间也是互斥的。
Go 语言的运行时系统(如下简称运行时系统)只会执行对同一个通道的任意个发送操做中的某一个。直到这个元素值被彻底复制进该通道以后,其余针对该通道的发送操做才可能被执行。相似的,在同一时刻,运行时系统也只会执行,对同一个通道的任意个接收操做中的某一个。直到这个元素值彻底被移出该通道以后,其余针对该通道的接收操做才可能被执行。即便这些操做是并发执行的也是如此。这里所谓的并发执行,你能够这样认为,多个代码块分别在不一样的 goroutine 之中,并有机会在同一个时间段内被执行。另外,对于通道中的同一个元素值来讲,发送操做和接收操做之间也是互斥的。例如,虽然会出现,正在被复制进通道但还未复制完成的元素值,可是这时它毫不会被想接收它的一方看到和取走。中间件

这里要注意的一个细节是,元素值从外界进入通道时会被复制。更具体地说,进入通道的并非在接收操做符右边的那个元素值,而是它的副本。队列

另外一方面,元素值从通道进入外界时会被移动。这个移动操做实际上包含了两步,第一步是生成正在通道中的这个元素值的副本,并准备给到接收方,第二步是删除在通道中的这个元素值。内存

发送操做和接收操做中对元素值的处理都是不可分割的。这里的“不可分割”的意思是,它们处理元素值时都是一鼓作气的,毫不会被打断。例如,发送操做要么还没复制元素值,要么已经复制完毕,毫不会出现只复制了一部分的状况。接收操做在准备好元素值的副本以后,必定会删除掉通道中的原值,毫不会出现通道中仍有残留的状况。这既是为了保证通道中元素值的完整性,也是为了保证通道操做的惟一性。对于通道中的同一个元素值来讲,它只多是某一个发送操做放入的,同时也只可能被某一个接收操做取出。

发送操做在彻底完成以前会被阻塞。接收操做也是如此。

通常状况下,发送操做包括了“复制元素值”和“放置副本到通道内部”这两个步骤。在这两个步骤彻底完成以前,发起这个发送操做的那句代码会一直阻塞在那里。也就是说,在它以后的代码不会有执行的机会,直到这句代码的阻塞解除。更细致地说,在通道完成发送操做以后,运行时系统会通知这句代码所在的 goroutine,以使它去争取继续运行代码的机会。另外,接收操做一般包含了“复制通道内的元素值”“放置副本到接收方”“删掉原值”三个步骤。在全部这些步骤彻底完成以前,发起该操做的代码也会一直阻塞,直到该代码所在的 goroutine 收到了运行时系统的通知并从新得到运行机会为止。说到这里,你可能已经感受到,如此阻塞代码其实就是为了实现操做的互斥和元素值的完整。

形成阻塞的状况

先说针对缓冲通道的状况。若是通道已满,那么对它的全部发送操做都会被阻塞,直到通道中有元素值被接收走。这时,通道会优先通知最先所以而等待的、那个发送操做所在的 goroutine,后者会再次执行发送操做。因为发送操做在这种状况下被阻塞后,它们所在的 goroutine 会顺序地进入通道内部的发送等待队列,因此通知的顺序老是公平的。相对的,若是通道已空,那么对它的全部接收操做都会被阻塞,直到通道中有新的元素值出现。这时,通道会通知最先等待的那个接收操做所在的 goroutine,并使它再次执行接收操做。所以而等待的、全部接收操做所在的 goroutine,都会按照前后顺序被放入通道内部的接收等待队列。对于非缓冲通道,状况要简单一些。不管是发送操做仍是接收操做,一开始执行就会被阻塞,直到配对的操做也开始执行,才会继续传递。因而可知,非缓冲通道是在用同步的方式传递数据。也就是说,只有收发双方对接上了,数据才会被传递。而且,数据是直接从发送方复制到接收方的,中间并不会用非缓冲通道作中转。相比之下,缓冲通道则在用异步的方式传递数据。在大多数状况下,缓冲通道会做为收发双方的中间件。正如前文所述,元素值会先从发送方复制到缓冲通道,以后再由缓冲通道复制给接收方。可是,当发送操做在执行的时候发现空的通道中,正好有等待的接收操做,那么它会直接把元素值复制给接收方。以上说的都是在正确使用通道的前提下会发生的事情。下面我特别说明一下,因为错误使用通道而形成的阻塞。对于值为nil的通道,不论它的具体类型是什么,对它的发送操做和接收操做都会永久地处于阻塞状态。它们所属的 goroutine 中的任何代码,都再也不会被执行。注意,因为通道类型是引用类型,因此它的零值就是nil。换句话说,当咱们只声明该类型的变量但没有用make函数对它进行初始化时,该变量的值就会是nil。咱们必定不要忘记初始化通道!

package main

func main() {
    // 示例1。
    ch1 := make(chan int, 1)
    ch1 <- 1
    //ch1 <- 2 // 通道已满,所以这里会形成阻塞。

    // 示例2。
    ch2 := make(chan int, 1)
    //elem, ok := <-ch2 // 通道已空,所以这里会形成阻塞。
    //_, _ = elem, ok
    ch2 <- 1

    // 示例3。
    var ch3 chan int
    //ch3 <- 1 // 通道的值为nil,所以这里会形成永久的阻塞!
    //<-ch3 // 通道的值为nil,所以这里会形成永久的阻塞!
    _ = ch3
}

发送操做和接收操做在何时会引起 panic?

对于一个已初始化,但并未关闭的通道来讲,收发操做必定不会引起 panic。可是通道一旦关闭,再对它进行发送操做,就会引起 panic。
另外,若是咱们试图关闭一个已经关闭了的通道,也会引起 panic。注意,接收操做是能够感知到通道的关闭的,并可以安全退出。更具体地说,当咱们把接收表达式的结果同时赋给两个变量时,第二个变量的类型就是必定bool类型。它的值若是为false就说明通道已经关闭,而且再没有元素值可取了。
注意,若是通道关闭时,里面还有元素值未被取出,那么接收表达式的第一个结果,仍会是通道中的某一个元素值,而第二个结果值必定会是true。
所以,经过接收表达式的第二个结果值,来判断通道是否关闭是可能有延时的。因为通道的收发操做有上述特性,因此除非有特殊的保障措施,咱们千万不要让接收方关闭通道,而应当让发送方作这件事。这在 demo22.go 中有一个简单的模式可供参考。

package main

import "fmt"

func main() {
    ch1 := make(chan int, 2)
    // 发送方。
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Printf("Sender: sending element %v...\n", i)
            ch1 <- i
        }
        fmt.Println("Sender: close the channel...")
        close(ch1)
    }()

    // 接收方。
    for {
        elem, ok := <-ch1
        if !ok {
            fmt.Println("Receiver: closed channel")
            break
        }
        fmt.Printf("Receiver: received an element: %v\n", elem)
    }

    fmt.Println("End.")
}

go run demo22.go Sender: sending element 0...Sender: sending element 1...Sender: sending element 2...Sender: sending element 3...Receiver: received an element: 0Receiver: received an element: 1Receiver: received an element: 2Receiver: received an element: 3Sender: sending element 4...Sender: sending element 5...Receiver: received an element: 4Sender: sending element 6...Sender: sending element 7...Sender: sending element 8...Receiver: received an element: 5Receiver: received an element: 6Receiver: received an element: 7Receiver: received an element: 8Sender: sending element 9...Sender: close the channel...Receiver: received an element: 9Receiver: closed channelEnd.

相关文章
相关标签/搜索