当前位置:首页 » 《关注互联网》 » 正文

【Golang】Go高并发之线程间数据通信Channel原理解析与应用实战

18 人参与  2024年10月15日 08:41  分类 : 《关注互联网》  评论

点击全文阅读


在这里插入图片描述

✨✨ 欢迎大家来到景天科技苑✨✨

?? 养成好习惯,先赞后看哦~??

? 作者简介:景天科技苑
?《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,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的用法,并在实际编程中灵活运用。


点击全文阅读


本文链接:http://zhangshiyu.com/post/171939.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1