【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的用法,并在实际编程中灵活运用。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。