go channel 详解

news2024/9/28 11:25:27

一、概述

在Go语言中,channel是一种特殊的类型,用于在并发编程中实现不同的goroutine之间的通信和同步。本文将深入探讨golang的channel是如何工作的,并介绍如何使用channel来提高程序的性能和可靠性。

二、什么是Channel?

在Go语言中,使用goroutine单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。

虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。

Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。

如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。

Channel是Go中的一个核心类型,你可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯(communication)。

Channel提供了一种同步的机制,确保在数据发送和接收之间的正确顺序和时机。通过使用channel,我们可以避免在多个goroutine之间共享数据时出现的竞争条件和其他并发问题。

Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

Channel的操作符是箭头 <- (箭头的指向就是数据的流向)。

三、Channel 类型

Channel是一种类型,一种引用类型。声明通道类型的格式如下:

var 变量 chan 元素类型

示例:

var ch1 chan int       // 声明一个传递整型的通道
var ch2 chan bool      // 声明一个传递布尔值的通道
var ch3 chan []int     // 声明一个传递int切片的通道
var ch4 chan struct{}  // 声明一个struct的通道

Channel类型的定义格式如下:

ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType .

它包括三种类型的定义。可选的<-代表channel的方向。如果没有指定方向,那么Channel就是双向的,既可以接收数据,也可以发送数据。

chan T          // 可以接收和发送类型为 T 的数据
chan<- float64  // 只可以用来发送 float64 类型的数据
<-chan int      // 只可以用来接收 int 类型的数据

<-总是优先和最左边的类型结合。

chan<- chan int    // 等价 chan<- (chan int)
chan<- <-chan int  // 等价 chan<- (<-chan int)
<-chan <-chan int  // 等价 <-chan (<-chan int)
chan (<-chan int)

四、创建 Channel

通道是引用类型,通道类型的空值是 nil

var ch chan int 
fmt.Println(ch)  // 结果是: <nil>

声明后的通道需要使用 make 函数初始化之后才能使用。

使用make初始化channel格式:

make(chan 元素类型, [容量])

容量(capacity)代表Channel容纳的最多的元素的数量,代表Channel的缓存的大小。
如果没有设置容量,或者容量设置为0, 说明Channel没有缓存,只有sender和receiver都准备好了后它们的通讯(communication)才会发生(Blocking)。如果设置了缓存,就有可能不发生阻塞, 只有buffer满了后 send才会阻塞, 而只有缓存空了后receive才会阻塞。一个nil channel不会通信。

所以Channel可分为:无缓冲通道(阻塞),有缓存通道(非阻塞)

五、Channel 操作

通道有发送(send)、接收(receive)和关闭(close)三种操作。

发送和接收都使用<-符号。

现在我们先使用以下语句定义一个通道:

ch := make(chan int)

发送(send)

send被执行前(proceed)通讯(communication)一直被阻塞着。如前所言,无缓存的channel只有在receiver准备好后send才被执行。如果有缓存,并且缓存未满,则send会被执行。

往一个已经被close的channel中继续发送数据会导致run-time panic

往nil channel中发送数据会一致被阻塞着。

将一个值发送到通道中。

ch <- 10    // 把 10 发送到 ch 中

接收(receive)

从一个nil channel中接收数据会一直被block。

从一个被close的channel中接收数据不会被阻塞,而是立即返回,接收完已发送的数据后会返回元素类型的零值(zero value)。

从一个通道中接收值

x := <-ch   // 从 ch 中接收值并赋值给变量x
<-ch        // 从 ch 中接收值,忽略结果

关闭(close)

可以通过内建的close方法可以关闭Channel。关闭channel后,任何接收方将收到一个零值和一个布尔标志,指示channel已关闭。

close(ch)

如果你的管道不往里存值或者取值的时候一定记得关闭管道。

关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。

关闭后的通道有以下特点:

  1. 对一个关闭的通道再发送值就会导致panic。
  2. 对一个关闭的通道进行接收会一直获取值直到通道为空。
  3. 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
  4. 关闭一个已经关闭的通道会导致panic。
v, ok := <-ch  

它可以用来检查Channel是否已经被关闭了。

如果OK 是false,表明接收的x是产生的零值,这个channel被关闭了或者为空。

六、无缓冲的通道(阻塞)

无缓冲的通道又被称为阻塞的通道。 我们看一下示例:

func main() {
    ch := make(chan int)
    ch <- 10
    fmt.Println("发送成功")
}

上面这段代码能够通过编译,但是执行时会出现一下错误:

fatal error: all goroutines are asleep - deadlock!

为什么会出现deadlock错误呢?

因为我们使用ch := make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有人接收值的时候才能发送值。就像你住的小区没有快递柜和代收点,快递员给你打电话必须要把这个物品送到你的手中,简单来说就是无缓冲的通道必须有接收才能发送。

上面的代码会阻塞在ch <- 10这一行代码形成死锁,那如何解决这个问题呢?

一种方法是启用一个goroutine去接收值,例如:

func recv(c chan int) {
    ret := <-c
    fmt.Println("接收成功", ret)
}
func main() {
    ch := make(chan int)
    go recv(ch) // 启用goroutine从通道接收值
    ch <- 10
    fmt.Println("发送成功")
}

无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值。

使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道。

七、有缓冲的通道(非阻塞)

解决无缓冲通道(阻塞)死锁的问题,就是使用有缓冲的通道。

通过缓存的使用,可以尽量避免阻塞,提供应用的性能。

我们使用 make 函数在初始化的时候为其指定通道的容量(缓冲大小):

func main(){
    ch := make(chan int ,1)  // 创建一个容量为 1 的有缓冲区的通道
    ch <- 10
    fmt.Println("发送成功")
}

只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。

我们可以使用内置的len函数获取通道内元素的数量,使用cap函数获取通道的容量,虽然我们很少会这么做。

八、单向通道

有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数中只能发送或只能接收。

Go语言中提供了单向通道来处理这种情况:

// 单向发送 out 通道
func cter(out chan <- int){
    for i := 0; i < 10; i++ {
        out <- i
    }
    close(out)
}

// 单向发送 out 通道, 单向接收 in 通道
func sqer(out chan <- int , in <- chan int){
    for i := range in{
        out <- i * i
    }
    close(out)
}

// 单向接收 in 通道
func prter(in <-chan int){
    for i := range in {
        fmt.Println(i)
    }
}

func main(){
    out := make(chan int)
    in := make(chan int)
    go cter(out)
    go sqer(out, in)
    prter(in)
}
  1. chan<- int 是一个只能发送的通道,可以发送但是不能接收;

  2. <-chan int 是一个只能接收的通道,可以接收但是不能发送。

在函数传参及任何赋值操作中将双向通道转换为单向通道是可以的,但反过来是不可以的。

九、如何优雅的从通道中循环取值?

channel 有一个特性:close关闭之后,在发送的时候会 panic,但是在接收的时候,是可以正常接收的。

这里介绍三种方式:

for range

for {}

select{}

for…range

通常使用 for range 的形式来循环取值。

func main(){
    c := make(chan int)
    go func() {
        for i := 0; i < 10; i = i + 1 {
            c <- i
        }
        close(c)
    }()
    for i := range c {
        fmt.Println(i)
    }
    fmt.Println("Finished")
}

range c产生的迭代值为Channel中发送的值,它会一直迭代直到channel被关闭。上面的例子中如果把close(c)注释掉,程序会一直阻塞在for …… range那一行。

for {} 死循环

我们还可以通过 for {} 死循环的形式,通过判断channel是否关闭来进行跳出循环进行取值。

func main(){
    c := make(chan int)
    go func() {
        for i := 0; i < 10; i = i + 1 {
            c <- i
        }
        close(c)
    }()
    
    for {
        i, ok := <- c  // 通道关闭后再取值ok=false
        if !ok {
            break;
        }
         fmt.Println(i)
    }
    fmt.Println("Finished")
}

select 语句

select语句选择一组可能的send操作和receive操作去处理。它类似switch,但是只是用来处理通讯(communication)操作。
它的case可以是send语句,也可以是receive语句,亦或者default

receive语句可以将值赋值给一个或者两个变量。它必须是一个receive操作。

最多允许有一个default case,它可以放在case列表的任何位置,尽管我们大部分会将它放在最后。

func fibonacci(c, quit chan int) {
    x, y := 0, 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 < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)
}

如果有同时多个case去处理,比如同时有多个channel可以接收数据,那么Go会伪随机的选择一个case处理(pseudo-random)。如果没有case需要处理,则会选择default去处理,如果default case存在的情况下。如果没有default case,则select语句会阻塞,直到某个case需要处理。

需要注意的是,nil channel上的操作会一直被阻塞,如果没有default case,只有nil channel的select会一直被阻塞。

select语句和switch语句一样,它不是循环,它只会选择一个case来处理,如果想一直处理channel,你可以在外面加一个无限的for循环:

for {
    select {
    case c <- x:
        x, y = y, x+y
    case <-quit:
        fmt.Println("quit")
        return
    }
}

select有很重要的一个应用就是超时处理。 因为上面我们提到,如果没有case需要处理,select语句就会一直阻塞着。这时候我们可能就需要一个超时操作,用来处理超时的情况。
下面这个例子我们会在2秒后往channel c1中发送一个数据,但是select设置为1秒超时,因此我们会打印出timeout 1,而不是result 1

func main() {
    c1 := make(chan string, 1)
    go func() {
        time.Sleep(time.Second * 2)
        c1 <- "result 1"
    }()
    select {
    case res := <-c1:
        fmt.Println(res)
    case <-time.After(time.Second * 1):
        fmt.Println("timeout 1")
    }
}

其实它利用的是time.After方法,它返回一个类型为<-chan Time的单向的channel,在指定的时间发送一个当前时间给返回的channel中。

十、Timer 和 Ticker

我们看一下关于时间的两个Channel。

timer是一个定时器,代表未来的一个单一事件,你可以告诉timer你要等待多长时间,它提供一个Channel,在将来的那个时间那个Channel提供了一个时间值。下面的例子中第二行会阻塞2秒钟左右的时间,直到时间到了才会继续执行。

timer1 := time.NewTimer(time.Second * 2)
<-timer1.C
fmt.Println("Timer 1 expired")

当然如果你只是想单纯的等待的话,可以使用time.Sleep来实现。

你还可以使用timer.Stop来停止计时器。

timer2 := time.NewTimer(time.Second)
go func() {
    <-timer2.C
    fmt.Println("Timer 2 expired")
}()
stop2 := timer2.Stop()
if stop2 {
    fmt.Println("Timer 2 stopped")
}

ticker是一个定时触发的计时器,它会以一个间隔(interval)往Channel发送一个事件(当前时间),而Channel的接收者可以以固定的时间间隔从Channel中读取事件。下面的例子中ticker每500毫秒触发一次,你可以观察输出的时间。

ticker := time.NewTicker(time.Millisecond * 500)
go func() {
    for t := range ticker.C {
        fmt.Println("Tick at", t)
    }
}()

类似timer, ticker也可以通过Stop方法来停止。一旦它停止,接收者不再会从channel中接收数据了。

十一、Channel 异常情况总结

channelnil非空空的满了没满
接收阻塞接收值阻塞接收值接收值
发送阻塞发送值发送值阻塞发送值
关闭panic关闭成功,读完数据后返回零值关闭成功,返回零值关闭成功,读完数据后返回零值关闭成功,读完数据后返回零值

十二、 结束语

本篇文章介绍说明了:

什么是Channel?

Channel 类型怎么定义?

如何创建 Channnel ?

如何使用 Channel ?

Channel 的阻塞和非阻塞的定义和使用

Channel 如何使用单向通道?

如何优雅从通道中取值?

特殊的 Channel:Timer 和 Ticker

希望本篇文章对你有所帮助,谢谢。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/695545.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Opencv】----- 图片无缝融合

上次写了一篇关于GAN缺陷生成的图片的&#xff0c;发现大家的对这个根本不感兴趣&#xff0c;可能是用于在座的各位觉得样本有的是&#xff0c;我为什么还苦哈哈的去生成啊&#xff0c;说的也不是没有道理&#xff0c;可能只是我搞了更加苦哈哈的工业深度学习吧&#xff0c;所以…

软件测试技能,JMeter压力测试教程,监听器之每秒事务数与响应时间(十四)

前言 压测的时候&#xff0c;我们会经常关注2个重要的指标 TPS 和 RT TPS&#xff1a;每秒处理的事务数(Transactions per Second)&#xff0c;jmeter的Throughput为吞吐量&#xff08;请求数/秒&#xff09; RT&#xff1a;响应时间(Reponse Time&#xff09;&#xff0c;从…

西亚文明的时空概念与历史上的文明形态

“西亚”这个地理概念主要指的是今日土耳其、伊朗、伊拉克、叙利亚、黎巴嫩、约旦、以色列和沙特等阿拉伯半岛国家所在的地区。若以地理区域来划分&#xff0c;与西亚文明比较密切的区域主要有&#xff1a;小亚细亚&#xff08;今土耳其&#xff09;、两河流域&#xff08;今伊…

n.moduleList 和Sequential的解析与对比

n.moduleList 和Sequential的解析与对比 1.nn.ModuleList2.nn.Sequential3、nn.Sequential与nn.ModuleList的区别4、使用场景 1.nn.ModuleList 2.nn.Sequential 3、nn.Sequential与nn.ModuleList的区别 4、使用场景

汇编输入int 16h

int 16h 读入缓冲区ASCII码 检测点17.1 int 16h会设置if1 assume cs:code code segment start:mov ah,0int 16hmov ah,1cmp al,rje redcmp al,gje greencmp al,bje bluejmp sret red:shl ah,1 green:shl ah,1 blue:mov bx,0b800hmov es,bxmov si,1mov cx,2000 s: mov byte ptr…

mysql 基础架构与组件

基础架构 下图是 MySQL 的一个简要架构图&#xff0c;从下图你可以很清晰的看到用户的 SQL 语句在 MySQL 内部是如何执行的。 先简单介绍一下下图涉及的一些组件的基本作用帮助大家理解这幅图。 连接器&#xff1a; 身份认证和权限相关(登录 MySQL 的时候)。查询缓存: 执行查…

Docker安装mysql8-超详细、每步都有截图

1.下载mysql8镜像&#xff08;版本根据自己选择&#xff1a;这里是8.0.20&#xff09; docker pull mysql:8.0.202.启动镜像 docker run -p 3307:3306 --name mysql8 -e MYSQL_ROOT_PASSWORD123456 -d mysql:8.0.20这里特殊的地方只是说明一下、对每个参数有个了解。没其他实…

webassembly添加调试

本文主要参考&#xff1a;webassembly下C调试 一、安装扩展程序 如果有条件的&#xff1a;DevTools下载 使用第三方Chrome扩展插件搜索下载网站地址 &#xff0c;搜索DWARF下载。 二、使用扩展程序 解压安装包后&#xff0c;直接将crx文件拖入到web浏览器上&#xff0c;如果…

vue element-ui的<el-tag>实现文字超过自动换行

目录 前言 解决方法 前言 今天我在使用element-ui的<el-tag>标签&#xff0c;但我发现一个问题&#xff0c;当我字体过多的时候&#xff0c;会出现这样的情况&#xff1a; 文字远远超出了蓝色框的宽度&#xff0c;那么如何解决呢&#xff1f; 解决方法 .el-tag{white…

VSCode RESTClient 返回结果中文乱码

VSCode RESTClient 返回结果中文总是显示为乱码 &#xff08;其实是 unicode 方式&#xff09; 进入设置 – 扩展 – REST Client &#xff0c; 找到 Rest-clien&#xff1a; DecodeEscaped Unicode Charactors &#xff0c;勾选上即可&#xff01; 再来运行就出来正常的中文了…

在线PDF格式转换器推荐,小圆象PDF转换器满足您的办公需求

作为上班族&#xff0c;我们都知道在办公应用中经常使用PDF文件。PDF具有较高的内容安全性&#xff0c;并且可以在多种设备上打开和浏览而不会出现格式混乱的问题。然而&#xff0c;PDF文件的一个短板是其不易编辑。通常情况下&#xff0c;我们需要将PDF文件转换为Word文档&…

Qt控件(按钮、单选、复选、list、tree、table)

一、布局 工具栏图标文字一起显示&#xff0c;背景透明。 二、代码 widget.ui <?xml version"1.0" encoding"UTF-8"?> <ui version"4.0"><class>Widget</class><widget class"QWidget" name"Widg…

Windows 安装 Redis

安装和配置Redis Redis的安装有两种安装形式&#xff0c;一种是安装包式&#xff0c;另一种是压缩包 注意&#xff1a;windows没有32位的&#xff0c;所以32位机器的朋友考虑换一下机器。 一、安装包式 1、windows环境下的redis需要去github上获取: https://github.com/Mic…

MATLAB matlab图像压缩程序--采用嵌入式小波零树编码算法源码

MATLAB matlab图像压缩程序--采用嵌入式小波零树编码算法源码 function varargout ezw(varargin) % EZW MATLAB code for ezw.fig % EZW, by itself, creates a new EZW or raises the existing % singleton*. % % H EZW returns the handle to a new EZW or…

【裸机开发】GPT 定时器(三) —— 使用GPT实现高精度延时

延时最简单粗暴的方式就是使用空循环来延时&#xff0c;依赖的是时钟主频&#xff08;默认是396M&#xff09;来计数&#xff0c;一旦修改了 6ull 的时钟主频&#xff0c;延时效果就会存在偏差。 因此我们可以使用 EPIT 或者 GPT 的计数功能实现高精度延时&#xff0c;EPIT 是…

电气设备无线测温技术的优势有哪些?安科瑞 许敏

摘 要&#xff1a; 无线测温技术以其安装方便灵活、测温精度高、安全可靠、环境适应性好、便于集中管理等优点&#xff0c; 解决了电气设备长期带电运行状态下的温度在线监测问题&#xff0c; 提高了电气设备的运行可靠性&#xff0c; 在电力行业得到了广泛的应用。本文主要介…

将当前conda环境导出为yaml文件

conda环境的转移和复制 conda导出已有环境&#xff0c;环境会被保存在environment.yaml文件中。 conda env export > environment.yaml 当我们想再次创建该环境&#xff0c;或根据别人提供的.yaml文件复现环境时&#xff0c;就可以通过下面的命令来复现安装环境了。 conda …

【深度学习】Stable Diffusion

Stable Diffusion原理&#xff1a; https://zhuanlan.zhihu.com/p/632866251 训练过程&#xff1a; 文生图方法&#xff1a; 图生图方法&#xff1a;

CSS实现多头像叠加ui效果

第一种实现方式 简单粗暴直接使用margin-right实现&#xff0c;缺点是第一行右侧最右边头像溢出容器&#xff0c;代码中的三行注释的代码放开后可解决这个问题。 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8">&…

SQLyog登录错误解决

sqlyog登录错误号码2058解决 登录本地MySQL数据库 Microsoft Windows [版本 6.1.7601] 版权所有 (c) 2009 Microsoft Corporation。保留所有权利。C:\Users\Administrator>mysql -u root -p Enter password: **** Welcome to the MySQL monitor. Commands end with ; or \…