【Golang】Go高并发之线程间数据通信Channel原理解析与应用实战
CSDN 2024-10-14 09:35:01 阅读 52
✨✨ 欢迎大家来到景天科技苑✨✨
🎈🎈 养成好习惯,先赞后看哦~🎈🎈
🏆 作者简介:景天科技苑
🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。
🏆《博客》:Python全栈,Golang开发,PyQt5和Tkinter桌面开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi,flask等框架,云原生K8S,linux,shell脚本等实操经验,网站搭建,数据库等分享。
所属的专栏:Go语言开发零基础到高阶实战
景天的主页:景天科技苑
文章目录
Go多线程数据通信Channel一、Channel的基本概念二、Channel的基本操作1. 发送操作2. 接收操作3. 关闭操作
三、通道的阻塞与死锁1. 通道的阻塞2. 通道的死锁
四、缓冲通道五、定向通道七、使用select语句监听多个Channel八、Channel的常见使用场景九、总结
Go多线程数据通信Channel
在Go语言中,Channel是一种强大的并发通信工具,用于在Goroutine之间安全地传递数据。
通过Channel,我们可以实现并发通信和同步操作,确保数据的安全传输。
本文将详细介绍Go语言中的Channel,包括其创建、发送、接收、关闭等操作,以及一些常见的使用场景和高级特性。
一、Channel的基本概念
Channel是Go语言中的一种特殊类型,用于在不同Goroutine之间传递数据。它类似于数据结构中的队列,其中的元素遵循先入先出的规则。
每个Channel在声明时需要指定其传递的元素类型,之后该Channel只能发送或接收对应类型的数据。
通道:可以被认为是 Goroutine 通信管道。
类似于水管,数据可以从一端流到另一端。
Go语言不建议我们使用锁机制来解决多线程问题、建议我们使用通道
不要通过共享内存来通信(锁),而应该通过通信来共享内存(chan) 这是一句风靡golang社区的经典语。
一个goroutine需要将一些信息告诉另外一个goroutine ,就直接将数据信息放入chan即可。
Channel的声明语法如下:
<code>var 变量名 chan 元素类型
例如:
var ch1 chan int // 声明一个int类型的Channel
var ch2 chan string // 声明一个string类型的Channel
Channel是引用类型,其默认值为nil。如果一个Channel只声明没有初始化,那么直接使用这个Channel会触发死锁。因此,我们需要使用make函数来初始化Channel。
Channel的初始化语法如下:
<code>ch := make(chan 元素类型, [缓冲大小])
缓冲大小是可选的,如果不指定,则默认是无缓冲的Channel。
二、Channel的基本操作
Channel有三种基本操作:发送、接收和关闭。
1. 发送操作
发送操作是指向Channel发送一个值的操作。语法如下:
ch <- 值
例如:
ch := make(chan int)
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"
func main() { -- -->
//在主Goroutine中定义通道
ch := make(chan int)
go func() {
ch <- 42 // 在另一个Goroutine中发送数据
}()
value := <-ch // 在主Goroutine中 从Channel中接收数据
fmt.Println(value) // 输出42
}
3. 关闭操作
关闭操作使用close函数来关闭一个Channel,语法如下:
<code>close(ch)
关闭后的Channel仍然可以从其中接收数据,但不能再向其发送数据。如果向一个已关闭的Channel发送数据,会引发panic。
从这个关闭的channel中不但可以读取出已发送的数据,还可以不断的读取零值:
c := make(chan int, 10)
c <- 1
c <- 2
close(c)
fmt.Println(<-c) //1
fmt.Println(<-c) //2
fmt.Println(<-c) //0
fmt.Println(<-c) //0
但是如果通过range读取,channel关闭后for循环会跳出:
c := make(chan int, 10)
c <- 1
c <- 2
close(c)
for i := range c { -- -->
fmt.Println(i)
}
通过i, ok := <-c可以查看Channel的状态,判断值是零值还是正常读取的值。
ok 判断chan的状态是否是关闭,如果是关闭,不会再取值了。
ok, 如果是true,就代表我们还在读数据
ok, 如果是fasle,就说明该通道已关闭
<code>c := make(chan int, 10)
close(c)
i, ok := <-c
fmt.Printf("%d, %t", i, ok) //0, false
关闭通道综合应用
告诉接收方,我不会再有其他数据发送到chan了。
<code>package main
import (
"fmt"
"time"
)
// 关闭通道
// 告诉接收方,我不会再有其他数据发送到chan了。
func main() { -- -->
// 在main线程中定义的通道
ch1 := make(chan int)
go test7(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)
}
}
// 通道可以参数传递
func test7(ch chan int) {
for i := 0; i < 10; i++ {
ch <- i
}
// 关闭通道,告诉接收方,不会在往ch中放入数据
close(ch)
}
执行流程图
通过for range简化
读取chan中的数据, for 一个个取,并且会自动判断chan是否close 迭代器
通过for range来遍历通道,返回只有一个数据,就是每次循环读取的通道中的数据
<code>package main
import (
"fmt"
"time"
)
// 关闭通道
// 告诉接收方,我不会再有其他数据发送到chan了。
func main() { -- -->
// 在main线程中定义的通道
ch1 := make(chan int)
go test8(ch1)
// 读取chan中的数据, for 一个个取,并且会自动判断chan是否close 迭代器
//通过for range来遍历通道,返回只有一个数据,就是每次循环读取的通道中的数据
for data := range ch1 {
time.Sleep(time.Second)
fmt.Println(data)
}
fmt.Println("end")
}
// 通道可以参数传递
func test8(ch chan int) {
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中存放,取值,会造成死锁
<code>package main
import "fmt"
//单线程中,即便往通道中放值,并且从通道中取值,还是会造成死锁
//存放与取值,必须发生在不同goroutine中才不会造成死锁
func main() { -- -->
ch := make(chan int)
ch <- 2
data := <-ch
fmt.Println(data)
}
存放值,不取值,造成死锁
<code>package main
import (
"fmt"
)
// 定义通道 chan
// 这个 goroutine 希望告诉 main 线程,我还没结束。(通信)
func main() { -- -->
// 定一个bool的通道
var ch chan bool
ch = make(chan bool)
在一个goroutine中去往通道中放入数据
go func() {
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(chan int)
ch2 <- 10
}
在主goroutine中定义的ch2通道没有另外一个goroutine使用,造成了死锁
四、缓冲通道
非缓冲通道
上面我们讲的通道都是无缓冲通道,只能放一个数据,无缓冲的Channel也称为同步Channel。在无缓冲的Channel中,发送操作和接收操作必须同时准备就绪,否则会被阻塞。
发送和接受都是阻塞的。一次发送对应一个接收。
缓冲通道
有缓冲的Channel也称为异步Channel。它允许在缓冲区未满的情况下发送多个数据,直到缓冲区满为止。
通道带了一个缓冲区,发送的数据直到缓冲区填满为止,才会被阻塞,接收的也是,只有缓冲区清空,才会阻塞。
缓冲区通道,放入数据,不会产生死锁,它不需要等待另外的线程来拿,它可以放多个数据。
如果缓冲区满了,还没有人取,也会产生死锁。
缓冲通道可以在同一个goroutine中发送数据和接收数据
可以通过len来判断缓冲通道中的数据数量
创建有缓冲的Channel的语法如下:
<code>var ch = make(chan<type>, capacity)
其中,capacity是缓冲区的大小。
chan如果只有一个容量,老是阻塞,效率是很低的。
<code>package main
import (
"fmt"
"strconv"
"time"
)
// 缓冲通道 chan,cap
func main() { -- -->
// 非缓冲通道
ch1 := make(chan int)
//非缓冲通道默认的大小和容量都为0值
fmt.Println(cap(ch1), len(ch1)) // 0 0
//非缓冲通道只能在不同的goroutine中存放和取值,否则报死锁错误
//ch1 <- 100
//
//v := <-ch1
//fmt.Println(v)
// 缓冲通道
// 缓冲区通道,放入数据,不会产生死锁,它不需要等待另外的线程来拿,它可以放多个数据。
// 如果缓冲区满了,还没有人取,也会产生死锁。
// 缓冲通道可以在同一个goroutine中发送数据和接收数据
ch2 := make(chan string, 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(chan string, 4)
go test9(ch3)
fmt.Println("--------------------------")
for s := range ch3 {
time.Sleep(time.Second)
fmt.Println("main中读取的数据:", s)
}
fmt.Println("main-end")
}
func test9(ch chan string) {
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的声明语法如下:
<code>var ch chan<- int // 只能发送int类型数据到Channel中 send-only channel 只能写
var ch <-chan int // 只能从Channel中接收int类型数据 receive-only channel 只能读
示例代码:
package main
import (
"fmt"
"time"
)
// 只发送的通道
func send(ch chan<- int) { -- -->
for i := 0; i < 10; i++ {
ch <- i
fmt.Println("发送的值:", i)
}
close(ch)
}
// 只接收的通道
func receive(ch <-chan int) {
for x := range ch {
fmt.Println("接收到的值:", x)
}
}
func main() {
ch := make(chan int, 2)
go send(ch)
go receive(ch)
time.Sleep(time.Second)
}
在这个例子中,send函数只能向Channel发送数据,而receive函数只能从Channel接收数据。
单向通道应用场景二:
<code>package main
import (
"fmt"
"time"
)
// 单向通道使用场景
func main() { -- -->
ch1 := make(chan int) // 可读可写
go writeOnly(ch1)
go readOnly(ch1)
time.Sleep(time.Second * 3)
}
// 作为函数的参数或者返回值之类的。
// 指定函数去写,不让他读取,防止通道滥用
func writeOnly(ch chan<- int) {
// 函数的内部,处理一些写数据的操作
ch <- 100
}
// 指定函数去读,不让他写,防止通道滥用
func readOnly(ch <-chan int) int {
// 取出通道的值,做一些操作,不可写的。
data := <-ch
fmt.Println(data)
return data
}
七、使用select语句监听多个Channel
select选择语句可以用于监听多个Channel的操作,以实现非阻塞的并发控制。
select只能用在通道中,它的语法类似于switch语句,但case分支中处理的是Channel的发送和接收操作。
读取chan数据,无论谁先放入,我们就用谁,抛弃其他的
示例代码:
<code>package main
import (
"fmt"
"time"
)
func main() { -- -->
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(2 * time.Second)
ch1 <- "Hello"
}()
go func() {
time.Sleep(1 * time.Second)
ch2 <- "World"
}()
// 读取chan数据,无论谁先放入,我们就用谁,抛弃其他的.
// select 和 swtich差不多, 只是select在通道中使用,case表达式需要是一个通道结果
//如果上面的结果还处于阻塞中,就会先执行default
select {
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的用法,并在实际编程中灵活运用。
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。