go channel 详解

一、概述

在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 <- 10fmt.Println("发送成功")
}

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

fatal error: all goroutines are asleep - deadlock!

为什么会出现deadlock错误呢?

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

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

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

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

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

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

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

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

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

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

func main(){ch := make(chan int ,1)  // 创建一个容量为 1 的有缓冲区的通道ch <- 10fmt.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=falseif !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, 1for {select {case c <- x:x, y = y, x+ycase <-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+ycase <-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.Cfmt.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.hqwc.cn/news/3277.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Kafka

资料来源视频&#xff1a;尚硅谷-Kafka3.x教程 一、Kafka概述 1.1 定义 Kafka传统定义:Kafka是一个分布式的基于发布/订阅模式的消息队列(MessageQueue)&#xff0c;主要应用于大数据实时处理领域。 发布订阅:消息的发布者不会将消息直接发送给特定的订阅者&#xff0c;而是…

工程安全监测无线振弦采集仪在建筑物的应用

工程安全监测无线振弦采集仪在建筑物的应用 工程安全监测无线振弦采集仪是一种先进的监测设备&#xff0c;可以在建筑物、桥梁、隧道、大坝等工程施工或运营中进行振动监测和安全评估。其在建筑物的应用主要是针对建筑物结构的振动和变形进行监测和分析&#xff0c;以确保建筑…

C语言进阶---自定类型详解(结构体+枚举+联合)

结构体 1、结构体类型的声明 1.1、结构的基础知识 结构是一些值得集合&#xff0c;这些值称为成员变量。结构的每个成员可以是不同类型的变量。 1.2、结构体类型的声明 struct tag {member-list; }variable-list;//写法一&#xff1a; struct Stu {char name[20];int age;…

在windows环境下安装支持CUDA的opencv-python

文章目录 附件&#xff1a;GPU和CUDA的关系 —— 开发人员通过CUDA可以使用GPU的计算能力来加速各种计算任务&#xff0c;并提高计算性能和效率。一、环境配置&#xff08;0&#xff09;我的电脑配置环境&#xff08;1&#xff09;CUDA cuDNN下载与安装&#xff08;2&#xff…

NXP i.MX 8M Mini工业核心板硬件说明书(四核ARM Cortex-A53 + 单核ARM Cortex-M4,主频1.6GHz)

1 硬件资源 创龙科技SOM-TLIMX8是一款基于NXP i.MX 8M Mini的四核ARM Cortex-A53 单核ARM Cortex-M4异构多核处理器设计的高端工业级核心板&#xff0c;ARM Cortex-A53(64-bit)主处理单元主频高达1.6GHz&#xff0c;ARM Cortex-M4实时处理单元主频高达400MHz。处理器…

【日志加载 log4j】

日志 笔记记录 1. 日志介绍2. 日志体系结构3.Log4j开发流程4.Log4j组成4.1 Loggers 记录器4.2 Appenders 输出源4.3 Layouts 布局5. 配置文件 log4j.properties 1. 日志介绍 2. 日志体系结构 3.Log4j开发流程 1.引入依赖<dependency><groupId>log4j</groupId>…

星辰秘典:解开Python项目的神秘面纱——迷宫之星(迷宫探索与求解)

✨博主&#xff1a;命运之光 &#x1f338;专栏&#xff1a;Python星辰秘典 &#x1f433;专栏&#xff1a;web开发&#xff08;html css js&#xff09; ❤️专栏&#xff1a;Java经典程序设计 ☀️博主的其他文章&#xff1a;点击进入博主的主页 前言&#xff1a;你好&#x…

【视频文稿】车载Android应用开发与分析 - AIDL实践与封装(上)

本期视频地址 &#xff1a; 车载Android应用开发与分析 - AIDL实践与封装&#xff08;上&#xff09;_哔哩哔哩_bilibili 开发手机APP时我们一般都是写一个独立的应用&#xff0c;很少会涉及到除了系统服务以外的多个进程间交互的情况&#xff0c;但开发车载应用则不同&#x…

Kafka 为什么那么快?

有人说&#xff1a;他曾在一台配置较好的机子上对 Kafka 进行性能压测&#xff0c;压测结果是 Kafka 单个节点的极限处理能力接近每秒 2000万 条消息&#xff0c;吞吐量达到每秒 600MB。 那 Kafka 为什么这么快&#xff1f;如何做到这个高的性能&#xff1f; 本篇文章主要从这…

Restful风格笔记

Restful风格知识点 RestController注解 在类上添加RestController可以默认类中的所有方法都带有ResponseBody注解&#xff0c;可以省去一个个添加的麻烦。 RestController RequestMapping("/restful") //CrossOrigin(origins {"http://localhost:8080"…

Java基础——正则表达式

1 概述 正则表达式用于匹配规定格式的字符串。 除了上面的以外&#xff0c;还有一个符号就是括号&#xff0c;括号括起来的表示一个捕获组&#xff0c;一个捕获组可以作为一个重复单位来处理。 2 使用 2.1 判断是否匹配 String自带了一个可以使用正则表达式判断字符串是…

随着人工智能时代的到来,算力需求的成倍增长成为新的趋势

方向一&#xff1a;AI与算力相辅相成 人工智能&#xff08;Artificial Intelligence&#xff09;&#xff0c;英文缩写为AI。它是研究、开发用于模拟、延伸和扩展人的智能的理论、方法、技术及应用系统的一门新的技术科学。人工智能是新一轮科技革命和产业变革的重要驱动力量。…