写点什么

Channels In Go

2019 年 11 月 21 日

Channels In Go

Go 有两个重要的内置功能,同时也是它的特性。分别是 channel、Goroutine。这两个特性使 Go 编写并发程序变的简单、有趣。本文将主要介绍 channel。原文来自 go101,本文是翻译后留存,方便自己学习。


Channel Introduction

不要通过共享内存来通信,应该通过通信来共享内存,出自 Rob Pike。是在 Go 社区非常流行的一句话,而 channel 就是为此而生。


通过共享内存来通信与通过同通信来共享内存是两种模式的并发程序。当通过共享内存来通信,我们需要使用一些传统的并发技术,例如互斥锁,来保证共享内存可以被安全的访问,防止数据竞争。


channel 是在多个 Goroutine 之间传递数据和同步的重要手段,而对通道的操作本身也是同步的。同一时刻,仅有一个 Goroutine 能向一个 channel 发送元素值,同时仅有一个 Goroutine 能从它那里接受元素值。在 channel 中,各个元素值都是严格按照发送到此的先后顺序排序,最早被发送至 channel 的元素值会最先被接收。它类似一个内部的 FIFO(first in,first out 先进先出)数据队列。


此外,channel 中的元素值都具有原子性,是不可被分割的。channel 中的每个元素都只能被某一个 Goroutine 接收,已被接收的元素值会立刻从 channel 中删除。


某些 value 的使用权会随着 value 在 Goroutines 间传递,当 Goroutine 发送数据到 channel 会释放 value 的所有权,而在接收数据时会同时获得 value 的所有权。


Go 也支持一些传统的并发技术,但 channel 应该优先被考虑。


老实说,每个并发同步技术都有其最佳的使用场景。 但 channel 应用范围更广,使用场景更多。 而且在许多情况下,使用 channel 的并发代码通常比使用其他数据同步处理技术看起来更清晰和易于理解。


1 Channel 的分类

channel 是复合类型,类似 array、slice、map,每个 channel 都有一个元素类型。 所有要发送到通道的数据都必须是元素类型的值。channel 可以为 nil


channel 可分为双向、单向,假设以下的 T 是任意类型:


  • chan T, 双向 channel,同时允许发送数据到 channel、从 channel 接收数据。

  • chan<- T,单向 channel,只允许发送数据到 channel,操作符<-形象的表示了元素值的流向。

  • <-chan T,单向 channel,只允许从 channel 接收数据,这次操作符<-位于关键字 chan 左边,这真的很棒。


使用内置的 make 函数可以创建一个 channel,下面这个例子会创建一个元素类型为 int 的 channel,make 函数的第二个参数为可选参数,可以设置 channel 的容量,默认为 0。



2 Channel 的操作

这里有 5 个 channel 的操作,假设 ch 为 channel 类型


1.关闭 channel



close 是一个内置函数,参数必须是 channel 类型变量且不能是<-ch 类型。


  1. 发送值到 channel



  1. 从 channel 接收值



接收操作至少返回一个元素类型的值,它也可以用作赋值表达式



  1. 查看 channel 容量



cap 是一个内置函数,返回的值为 int 类型


  1. 查看 channel 中当前值的数量



len 是一个内置函数,返回的值为 int 类型。


如果 channel 元素类型为 nil,cap、len 将返回 0。


3 Channel 操作规则总结

为了能将 channel 解释清楚,剩余的文章中会将 channel 分为三类:


1.nil channel


2.non-nil 但已关闭的 channel


3.not-nil 未关闭的 channel


下面这个表简单表述以上三类 channel 的操作场景



背景简介

表中 5 种未标记的场景,应用规则非常清晰:


  • 关闭 nil channel 或已关闭的 channel 会引发 panic

  • 发送元素值至已关闭的 channel 会引发 panic

  • 发送元素值至 nil channel 或从 nil channel 接收元素值,都会导致当前 goroutine 永久阻塞


未标记的 4 种场景在下面会详细解释

1.为了更好的理解 channel,先了解下 channel 的内部结构。我们可以认为每个 channel 在内部维护 3 个队列


2.receiving goroutine queue(简称 RGQ)是没有大小限制的链表,队列中是阻塞的 receiving goroutine,准备存储值的地址也与每个 goroutine 一起存储在队列中


3.sending goroutine queue(简称 SGQ)同样是没有大小限制的链表,队列中是阻塞的 sending goroutine, 准备发送的值的地址也与每个 goroutine 一起存储在队列中


4.value buffer queue(简称 VBQ)是个圆形队列,大小等于 channel 的容量。如果队列中值的数量达到 channel 的容量,channel 会以 full 状态被调用。如果队列中没有存储值,channel 会以 emply 状态被调用。容量为 0 的 channel 只能是 full 或 emply。


Channel 规则场景 A

当 gorontine 尝试从非 nil 未关闭 channel 接收元素值,该 goroutine 先尝试获得 channel 关联的锁,然后执行以下步骤,直到满足一个条件。


1.如果 channel 的 VBQ 不为空,这种情况下 channel 的 RGQ 必须为空,此时的 goroutine 将通过 unshift 从 VBQ 接收元素值。 如果 channel 的 SGQ 也不为空,则将 SGQ 中的某个 sending goroutine 通过 unshift 移出队列并设置为运行状态,要发送的元素值将被放入 channel 的 VBQ,receiving goroutine 继续运行。该场景下 channel 的发送操作是非阻塞的


2.如果 channel 的 VBQ 为空,但 SGQ 不为空。这种情况下 channel 必须为非缓冲 channel。receiving goroutine 将从 RGQ 中 unshift 出某个 sending goroutine,并接收这个 sending goroutine 发送的元素值。该 sending goroutine 将被解锁并设置为运行状态。该场景下 channel 的发送操作是非阻塞的


3.如果 channel 的 VBQ 和 SGQ 都为空,此时的 gorontine 会被放入 RGQ 中,进入(停留)在阻塞状态。当有其他 goroutine 发送元素值到 channel,它可能恢复运行。该场景下 channel 的发送操作是阻塞的


Channel 规则场景 B

当 goroutine 尝试发送元素值至非空未关闭的 channel,该 goroutine 先尝试获取 channel 关联的锁,然后执行以下步骤,直到满足一个条件。


1.如果 channel 的 RGQ 不为空,这种情况下 VBG 必须为空,sending goroutine 将从 RGQunshift 出某个 receiving goroutine,并发送元素值至这个 receiving goroutine。sending goroutine 继续运行。该场景下 channel 的发送是非阻塞的。


2.如果 channel 的 RGQ 为空,并且 VBQ 没有满。在这种情况下 SGQ 必须为空。将 sending goroutine 要发送的元素值放入 VBQ,sending goroutine 继续运行。该场景下 channel 的发送操作是非阻塞的。


3.如果 channel 的 RGQ 为空,并且 VBQ 已满。sending goroutine 将被放入 SGQ,进入(停留)在阻塞状态。当有其他 goroutine 从 channel 接收元素值,它可能恢复运行。该场景下 channel 的发送操作是阻塞的。


Channel 规则场景 C

当 goroutine 尝试关闭一个非空未关闭的 channel,将按照以下顺序执行两个步骤


1.如果 channel 的 RGQ 不为空,在这种情况下 VBQ 必须为空。RGQ 中所有 goroutine 会被逐个 unshift,并且每个 goroutine 会接收到一个元素值类型的零值。


2.如果 channel 的 SGQ 不为空,SGQ 中所有 gorontine 会被逐个 unshift,每个向已关闭 channel 发送元素值的 goroutine 都会产生一个 panic。已经放入到 VBG 的元素值仍然存在。


Channel 规则场景 D

channel 关闭后,channel 的接收操作将不会再被阻塞。VBQ 已有的元素值可以继续被接收。当 VBQ 中所有的元素值都被取出后,后续的接收操作都会收到元素值的零值。


通过上述的规则,我们可以得到一些事实


  • 如果 channel 是关闭的,它的 VGQ、SGQ 必须为空,但 VBQ 可以不为空

  • 任何情况下,如果 VBQ 不为空,那么它的 RGQ 必须为空

  • 任何情况下,如果 VBQ 没满,那么它的 SGQ 必须为空

  • 缓冲 channel 在任何情况下,SBQ、RGQ 其中之一必须为空


非缓冲 channel 在任何情况下,通常 SBQ、RGQ 其中之一必须为空,但有一个例外,那就是 select 可能会导致某个 goroutine 被放入到这两个队列中。


4 Channel 使用实例

现在来看些 channel 使用的例子


package mainimport "fmt"func main() {       c := make(chan int) // 非缓冲通道        go func() {                x := <- c // 这里会被阻塞,直到通道收到元素值                c <- x*x  // 这里会被阻塞,直到通道中的值被接收        }()    c <- 3   // 这里会被阻塞,直到通道中的值被接收         y := <-c // 这里会被阻塞,直到通道收到元素值        fmt.Println(y) // 9}
复制代码


下面这个例子使用了缓冲通道,但这个程序不是并发的


package mainimport "fmt"func main() {    c := make(chan int, 2) // 缓冲通道    c <- 3    c <- 5    close(c)    fmt.Println(len(c), cap(c)) // 2 2    x, ok := <-c    fmt.Println(x, ok) // 3 true    fmt.Println(len(c), cap(c)) // 1 2    x, ok = <-c    fmt.Println(x, ok) // 5 true    fmt.Println(len(c), cap(c)) // 0 2    x, ok = <-c    fmt.Println(x, ok) // 0 false    x, ok = <-c    fmt.Println(x, ok) // 0 false    fmt.Println(len(c), cap(c)) // 0 2    close(c) // panic!    c <- 7   // also panic if the last line is removed.}
复制代码


一场永不停止的足球赛


package mainimport (    "fmt"    "time")func main() {    var ball = make(chan string)    kickBall := func(playerName string) {        for {            fmt.Println(<-ball, "kicked the ball.")                        time.Sleep(time.Second)            ball <- playerName           }    }   go kickBall("John")   go kickBall("Alice")    go kickBall("Bob")     go kickBall("Emily")     ball <- "referee" // kick off     var c chan bool   // nil    <-c               // blocking here for ever}
复制代码


5 Channel 元素值通过拷贝传递

无论发送元素值至 channel 还是从 channel 接收元素值,都会对这个值进行拷贝。类似赋值、函数传参操作。


标准的 go 编译器,要求 channel 元素类型不能超过 65535。通常,我们不会限制 channel 的大小,所有通过 channel 传送的元素值都会发生拷贝。当某个元素值从一个 goroutine 被传递到另外一个 goroutine,会有 2 个元素值被拷贝。所以如果传送的值很大,最好还是用指针来代替。


6 Channel 中的 For-Range 循环

for-range 代码结构也可以应用到 channel。循环将尝试迭代的接收 channel 中的,直到 channel 被关闭并且 VBQ 为空。


for v = range aChannel {    // use v}
复制代码


等同于


for {    v, ok = <-aChannel    if !ok {        break    }    // use v}
复制代码


这里的 aChannel 不能是只允许发送的通道类型。如果它是一个 nil channel, 循环将永久阻塞。


本文转载自公众号 360 云计算(ID:hulktalk)。


原文链接:


https://mp.weixin.qq.com/s/xIeoClbb6wszqLRNkaV2yw


2019 年 11 月 21 日 23:47231

评论

发布
暂无评论
发现更多内容

1分钟学习Java中数组快速复制

HQ数字卡

Java 数组

【摘】Git-从零单排 01期

卡尔

git 效率工具 工具 开发工具

mac 安装特定版本php-redis

HQ数字卡

php

ARTS - 第一周打卡

陈文昕

JAVA AGENT 学习

zane

Java

使用docker-compose部署单机RabbitMQ

Kevin Liao

Docker Docker-compose RabbitMQ

《零基础学 Java》 FAQ 之 13-编程里的两个特殊的值

臧萌

Java

简单聊聊什么是苹果生态

李俊辰

重磅!Apache Flink 1.11 功能前瞻抢先看!

Apache Flink

大数据 flink 流计算 实时计算 大数据处理

"第1天,读以太坊白皮书 | 5天掌握以太坊 dApp 开发"

陈东泽 EuryChen

区块链 以太坊 dapp Ethereum blockchain

记:mybatis <foreach> 语法错误

Kevin Liao

mybatis foreach SQL语法 SQLSyntaxErrorException

Golang热更新原理

我心依然

golang nginx Linux 信号

全栈工程师为什么越混越困难,看这篇就够了

金刚小书童

职业规划 技术管理 全栈工程师 程序员成长 程序员次第

介绍一款文本分析工具

黄大路

数据挖掘 数据分析 nlp

2020年2月北京BGP机房网络质量评测报告

博睿数据

APM 机房 评测 世纪互联

Android实现人脸识别(人脸检测)初识

sar

android OpenCV renlianshibie

IO多路复用整理

戈坞昂

Linux io

《零基础学 Java》 FAQ 之 15-Java范型做了两件事

臧萌

Java

专业的力量

无量靠谱

淘宝 美团 专业 专业主义 大前研一

浅谈使命、愿景、价值观。

石云升

价值观 使命 愿景

MySQL查询优化一般步骤

HQ数字卡

MySQL sql 查询优化

《零基础学 Java》 FAQ 之 14-访问控制符总结

臧萌

Java

基于mysqldump聊一聊MySQL的备份和恢复

麦洛

MySQL

听过很多道理,依然过不好这一生。

Neco.W

感悟 创业心态

李想解读《高效能人士的七个习惯》

我心依然

习惯 高效能人士的七个习惯 李想 汽车之家

工厂模式(二)MyBatis中展示的简单的工厂模式

LSJ

mybatis 工厂模式

唯技术论坏处都有啥?如何跳出唯技术论思维?

KAMI

方法论 思考 思维方式 开发 唯技术论

给学妹的 Java 学习路线

武培轩

Java 学习 程序员 程序媛

2020年2月北京BGP机房网络质量评测报告

博睿数据

RestTemplate 配置手册

zane

Spring Boot HTTP

OpenResty 部署配置和日志切割

wong

centos log openresty

Channels In Go-InfoQ