文章目录
- chan基础使用和理解
- 通道模型:单通道、双通道
- 双向通道
- 单向通道
- 单向通道的作用
- 缓冲通道和非缓冲通道数据发送和接收过程
- 缓冲通道
- 非缓冲通道
- 通道基本特性
- 通道何时触发panic
- Channel和Select结合使用
- Select语句和通道的关系
- Select语句的分支选择规则有那些
- Select和Channel结合使用案例一
- Select和Channel结合使用案例二
- Select和for联合使用时如何退出最外层循环
在前面学习中了解到对于单值变量,如:int、string;多值变量,如:map存在多协程对资源竞争的并发问题,为了解决并发性通常需要引入sync.Mutex解决。 通道类型本身就是并发安全的,是Go语言自带的、唯一一个可以满足并发安全性的类型。
chan基础使用和理解
package main
import (
"fmt"
)
func main() {
// 使用make声明并初始化一个int型的带缓冲的通道,并将其容量设置为3
ch1 := make(chan int, 3)
// 使用make声明并初始化一个int型的不带缓冲的通道,其容量为0
ch2 := make(chan int)
// 声明一个int型的通道
var ch3 chan int
// 使用接送操作符 <- 向通道ch1中发送int型数据1
go func(){
ch1 <- 1
ch1 <- 2
ch1 <- 3
}()
// 使用接送操作符 <- 从通道ch1中读取数据,下面的短变量表达式左边有两个变量num, ok:其中ok用于判断通道ch1是否关闭,
// 当ok == ture时表示通道没有关闭,可以读取数据并将其保存到变量nums中,当ok == false时表示通道关闭,不能读取数据。
// 此外读取通道中的数据,可以直接使用num := <- ch1进行读取,不添加第二个判断通道是否关闭的条件,此时有风险存在
num, ok := <-ch1
if ok {
fmt.Println(num)
}
close(ch1)
close(ch2)
// close(ch3)
}
对于通道的基本声明方式有三种:声明并初始化带缓冲的通道(ch1);声明并初始化一个不带缓冲的通道(ch2);仅仅声明一个通道(ch3)
什么是通道:一个通道相当于一个先进先出(FIFO)的队列。也就是说,通道中的各个元素值都是严格地按照发送的顺序排列的,先被发送通道的元素值一定会先被接收。元素值的发送和接收都需要用到操作符<-。我们也可以叫它接送操作符。一个左尖括号紧接着一个减号形象地代表了元素值的传输方向。
上述代码:对于通道3由于仅仅声明没有进行初始化,所以不能执行关闭操作。 对于未初始化的通道执行close操作报panic:
panic: close of nil channel
// go语言中通道类型为引用类型,故只声明而未初始化时通道值为nil。
通道模型:单通道、双通道
双向通道
channel两端的goroutine既可以发送数据也可以接受数据。
默认情况下使用创建的通道即为双向通道。
单向通道
创建的channel规定了数据流向,对于channel双端,分别一端作为发送者(producer)、一端作为接收者(consumer),只能发送或者接收。
创建单向通道:对于单向通道而言,发送和接收均是站在数据的角度:
- 如果向通道中发送数据,则为发送通道。
- 如果从通道中接收数据,则为接收通道。
// 单向发送通道
ch1 := make(chan<-, 2)
var ch2 chan<-
// 单向接收通道
ch3 := make(<-chan, 2)
var ch4 <-chan
单向通道的作用
- 单向通道的主要约束其他代码的行为。 看下面示例代码:
package main
import "fmt"
/*
*
创建单向通道
*/
func main() {
// 定义带缓冲通道
ch := make(chan int, 3)
producer(ch, 7)
ans := consumer(ch)
fmt.Printf("main function get num is %v\n", ans)
}
func producer(ch chan<- int, num int) {
ch <- num
}
func consumer(ch <-chan int) (ans int) {
ans, ok := <-ch
if ok {
fmt.Printf("consumer get data is %v\n", ans)
return ans
}
return 0
}
在Go语言中把元素类型匹配的双向通道传递给单向通道,会自动把双向通道转换为函数需要的单向通道(发送 or 接收)
上述代码简单定义一个生产者函数,向通道中写入数据;定义一个消费者函数,从特定通道中消费数据。借助于通道实现了消息的特定方向移动。
更普适应的使用——对接口的行为进行约束:从而对接口的所有实现者都进行约束的目的。
type Notifier interface{
SendInt(ch chan<- int)
}
上述代码中我们对Notidier接口的SendInt函数使用了单向通道约束,在所有Notifier接口的所有实现类型中,SendInt函数都会受到单向通道的约束。
- 在函数声明的结果列表中使用单向通道:
package main
import "fmt"
func main() {
getChan := getIntChan()
for elem := range getChan {
fmt.Println(elem)
}
}
// 获取一个单向发送通道
func getIntChan() <-chan int {
num := 5
ch := make(chan int, num)
for i := 0; i < num; i++ {
ch <- i
}
close(ch)
return ch
}
调用getIntChan()函数获取一个单向接收通道getChan,随后使用带有range的for遍历接收通道中的每一个元素并对其打印。
结合通道的特性:上述代码在getChan没有元素或者,为nil时会阻塞在for的那行代码处,上述只是展示单向通道的用于,不具有实际意义。
缓冲通道和非缓冲通道数据发送和接收过程
缓冲通道
ch := make(chan int, 5)
上述代码创建一个容量为5的双向通道。在Go语言中对于通道的初始化不像切片初始化可以指定切片的长度,在通道初始化时只需要设置一个缓冲容量,通道中的长度含义就是通道中的元素个数。
带缓冲通道数据发送:元素进行复制,将副本放入通道中,同时将通道中的原值删除。
如果缓冲容量已经满了,此时新来的goroutine会被放到一个FIFO队列中,等到缓冲区有容量,此时最前面的goroutine执行元素放置操作。
带缓冲通道数据接收:生成在通道中的元素值的副本,将副本给到接收方。
接收操作类似,如果缓冲区中没有元素,此时所有接收者会阻塞,并进入一个FIFO队列中,当缓冲区有数据后,在队列最前面的goroutine会获取channel最前端的元素值。
缓冲通道何时阻塞:
- 缓冲区满时,所有发送goroutine阻塞。
- 缓冲区没有数据时,所有接收goroutine阻塞。
- 对与nil的通道,无论何时一直阻塞。
在一般情况下缓冲通道起到数据传输的中间件作用,需要将数据暂时存储到缓冲区中,但是当缓冲通道执行发送操作时发现空的通道中正好有等待的接收者,此时发送者会直接将数据拷贝给接收者,减少数据在缓冲区的临时拷贝。(类似与非缓冲通道)
非缓冲通道
ch := make(chan int) // 创建非缓冲通道
非缓冲通道必须等待发送方和接收方均就绪,才会进行数据发送以及接收,否则对应的goroutine处于组塞。并且数据发送以及接收过程,在通道中不会产生数据副本,发送方数据拷贝后直接通过通道传递给接收方。
非缓冲通道何时阻塞:
- 发送方或接收方任何一方没有准备好,都会阻塞。
- 对于nil的通道,无论何时一直阻塞。
通道基本特性
- 对于同一个通道,发送操作之间是互斥的,接收操作之间也是互斥的:在同一时刻,Go 语言的运行时系统只会执行对同一个通道的任意个发送操作中的某一个。直到这个元素值被完全复制进该通道之后,其他针对该通道的发送操作才可能被执行。类似的,在同一时刻,运行时系统也只会执行,对同一个通道的任意个接收操作中的某一个。直到这个元素值完全被移出该通道之后,其他针对该通道的接收操作才可能被执行。即使这些操作是并发执行的也是如此。
- 发送操作和接收操作中对元素值的处理都是不可分割的。:保证通道中元素值的完整性,同时保证通道操作的唯一性
- 发送操作要么还没复制元素值,要么已经复制完毕,绝不会出现只复制了一部分的情况。
- 接收操作在准备好元素值的副本之后,一定会删除掉通道中的原值,绝不会出现通道中仍有残留的情况。
- 发送操作在完全完成之前会被阻塞。接收操作也是如此。
通道何时触发panic
- 对于值为nil(未初始化)的通道执行close()操作。
- 对于已经执行了close()操作的通道再次执行close()操作。
- 对于执行了close()操作的通道执行收发操作。
Channel和Select结合使用
Select语句和通道的关系
- select语句只能与通道联用,它一般由若干个分支组成。每次执行这种语句的时候,一般只有一个分支中的代码会被运行。
- select语句的分支分为两种,一种叫做候选分支,另一种叫做默认分支。候选分支总是以关键字case开头,后跟一个case表达式和一个冒号,然后我们可以从下一行开始写入当分支被选中时需要执行的语句。
- 默认分支其实就是 default case,因为,当且仅当没有候选分支被选中时它才会被执行,所以它以关键字default开头并直接后跟一个冒号。同样的,我们可以在default:的下一行写入要执行的语句。
- 由于select语句是专为通道而设计的,所以每个case表达式中都只能包含操作通道的表达式,比如接收表达式。
Select语句的分支选择规则有那些
-
对于每一个case表达式,都至少会包含一个代表发送操作的发送表达式或者一个代表接收操作的接收表达式,同时也可能会包含其他的表达式。比如,如果case表达式是包含了接收表达式的短变量声明时,那么在赋值符号左边的就可以是一个或两个表达式,不过此处的表达式的结果必须是可以被赋值的。当这样的case表达式被求值时,它包含的多个表达式总会以从左到右的顺序被求值。
-
select语句包含的候选分支中的case表达式都会在该语句执行开始时先被求值,并且求值的顺序是依从代码编写的顺序从上到下的。结合上一条规则,在select语句开始执行时,排在最上边的候选分支中最左边的表达式会最先被求值,然后是它右边的表达式。仅当最上边的候选分支中的所有表达式都被求值完毕后,从上边数第二个候选分支中的表达式才会被求值,顺序同样是从左到右,然后是第三个候选分支、第四个候选分支,以此类推。
-
对于每一个case表达式,如果其中的发送表达式或者接收表达式在被求值时,相应的操作正处于阻塞状态,那么对该case表达式的求值就是不成功的。在这种情况下,我们可以说,这个case表达式所在的候选分支是不满足选择条件的。
-
仅当select语句中的所有case表达式都被求值完毕后,它才会开始选择候选分支。这时候,它只会挑选满足选择条件的候选分支执行。如果所有的候选分支都不满足选择条件,那么默认分支就会被执行。如果这时没有默认分支,那么select语句就会立即进入阻塞状态,直到至少有一个候选分支满足选择条件为止。一旦有一个候选分支满足选择条件,select语句(或者说它所在的 goroutine)就会被唤醒,这个候选分支就会被执行。
-
如果select语句发现同时有多个候选分支满足选择条件,那么它就会用一种伪随机的算法在这些分支中选择一个并执行。注意,即使select语句是在被唤醒时发现的这种情况,也会这样做。
-
一条select语句中只能够有一个默认分支。并且,默认分支只在无候选分支可选时才会被执行,这与它的编写位置无关。
-
select语句的每次执行,包括case表达式求值和分支选择,都是独立的。不过,至于它的执行是否是并发安全的,就要看其中的case表达式以及分支中,是否包含并发不安全的代码了。
Select和Channel结合使用案例一
package main
import (
"fmt"
"math/rand"
)
/*
*
channel和select的联合使用
*/
func main() {
// 创建一个多维通道
chs := []chan int{
make(chan int, 1),
make(chan int, 1),
make(chan int, 1),
}
// 创建随机数
index := rand.Intn(3)
fmt.Printf("index is: %v\n", index)
chs[index] <- 1
select {
case elem := <-chs[0]:
fmt.Printf("通道0被选中,元素为:%v\n", elem)
case elem := <-chs[1]:
fmt.Printf("通道1被选中,元素为:%v\n", elem)
case elem := <-chs[2]:
fmt.Printf("通道2被选中,元素为:%v\n", elem)
default:
fmt.Println("error.")
}
}
- 如果像上述示例那样加入了默认分支,那么无论涉及通道操作的表达式是否有阻塞,select语句都不会被阻塞。如果那几个表达式都阻塞了,或者说都没有满足求值的条件,那么默认分支就会被选中并执行。
- 如果没有加入默认分支,那么一旦所有的case表达式都没有满足求值条件,那么select语句就会被阻塞。直到至少有一个case表达式满足条件为止。
- 可能会因为通道关闭了,而直接从通道接收到一个其元素类型的零值。所以,在很多时候,我们需要通过接收表达式的第二个结果值来判断通道是否已经关闭。一旦发现某个通道关闭了,我们就应该及时地屏蔽掉对应的分支或者采取其他措施。
- select语句只能对其中的每一个case表达式各求值一次。所以,如果我们想连续或定时地操作其中的通道的话,就往往需要通过在for语句中嵌入select语句的方式实现。但这时要注意,简单地在select语句的分支中使用break语句,只能结束当前的select语句
Select和Channel结合使用案例二
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int, 1)
time.AfterFunc(time.Second*2, func() {
close(ch)
})
select {
case _, ok := <-ch:
if !ok {
fmt.Println("The candidate case is closed.")
break
}
fmt.Println("The candidate case is selected.")
}
}
声明并初始化了一个叫做intChan的通道,然后通过time包中的AfterFunc函数约定在二秒钟之后关闭该通道。后面的select语句只有一个候选分支,我在其中利用接收表达式的第二个结果值对intChan通道是否已关闭做了判断,并在得到肯定结果后,通过break语句立即结束当前select语句的执行。
Select和for联合使用时如何退出最外层循环
- break和标签配合使用,直接break处出指定的循环体。
- goto语句跳转到指定标签。
代码示例:
package main
import (
"fmt"
"time"
)
func main() {
go func() {
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
time.AfterFunc(time.Second*3, func() {
close(ch)
})
// 方式一:使用break配合标签实现
loop:
for {
select {
case _, ok := <-ch:
if !ok {
fmt.Println("The ch is closed.")
break loop // 使用return配合标签退出for循环
}
fmt.Println("The ch case is selected.")
}
}
fmt.Println("The end of ch.")
}()
go func() {
ch1 := make(chan int, 3)
ch1 <- 5
ch1 <- 6
ch1 <- 7
time.AfterFunc(3*time.Second, func() {
close(ch1)
})
for {
select {
case _, ok := <-ch1:
if !ok {
fmt.Println("The ch1 is closed.")
goto loop1 // 使用goto配合标签实现退出select和for的结合使用
}
fmt.Println("The ch1 case is selected.")
}
}
loop1:
fmt.Println("The end of ch1.")
}()
// 等待10防止主协程退出后所有子协程死亡
time.Sleep(time.Second * 10)
fmt.Println("The end of main.")
}
上述代码执行结果:
The ch1 case is selected.
The ch1 case is selected.
The ch1 case is selected.
The ch case is selected.
The ch case is selected.
The ch case is selected.
The ch is closed.
The end of ch.
The ch1 is closed.
The end of ch1.
The end of main.
上述代码创建两个协程,一个协程中验证break配合标签实现退出外层for循环,另一个协程中验证goto配合标签实现退出外层for循环。同时为了防止main协程退出导致两个子协程退出,在主协程中调用time.Sleep()函数休眠10秒钟。
如果在select语句中发现某个通道已关闭:为了防止再次进入这个分支,可以把这个channel重新赋值成为一个长度为0的非缓冲通道,这样这个case就一直被阻塞了