✨✨ 欢迎大家来到景天科技苑✨✨
🎈🎈 养成好习惯,先赞后看哦~🎈🎈
🏆 作者简介:景天科技苑
🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。
🏆《博客》:Python全栈,Golang开发,PyQt5和Tkinter桌面开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi,flask等框架,云原生K8S,linux,shell脚本等实操经验,网站搭建,数据库等分享。所属的专栏:Go语言开发零基础到高阶实战
景天的主页:景天科技苑
文章目录
Go多线程数据通信Channel
在Go语言中,Channel是一种强大的并发通信工具,用于在Goroutine之间安全地传递数据。
通过Channel,我们可以实现并发通信和同步操作,确保数据的安全传输。
本文将详细介绍Go语言中的Channel,包括其创建、发送、接收、关闭等操作,以及一些常见的使用场景和高级特性。
一、Channel的基本概念
Channel是Go语言中的一种特殊类型,用于在不同Goroutine之间传递数据。它类似于数据结构中的队列,其中的元素遵循先入先出的规则。
每个Channel在声明时需要指定其传递的元素类型,之后该Channel只能发送或接收对应类型的数据。
通道:可以被认为是 Goroutine 通信管道。
类似于水管,数据可以从一端流到另一端。
Go语言不建议我们使用锁机制来解决多线程问题、建议我们使用通道
不要通过共享内存来通信(锁),而应该通过通信来共享内存(chan) 这是一句风靡golang社区的经典语。
一个goroutine需要将一些信息告诉另外一个goroutine ,就直接将数据信息放入chan即可。
Channel的声明语法如下:
var 变量名 chan 元素类型
例如:
var ch1 chanint// 声明一个int类型的Channel var ch2 chanstring// 声明一个string类型的Channel
Channel是引用类型,其默认值为nil。如果一个Channel只声明没有初始化,那么直接使用这个Channel会触发死锁。因此,我们需要使用make函数来初始化Channel。
Channel的初始化语法如下:
ch :=make(chan 元素类型,[缓冲大小])
缓冲大小是可选的,如果不指定,则默认是无缓冲的Channel。
二、Channel的基本操作
Channel有三种基本操作:发送、接收和关闭。
1. 发送操作
发送操作是指向Channel发送一个值的操作。语法如下:
ch <- 值
例如:
ch :=make(chanint)
ch <-42// 将42发送到Channel ch中
2. 接收操作
接收操作从Channel中接收一个值
<-ch用来从channel ch中接收数据,这个表达式会一直被block,直到有数据可以接收。
从一个nil channel中接收数据会一直被block。
从一个被close的channel中接收数据不会被阻塞,而是立即返回,接收完已发送的数据后会返回元素类型的零值(zero value)。
如前所述,你可以使用一个额外的返回参数来检查channel是否关闭。
语法如下:
值 :=<- ch
或者,如果只接收值但不使用结果,可以写成:
<- ch
例如:
package main
import"fmt"funcmain(){//在主Goroutine中定义通道
ch :=make(chanint)gofunc(){
ch <-42// 在另一个Goroutine中发送数据}()
value :=<-ch // 在主Goroutine中 从Channel中接收数据
fmt.Println(value)// 输出42}
3. 关闭操作
关闭操作使用close函数来关闭一个Channel,语法如下:
close(ch)
关闭后的Channel仍然可以从其中接收数据,但不能再向其发送数据。如果向一个已关闭的Channel发送数据,会引发panic。
从这个关闭的channel中不但可以读取出已发送的数据,还可以不断的读取零值:
c :=make(chanint,10)
c <-1
c <-2close(c)
fmt.Println(<-c)//1
fmt.Println(<-c)//2
fmt.Println(<-c)//0
fmt.Println(<-c)//0
但是如果通过range读取,channel关闭后for循环会跳出:
c :=make(chanint,10)
c <-1
c <-2close(c)for i :=range c {
fmt.Println(i)}
通过i, ok := <-c可以查看Channel的状态,判断值是零值还是正常读取的值。
ok 判断chan的状态是否是关闭,如果是关闭,不会再取值了。
ok, 如果是true,就代表我们还在读数据
ok, 如果是fasle,就说明该通道已关闭
c :=make(chanint,10)close(c)
i, ok :=<-c
fmt.Printf("%d, %t", i, ok)//0, false
关闭通道综合应用
告诉接收方,我不会再有其他数据发送到chan了。
package main
import("fmt""time")// 关闭通道// 告诉接收方,我不会再有其他数据发送到chan了。funcmain(){// 在main线程中定义的通道
ch1 :=make(chanint)gotest7(ch1)// 循环读取chan中的数据,直到检测到通道关闭,就不再从通道中取数据,实现了向通道发送数据与取数据的联动for{
time.Sleep(time.Second)// ok 判断chan的状态是否是关闭,如果是关闭,不会再取值了。// ok, 如果是true,就代表我们还在读数据// ok, 如果是false,就说明该通道已关闭
data, ok :=<-ch1
if!ok {
fmt.Println("读取完毕", ok)break}
fmt.Println("ch1 data:", data)}}// 通道可以参数传递functest7(ch chanint){for i :=0; i <10; i++{
ch <- i
}// 关闭通道,告诉接收方,不会在往ch中放入数据close(ch)}
执行流程图
通过for range简化
读取chan中的数据, for 一个个取,并且会自动判断chan是否close 迭代器
通过for range来遍历通道,返回只有一个数据,就是每次循环读取的通道中的数据
package main
import("fmt""time")// 关闭通道// 告诉接收方,我不会再有其他数据发送到chan了。funcmain(){// 在main线程中定义的通道
ch1 :=make(chanint)gotest8(ch1)// 读取chan中的数据, for 一个个取,并且会自动判断chan是否close 迭代器//通过for range来遍历通道,返回只有一个数据,就是每次循环读取的通道中的数据for data :=range ch1 {
time.Sleep(time.Second)
fmt.Println(data)}
fmt.Println("end")}// 通道可以参数传递functest8(ch chanint){for i :=0; i <10; i++{
ch <- i
}// 关闭通道,告诉接收方,不会在往ch中放入数据close(ch)}
三、通道的阻塞与死锁
1. 通道的阻塞
一个通道发送和接收数据,默认是阻塞的。
当一个数据被发送到通道时,在发送语句中被阻塞,直到另一个Goroutine从该通道读取数据。
相对地,当从通道读取数据时,读取被阻塞,直到一个Goroutine将数据写入该通道。
本身channel就是同步的, 意味着同一时间,只能有一条goroutine来操作。
最后:通道是goroutine之间的连接,**所有通道的发送和接收必须处在不同的goroutine中,如果在同一个Goroutine中,代码运行将会报死锁的错 all goroutines are asleep -deadlock!**。
这些通道的特性是帮助Goroutines有效地进行通信,而无需像使用其他编程语言中非常常见的显式锁或条件变量。
2. 通道的死锁
死锁并不是锁的一种,而是一种错误使用锁导致的现象,死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
系统发生死锁现象不仅浪费大量的系统资源,甚至导致整个系统崩溃,带来灾难性后果。所以,对于死锁问题在理论上和技术上都必须予以高度重视。
如果创建了chan,没有 Goroutine 来使用了,则会出现死锁。
使用通道时要考虑的一一个重要因素是死锁。如果Goroutine在一 个通道 上发送数据,那么预计其他的Goroutine应该接收数据。如果这种情况不发生,那么程序将在运行时出现死锁。
类似地,如果Goroutine 正在等待从通道接收数据,那么另一些Goroutine将会在该通道上写入数据,否则程序将会死锁。
存放与取值必须同时存在,并且在不同的goroutine中,才不会造成死锁
单单只有存放,或者只有取值,或者存放与取值都在同一goroutine中,都会造成死锁
造成死锁的几种情况:
有且只有一个协程时,无缓冲的通道
先发送会阻塞在发送,先接收会阻塞在接收处。
发送操作在接收者准备好之前是阻塞的,接收操作在发送之前是阻塞的,
解决办法就是改为缓冲通道,或者使用协程配对
单一goroutine中存放,取值,会造成死锁
package main
import"fmt"//单线程中,即便往通道中放值,并且从通道中取值,还是会造成死锁//存放与取值,必须发生在不同goroutine中才不会造成死锁funcmain(){
ch :=make(chanint)
ch <-2
data :=<-ch
fmt.Println(data)}
存放值,不取值,造成死锁
package main
import("fmt")// 定义通道 chan// 这个 goroutine 希望告诉 main 线程,我还没结束。(通信)funcmain(){// 定一个bool的通道var ch chanbool
ch =make(chanbool) 在一个goroutine中去往通道中放入数据gofunc(){for i :=0; i <10; i++{
fmt.Println("goroutine-", i)}//time.Sleep(time.Second * 3)
ch <-true}()// 定义好通道之后,如果没有 goroutine来使用(必须在两个及以上goroutine),那么就会产生死锁// deadlock!
data :=<-ch
fmt.Println("ch data:", data)// 死锁的产生,没有goroutine来消耗通道(存取)
ch2 :=make(chanint)
ch2 <-10}
在主goroutine中定义的ch2通道没有另外一个goroutine使用,造成了死锁
四、缓冲通道
非缓冲通道
上面我们讲的通道都是无缓冲通道,只能放一个数据,无缓冲的Channel也称为同步Channel。在无缓冲的Channel中,发送操作和接收操作必须同时准备就绪,否则会被阻塞。
发送和接受都是阻塞的。一次发送对应一个接收。
缓冲通道
有缓冲的Channel也称为异步Channel。它允许在缓冲区未满的情况下发送多个数据,直到缓冲区满为止。
通道带了一个缓冲区,发送的数据直到缓冲区填满为止,才会被阻塞,接收的也是,只有缓冲区清空,才会阻塞。
缓冲区通道,放入数据,不会产生死锁,它不需要等待另外的线程来拿,它可以放多个数据。
如果缓冲区满了,还没有人取,也会产生死锁。
缓冲通道可以在同一个goroutine中发送数据和接收数据
可以通过len来判断缓冲通道中的数据数量
创建有缓冲的Channel的语法如下:
var ch =make(chan<type>, capacity)
其中,capacity是缓冲区的大小。
chan如果只有一个容量,老是阻塞,效率是很低的。
package main
import("fmt""strconv""time")// 缓冲通道 chan,capfuncmain(){// 非缓冲通道
ch1 :=make(chanint)//非缓冲通道默认的大小和容量都为0值
fmt.Println(cap(ch1),len(ch1))// 0 0//非缓冲通道只能在不同的goroutine中存放和取值,否则报死锁错误//ch1 <- 100////v := <-ch1//fmt.Println(v)// 缓冲通道// 缓冲区通道,放入数据,不会产生死锁,它不需要等待另外的线程来拿,它可以放多个数据。// 如果缓冲区满了,还没有人取,也会产生死锁。// 缓冲通道可以在同一个goroutine中发送数据和接收数据
ch2 :=make(chanstring,5)
fmt.Println(cap(ch2),len(ch2))// 5 0
ch2 <-"1"
fmt.Println(cap(ch2),len(ch2))// 5 1 , 可以通过len来判断缓冲通道中的数据数量
ch2 <-"2"
ch2 <-"3"
fmt.Println(cap(ch2),len(ch2))// 5 3
ch2 <-"4"
ch2 <-"5"
fmt.Println(cap(ch2),len(ch2))// 5 5//缓冲通道可以在同一个goroutine中发送数据和接收数据
data :=<-ch2
ch2 <-"6"// 向通道中存数据,如果一直存没取,当存满,继续存时,会报死锁deadlock!
fmt.Println("缓冲通道取出的数据:", data)//1 先进先出,根据放入数据的先后顺序取出数据
ch3 :=make(chanstring,4)gotest9(ch3)
fmt.Println("--------------------------")for s :=range ch3 {
time.Sleep(time.Second)
fmt.Println("main中读取的数据:", s)}
fmt.Println("main-end")}functest9(ch chanstring){for i :=0; i <10; i++{
ch <-"test - "+ strconv.Itoa(i)
fmt.Println("子goroutine放入数据:","test - "+strconv.Itoa(i))}close(ch)}
缓冲通道,可以定义缓冲区的数量
如果缓冲区没有满,可以继续存放,如果满了,也会阻塞等待
如果缓冲区空的,读取也会等待,如果缓冲区中有多个数据,依次按照先进先出的规则进行读取。
如果缓冲区满了,同时有两个线程在读或者写,这个时候和普通的chan一样。一进一出。
五、定向通道
双向通道
channel 是用来实现 goroutine 通信的。一个写、一个读、这是双向通道,上面我们讲的都是双向通道。
单向Channel
在并发编程中,有时需要在不同的函数中对Channel进行限制,例如只允许发送或只允许接收。这时可以使用单向Channel。
单向Channel的声明语法如下:
var ch chan<-int// 只能发送int类型数据到Channel中 send-only channel 只能写var ch <-chanint// 只能从Channel中接收int类型数据 receive-only channel 只能读
示例代码:
package main
import("fmt""time")// 只发送的通道funcsend(ch chan<-int){for i :=0; i <10; i++{
ch <- i
fmt.Println("发送的值:", i)}close(ch)}// 只接收的通道funcreceive(ch <-chanint){for x :=range ch {
fmt.Println("接收到的值:", x)}}funcmain(){
ch :=make(chanint,2)gosend(ch)goreceive(ch)
time.Sleep(time.Second)}
在这个例子中,send函数只能向Channel发送数据,而receive函数只能从Channel接收数据。
单向通道应用场景二:
package main
import("fmt""time")// 单向通道使用场景funcmain(){
ch1 :=make(chanint)// 可读可写gowriteOnly(ch1)goreadOnly(ch1)
time.Sleep(time.Second *3)}// 作为函数的参数或者返回值之类的。// 指定函数去写,不让他读取,防止通道滥用funcwriteOnly(ch chan<-int){// 函数的内部,处理一些写数据的操作
ch <-100}// 指定函数去读,不让他写,防止通道滥用funcreadOnly(ch <-chanint)int{// 取出通道的值,做一些操作,不可写的。
data :=<-ch
fmt.Println(data)return data
}
七、使用select语句监听多个Channel
select选择语句可以用于监听多个Channel的操作,以实现非阻塞的并发控制。
select只能用在通道中,它的语法类似于switch语句,但case分支中处理的是Channel的发送和接收操作。
读取chan数据,无论谁先放入,我们就用谁,抛弃其他的
示例代码:
package main
import("fmt""time")funcmain(){
ch1 :=make(chanstring)
ch2 :=make(chanstring)gofunc(){
time.Sleep(2* time.Second)
ch1 <-"Hello"}()gofunc(){
time.Sleep(1* time.Second)
ch2 <-"World"}()// 读取chan数据,无论谁先放入,我们就用谁,抛弃其他的.// select 和 swtich差不多, 只是select在通道中使用,case表达式需要是一个通道结果//如果上面的结果还处于阻塞中,就会先执行defaultselect{case msg1 :=<-ch1:
fmt.Println(msg1)case msg2 :=<-ch2:
fmt.Println(msg2)//default:// fmt.Println("default")}}
在上述代码中,select语句会监听两个Channel ch1和ch2。由于ch2的发送操作先完成,因此会先接收到"World"并打印出来。
select用法总结
1、每一个case必须是一个通道的操作 <-
2、所有chan操作都有要结果(通道表达式都必须会被求值)
3、如果任意的通道拿到了结果。它就会立即执行该case、其他就会被忽略
4、如果有多个case都可以运行,select是随机选取一个执行,其他的就不会执行。
5、如果存在default,执行该语句,如果不存在,阻塞等待 select 直到某个通道可以运行。
八、Channel的常见使用场景
线程间的数据共享和通信
Channel可以用于在不同Goroutine之间共享和传递数据,实现线程间的通信。
任务的并发执行和结果汇总
可以使用Channel来协调多个Goroutine并发执行任务,并将结果汇总到主Goroutine中。
九、总结
Channel是Go语言中一种强大的并发通信工具,通过创建、发送、接收和关闭Channel,可以实现并发通信和同步操作,确保数据的安全传输。本文详细介绍了Channel的基本操作、高级特性以及常见使用场景,并通过多个案例展示了Channel的实际应用。希望读者通过本文的学习,能够掌握Channel的用法,并在实际编程中灵活运用。
版权归原作者 景天科技苑 所有, 如有侵权,请联系我们删除。