✨✨ 欢迎大家来到景天科技苑✨✨
?? 养成好习惯,先赞后看哦~??
? 作者简介:景天科技苑
?《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,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的声明语法如下:
var 变量名 chan 元素类型
例如:
var ch1 chan int // 声明一个int类型的Channel var ch2 chan string // 声明一个string类型的Channel
Channel是引用类型,其默认值为nil。如果一个Channel只声明没有初始化,那么直接使用这个Channel会触发死锁。因此,我们需要使用make函数来初始化Channel。
Channel的初始化语法如下:
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 mainimport "fmt"func main() { //在主Goroutine中定义通道 ch := make(chan int) go func() { ch <- 42 // 在另一个Goroutine中发送数据 }() value := <-ch // 在主Goroutine中 从Channel中接收数据 fmt.Println(value) // 输出42}
3. 关闭操作
关闭操作使用close函数来关闭一个Channel,语法如下:
close(ch)
关闭后的Channel仍然可以从其中接收数据,但不能再向其发送数据。如果向一个已关闭的Channel发送数据,会引发panic。
从这个关闭的channel中不但可以读取出已发送的数据,还可以不断的读取零值:
c := make(chan int, 10)c <- 1c <- 2close(c)fmt.Println(<-c) //1fmt.Println(<-c) //2fmt.Println(<-c) //0fmt.Println(<-c) //0
但是如果通过range读取,channel关闭后for循环会跳出:
c := make(chan int, 10)c <- 1c <- 2close(c)for i := range c { fmt.Println(i)}
通过i, ok := <-c可以查看Channel的状态,判断值是零值还是正常读取的值。
ok 判断chan的状态是否是关闭,如果是关闭,不会再取值了。
ok, 如果是true,就代表我们还在读数据
ok, 如果是fasle,就说明该通道已关闭
c := make(chan int, 10)close(c)i, ok := <-cfmt.Printf("%d, %t", i, ok) //0, false
关闭通道综合应用
告诉接收方,我不会再有其他数据发送到chan了。
package mainimport ( "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来遍历通道,返回只有一个数据,就是每次循环读取的通道中的数据
package mainimport ( "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中存放,取值,会造成死锁
package mainimport "fmt"//单线程中,即便往通道中放值,并且从通道中取值,还是会造成死锁//存放与取值,必须发生在不同goroutine中才不会造成死锁func main() { ch := make(chan int) ch <- 2 data := <-ch fmt.Println(data)}
存放值,不取值,造成死锁
package mainimport ( "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的语法如下:
var ch = make(chan<type>, capacity)
其中,capacity是缓冲区的大小。
chan如果只有一个容量,老是阻塞,效率是很低的。
package mainimport ( "fmt" "strconv" "time")// 缓冲通道 chan,capfunc 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的声明语法如下:
var ch chan<- int // 只能发送int类型数据到Channel中 send-only channel 只能写var ch <-chan int // 只能从Channel中接收int类型数据 receive-only channel 只能读
示例代码:
package mainimport ( "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接收数据。
单向通道应用场景二:
package mainimport ( "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数据,无论谁先放入,我们就用谁,抛弃其他的
示例代码:
package mainimport ( "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的用法,并在实际编程中灵活运用。