channel基本介绍·GoLang

GoLang·channel(通道)

  • channel(通道)是 golang 的一种重要特性
  • 因为channel的存在才使得golang不同于其它语言
  • channel 使得并发编程变得简单容易有趣

1. channel 的概念和语法

一个channel可以理解为一个先进先出的消息队列。
channel用来在协程[goroutine]之前传递数据,准确的说,是用来传递数据的所有权。
一个设计良好的程序应该确保同一时刻channel里面的数据只会被同一个协程拥有
这样就可以避免并发带来的数据不安全问题[data races]

协程:更像是一个任务,而不是一个持续执行代码的处理器, 系统不需要保证协程调度的公平性,只有在特定的时机才会从一个协程切换到另一个协程, 比如一些协程执行完的时候/主动出让CPU的时候/进入阻塞状态, 这样增加协程数量就不会导致增加单位时间内协程切换次数, 所以可以创建大量的协程, 不用去考虑任务调度的开销,所以协程适合IO密集型程序,不适合CPU密集型程序

2. channel 的类型

像数组、切片和字典一样,channel类型是一种组合类型,每一种 channel 类型都对应着一种简单的数据类型
比如元素的类型是 string,那么对应的 channel 类型就是 chan string
进入 channel 的数据也就必须是 string 类型的值
官方的 go 编译器限制 channel 里的单个元素最多 65535 个字节
也就是说如果channel缓冲数组里面容纳的是 struct,那这个 struct 的 size 不能大过 65535
尽管如此,我们也不应该传递体积过大的元素值
因为 channel 的数据从进入到流出会涉及到数据拷贝操作
如果元素体积过大,最好的方法还是使用传递指针来取代传递值
channel类型是可以带有方向的,假设T是一种类型
	1. chan T 是双向 channel 类型,编译器允许对双向 channel 同时进行发送和接收。
	2. chan<- T 是只写 channel 类型,编译器只允许往 channel 里面发送数据。
	3. <-chan T 是只读 channel 类型,编辑器只允许从 channel 里面接收数据
channel类型的零值形式称为空channel
一个非空channel类型必须通过**make关键字**进行创建。
例如 make(chan int, 10) 将会创建出一个可以容纳 10int 值 的 channel。
第二个整形的参数值代表的就是channel可以容纳数据的大小,如果不提供这个参数值,那默认值就是零
var ch chan string; // nil channel
ch := make(chan string); // zero channel
ch := make(chan string, 10); // buffered channel
channel 里面的 value buffer 的容量也就是channel的容量
channel 的容量为零表示这是一个阻塞型通道,非零表示缓冲型通道[非阻塞型通道]

3. channel 的内部结构

  • 每个channel内部实现都有三个队列
    1. 接收消息的协程队列
      这个队列的结构是一个限定最大长度的链表,所有阻塞在channel的接收操作的协程都会被放在这个队列里。
    2. 发送消息的协程队列
      这个队列的结构也是一个限定最大长度的链表。所有阻塞在channel的发送操作的协程也都会被放在这个队列里。
    3. 环形数据缓冲队列
      这个环形数组的大小就是 channel 的容量。如果数组装满了,就表示 channel 满了,如果数组里一个值也没有,就表示 channel 是空的。对于一个阻塞型 channel 来说,它总是同时处于即满又空的状态。
  • 一个 channel 被所有使用它的协程所引用,也就是说,只要这两个装了协程的队列长度大于零,那么这个 channel 就永远不会被垃圾回收。
  • 另外,协程本身如果阻塞在 channel 的读写操作上,这个协程也永远不会被垃圾回收,即使这个 channel 只会被这一个协程所引用。

4. channel 的使用

  • channel支持以下操作
1. 使用cap(ch) 函数查询 channel 的容量,cap 是 golang 的内置函数。
2. 使用 len(ch) 函数查询 channel 内部的数据长度,len 函数也是内置的,表面上这个函数很有意义,但实际上它很少用。
3. 使用 close(ch) 关闭 channel,close 也是内置函数。一个非空 channel 只能够被关闭一次,如果关闭一个已经被关闭的或者是关闭一个空 channel 将会引发panic。另外关闭一个只读 channel 是非法的,编译器直接报错。
4. 使用 ch <- v 发送一个值v到 channel 。发送值到 channel 可能会有多种结果,即可能成功,也可能阻塞,甚至还会引发 panic(恐慌),取决于当前 channel 在什么状态。
5. 使用 v, ok <- ch 接收一个值。第二个遍历ok是可选的,它表示channel是否已关闭。接收值只会又两种结果,要么成功要么阻塞,而永远也不会引发 panic
  • 所有的这些操作都是同步的协程安全的,不需要加任何其它同步控制

5. For-Range

  • for-range 语法可以用到通道上。循环会一直接收channel里面的数据,直到channel关闭。
  • 不同于 array/slice/map 上的 for-range,channel 的 for-range 只允许有一个变量
for v = range aChannel {
	// use v
}

等价于

for {
	v, ok = <-aChannel
	if !ok {
		break
	}
	// use v
}

注意:for-range对应的channel不能是只写channel。

6. Select-Cases

  • select块是为channel特殊设计的语法,它和switch语法非常相近。
  • 分支上它们都可以有多个case块和做多一个default块,但是也有很多不同
1. select 到 括号{之间不得有任何表达式
2. fallthrough关键字不能用在select里面
3. 所有的case语句要么是channel的发送操作,要么就是channel的接收操作
4. select里面的case语句是随机执行的,而不能是顺序执行的。设想如果第一个case语句对应的channel是非阻塞的话,case语句的顺序执行会导致后续的case语句一直得不到执行除非第一个case语句对应的channel里面的值都耗尽了。
5. 如果所有case语句关联的操作都是阻塞的,default分支就会被执行。
6. 如果没有default分支,当前goroutine就会阻塞,当前的goroutine会挂接到所有关联的channel内部的协程队列上。
7. 所以说单个goroutine是可以同时挂接到多个channel上的,甚至可以同时挂接到同一个channel的发送协程队列和接收协程队列上。
8. 当一个阻塞的goroutine拿到了数据接触阻塞的时候,它会从所有相关的channel队列中移除掉。
package main
import (
    "fmt"
)
func fibonacci(c, quit chan int) {
    x, y := 1, 1
	for {
        select {
			case c <- x:
				x, y = y, x+y
			case <-quit:
				fmt.Println("quit")
				return
        }
    }
}
func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() {
		for i := 0; i < 6; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)
}

Go⾥⾯提供了⼀个关键字select,通过select可以监听channel上的数据流动。

select的⽤法与switch语⾔⾮常类似,由select开始⼀个新的选择块,每个选择条件由case语句来描述。
与 switch 语句相⽐, select 有⽐较多的限制,其中最⼤的⼀条限制就是每个 case 语句⾥必须是⼀个 IO操作 ,⼤致的结构如下:
    select {
		case <-chan1:
        // 如果chan1成功读到数据,则进⾏该case处理语句
		case chan2 <- 1:
        // 如果成功向chan2写⼊数据,则进⾏该case处理语句
		default:
        // 如果上⾯都没有成功,则进⼊default处理流程
    }
  • 在⼀个select语句中,Go语⾔会按顺序从头⾄尾评估每⼀个发送和接收的语句。
  • 如果其中的任意⼀语句可以继续执⾏(即没有被阻塞),那么就从那些可以执⾏的语句中任意选择⼀条来使⽤。
  • 如果没有任意⼀条语句可以执⾏(即所有的通道都被阻塞),那么有两种可能的情况:
    • 如果给出了default语句,那么就会执⾏default语句,同时程序的执⾏会从select语句后的语句中恢复。
    • 如果没有default语句,那么select语句将被阻塞,直到⾄少有⼀个通信可以进⾏下去

7. channel 简单规则表

| 				| 空 Channel | 已关闭 Channel | 活跃 Channel |
| 	close(ch) 	| 	 panic	 |	 	panic 	 |	 	成功	|
| 	 ch <- v 	| 	 阻塞	 |	 	panic 	 |   成功或阻塞	|
|  v,ok =<- ch	| 	 阻塞	 | 	   不用阻塞	 |   成功或阻塞	|

8. channel 规则详细解释

8.1 空 channel

  1. 关闭一个空 channel 会导致当前 goroutine 引发 panic
  2. 向一个空 channel 发送值会导致当前的 goroutine 阻塞
  3. 从一个空 channel 接收值也会导致当前的 goroutine 阻塞
  • 在空channel上的调用len和cap函数都统一返回零。

8.2 已关闭的 Channel

  1. 关闭一个已关闭的channel会引发panic
  2. 向一个已关闭的channel发送值会引发panic。
  3. 当这种send操作处于select块里面的case语句上时,它会随时导致select语句引发panic。
  4. 从一个已关闭的channel上接收值既不会阻塞也不能panic,它一直能成功返回。
  5. 只是返回的第二个值ok永远是false,表示接收到的v是在channel关闭之后拿到的,对应得值也是相应元素类型的零值。
  6. 可以无限循环从已关闭的channel上接收值

8.3 活跃的 Channel

8.3.1 关闭操作
  1. 从 channel 的接收协程队列中移除所有的 goroutine,并唤醒它们。
  2. 从 channel 的接收协程队列中移除所有的 goroutine,并唤醒它们。
  3. 一个已关闭的channel内部的缓冲数组可能不是空的,没有接收的这些值会导致channel对象永远不会被垃圾回收。
8.3.2 发送操作
  1. 如果是阻塞型channel,那就从channel的接收协程队列中移出第一个协程,然后把发送的值直接递给这个协程。
  2. 如果是阻塞型channel,并且channel的接收协程队列是空的,那么当前的协程将会阻塞,并进入到channel的发送协程队列里。
  3. 如果是缓冲型channel,并且缓冲数组里还有空间,那么将发送的值添加到数组最后,当前协程不阻塞。
  4. 如果是缓冲型channel,并且缓冲数组已经满了,那么当前的协程将会阻塞,并进入到channel的发送协程队列中。
8.3.3 接收操作
  1. 如果是缓冲型channel,并且缓冲数组有值,那么当前的协程不会阻塞,直接从数组中拿出第一个值。如果发送队列非空,还需要将队列中的第一个goroutine唤醒。
  2. 如果是阻塞型channel,并且发送队列非空的话,那么唤醒发送队列第一个协程,该协程会将发送的值直接递给接收的协程。
  3. 如果是缓冲型channel,并且缓冲数组为空,或者是阻塞型channel,并且发送协程队列为空,那么当前协程将会阻塞,并加入到channel的接收协程队列中。

9. 总结

根据以上规则,我们可以得出以下结论

  1. 如果channel关闭了,那么它的接收和发送协程队列必然空了,但是它的缓冲数组可能还没有空。
  2. channel的接收协程队列和缓冲数组,同一个时间必然有一个是空的
  3. channel的缓冲数组如果未满,那么它的发送协程队列必然是空的
  4. 对于缓冲型channel,同一时间它的接收和发送协程队列,必然有一个是空的
  5. 对于非缓冲型channel,一般来说同一时间它的接收和发送协程队列,也必然有一个是空的,但是有一个例外,那就是当它的发送操作和接收操作在同一个select块里出现的时候,两个队列都不是空的。

10.引用

版权声明:本文为CSDN博主「weixin_44282540」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
本文参考channel基本介绍狗朗:https://blog.csdn.net/weixin_44282540/article/details/88658010

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值