go语言学习笔记(十)——channel

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/sinat_32023305/article/details/82493263
  • 介绍

channel作为goroutine间通信和同步的重要途径,是Go runtime层实现CSP并发模型重要的成员。channel 提供了一种通信机制,通过它,一个 goroutine 可以想另一 goroutine 发送消息。

  • 初始化

在声明并初始化一个通道的时候,我们需要用到Go语言的内建函数make。我们传给make的第一个参数是代表了通道的具体类型的类型字面量。如例

ch := make(chan int)
ch1 := make(chan int,100)//容量100

首先要确定该通道类型的元素类型,这决定了我们可以通过这个通道传递什么类型的数据。如例,类型字面量chan int,其中chan是表示通道类型的关键字,而int则说明了该通道类型的元素类型,只能传递int类型的数据

单向通道:ch := make(chan<-int,1)

<-紧挨关键字chan左边表示了这个通道是单向的,只能收不能发

这种单向通道主要的用途就是约束其他代码的行为

  • 类型

在初始化通道时,还可以声明该通道的容量。所谓通道的容量就是指通道最多可以缓存多少个元素值。(如上ch1)

当容量为0时,我们可以称通道为非缓冲通道。

当容量大于0时,我们可以称为缓冲通道。

  • 使用

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

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 is: %v\n", elem1)
}

该示例中初始化了一个元素类型为int类型,容量为3的通道ch1,其中使用接送操作符“<-”向ch1先后发送了2、1/3三个元素值。

语句“:<-ch1”是接受表达式,通常情况下结果是通道中的一个元素值,该例中elem1将最先进入ch1的元素2接受并存储,输出结果:

The first element received from channel ch1 is: 2

注:上述为缓冲channel的使用,使用非缓冲channel时,由于非缓冲channel的容量为0,所以必须至少有两个goroutine执行,一个收一个发。其实在真实的程序中缓冲channel程序使用中,也需要由不同的goroutine去执行,最好不要将channel当作队列在单个goroutine中执行

  • 发送和接收特性

基本特性如下:

1、同一个通道里,发送操作之间互相排斥,接受操作之间互相排斥;

2、发送操作和接受操作中对元素值的处理都是不可分割的(一对一);

3、发送操作和接受操作在各自完全完成之前,都会被阻塞。

详细解释:

1,在同一时刻,Go语言的运行时系统只会执行对同一个通道的任意个发送(接收)操作中的某一个。直到这个元素值完全被移出该通道之后,其他针对该通道的接收操作才可能被执行。

对于通道中的同一个元素来说,发送和接收操作之间也是互斥的。虽然会出现正在被复制进通道但是还未完全复制进去的元素值,这时它是绝对不会让想接收它的一方看到和取走。

元素从外界进入通道时会被复制,更具体说进入通道的是该元素值的副本。元素值从通道进入外界时会被移动,第一步是生成正在通道中的这个元素值的副本,并准备给到接收方,第二步是删除在通道中的这个元素值。

2,以上的这些操作都是一气呵成的,绝不会被打断,不会出现类似发送操作复制元素值复制了一半,通道残留上一个元素的这些情况

3,发送操作包括了“复制元素值”和“放置副本到通道内部”两步。在这两步全部完成之前,发送操作的代码会一致阻塞在那里,不会让其后的代码执行。

另外,接收操作包含了“复制通道内的元素值”、放置副本到接收方、删除原值三个步骤。在所有这些步骤全部完成之前,发起该操作的代码会一直被阻塞,直到该代码所在的goroutine(go程)收到了runtime系统的通知并重新获得运行机会为止。

如此阻塞代码,其实是为了实现操作的互斥和保证元素值的完整

  • channel阻塞

先针对缓冲通道的情况(容量大于0)。

如果通道已满,那么所有其他发送操作会被阻塞,知道通道中所有元素值被取走。这时,通道会优先通知最早那个发送操作所在的goroutine,后者会再次执行发送操作。该发送操作被阻塞后,所在的goroutine会顺序地进入通道内部的发送等待队列,保证了通知顺序的公平性。

如果通道已空,那么针对该通道的所有接收操作都会被阻塞,直到通道中有新的元素值出现,这时,通道会通知最早等待的那个接收操作所在的goroutine,使他再次执行接收操作。

所以,在缓冲通道中等待接收操作的所有goroutine,都会按照先后顺序被放入通道内部的接收等待队列。

对于非缓冲通道(容量等于0)

无论是发送操作还是接收操作,一开始执行就会被阻塞,知道配对的操作也开始执行。也就是说,非缓冲通道是在用同步的方式在传递数据,只要收发双方对接上了,数据才会被传递。

而缓冲通道则是在用异步的方式传递数据,类似于作为收发双方的中间件,元素值先从发送方复制到缓冲通道,之后再由缓冲通道复制给接收方。但是,当发送操作在执行时,如果发现空的通道中正好有等待的操作,那么就会直接把元素值复制给接收方(不通过缓冲)。

对于值为nil的通道

不论nil通道的具体类型是什么,对它的发送操作和接收操作都会永久地处于阻塞状态,它们所属的goroutine代码不会再被执行。

注意:由于通道类型是引用类型,所以它的零值就是nil。当我们只声明该类型的变量(var ch chan int)但没有用make函数进行初始化,该变量的值就是nil,所以,我们一定不要忘记初始化通道(channel)。

  • Close channel引起panic 

以下操作会引起panic

  • P1. Closing the nil channel.

实例:

func C1P1() {
    var ch chan int
    close(ch)
}
  • P2. Closing a closed channel.

实例:

func C1P2() {
    ch := make(chan int, 0)
    close(ch)
    close(ch)
}
  • P3. Sending on a closed channel.

实例:

func C1P3() {
    ch := make(chan int, 0)
    close(ch)
    ch <- 2
}
  • 联用select

select语句只能与通道联用,select语句分支分为两种,一种叫做候选分支,以关键字case开头,按情况执行。一种叫做默认分支,以关键字default:开头,当且仅当没有候选分支被选中的时候才会被执行。

每个case表达式中都只能包含操作通道的表达式。例

// 示例1。
func example1() {
	// 准备好几个通道。
	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语句的时候的注意事项:

1,如果加入了默认分支,那么无论case表达式是否有阻塞。select语句都不会被阻塞,默认分支会被选中执行

2,如果没有加入默认分支,那么一旦case表达式都不满足,select语句就会一直阻塞,直到至少有一个case满足条件为止

3,当通道关闭了会直接从通道接收到一个其元素类型的零值,所以可以通过接收表达式第二个结果值来判断通道是否关闭,一旦发现某个通道关闭,可以及时处理

4,select语句只能对其中的每一个case表达式各求值一次。如果要连续或定时地操作其中某个通道的话,可以通过在for语句中嵌入select语句的方式实现。

select语句的分支选择规则:

1,每一个case表达式可能包含多个表达式,包含的多个表达式总会从左到右的顺序被执行

2,select包含的case表达式都会在该语句执行开始时先被求值,并且求值的顺序为从上到下

3,每个case表达式,如果在被求值时,相应的操作正处于阻塞状态,那么对该case表达式的求值就是不成功的,也即该case表达式所在的候选分支不满足选择条件。

4,仅当select语句中的所有case表达式都被求值后,它才会开始选择候选分支。如果所有的候选分支都不满足选择条件,默认分支会被执行。如果没有默认分支,那么就会立即进入阻塞状态,直到至少有一个候选分支满足条件为止

5,如果select语句发现同时有多个候选分支满足选择条件,那么它会用一种伪随机的算法在这些分支中选择一个执行。

即使select语句是在被唤醒时发现的这种情况也会这样选择。

6,一条select语句只能有一个默认分支(default),与编写位置无关。

7,每次执行,包括case表达式和分支选择都是独立的,但是不一定并发安全,需要看其中的case表达式以及分支中,是否包含并发不安全的代码。

思考题

1、通道的长度代表着什么?它在什么时候会跟通道的容量相同?

答:长度代表通道当前包含的元素个数,容量就是初始化时你设置的那个数

2、元素值在经过通道传递时会被复制,那么这个复制是浅表复制还是深层复制呢?

答:浅表复制,Go语言里没有深拷贝。

3、如果在select语句中发现某个通道已关闭,那么应该怎样屏蔽它所在的分支?

答:发现某个channel被关闭后,为了防止再次进入这个分支,可以把这个channel重新赋值成为一个长度为0的非缓冲通道,这样这个case就一直被阻塞了

4、在select语句与for语句联用时,怎样直接退出外层的for语句?

答:可以用 break和标签配合使用,直接break出指定的循环体,或者goto语句直接跳转到指定标签执行
 

猜你喜欢

转载自blog.csdn.net/sinat_32023305/article/details/82493263