当前位置: 首页 > news >正文

[Golang] Channel

[Golang] Channel

文章目录

  • [Golang] Channel
    • 什么是Channel
    • channel的初始化
    • channel的操作
    • 双向channel和单向channel
    • 为什么有channel
    • 有缓冲channel和无缓冲channle
    • channel做一把锁

从之前我们知道go关键字可以开启一个Goroutine,但是Goroutine之间的通信还需要另一个条件:channel

什么是Channel

官方定义:Channel are typed conduit through which you can send and receive values with the channel operator.

channel是一个可以收发数据的管道。

channel的初始化

var channel_name chan channel_type
var channel_name [size]chan channel_type//声明一个容量为size的channel

例如:

var ch1 chan int
var ch2 [1]chan int

但是声明后的channel,我们没有进行初始化为其分配空间,其值为nil,我们还需要使用make函数来对其初始化,之后才可以在程序中使用该管道。

channel_name = make(chan channel_type)
channel_name = make(chan channel_type, size)

例如:

ch1 := make(chan int)
ch2 := make(chan int, 1)//一个容量为size的channel

channel的操作

发送数据:

ch := make(chan int)	// 创建管道
ch <- 1					// 向管道发送数据
v := <-ch				// 从管道读取数据,并存储的变量v
close(ch)				// 关闭管道

注意:用完管道后,我们需要关闭管道close(ch),避免程序一直等待以及资源的浪费。但是关闭的管道仍然能读取数据,如果管道中还有数据,那就可以读到实际的值;如果管道中没有数据,此时读取的值就是该类型的零值,不会阻塞等待数据。

比如,例:

package mainimport ("fmt""time"
)func main() {ch := make(chan int, 5)ch <- 1close(ch)go func() {for i := 0; i < 5; i++ {v := <-chfmt.Println(v)}}()time.Sleep(2 * time.Second)
}

执行结果:

image-20240913234519220

创建一个缓存为5的int类型管道,向管道里写入一个1之后关闭管道。开启一个Goroutine从管道中读取数据,读5次,我们可以看到从第二次开始,读到的数据一直是0。

但是如果我们想要向管道中写入0呢?

一般采用:

1.判断读取:

package mainimport ("fmt""time"
)func main() {ch := make(chan int, 5)ch <- 1close(ch)go func() {for i := 0; i < 5; i++ {v, ok := <-ch 						// 判断式读取if ok {fmt.Println("数据读完了", v)} else {fmt.Println("数据没读完", v)}}}()time.Sleep(2 * time.Second)
}

执行结果:

image-20240913234937999

我们在读取数据时,加上了一个ok进行判断。ok为true时,读取的是正常值;ok为false,读取的是零值。

2.for range 读取

有时,我们的读取是不知道次数的,只是在channel中进行读取,有数据我们就读,直到管道关闭。

此时可以使用for range 读取管道中的数据

package mainimport ("fmt""time"
)func main() {ch := make(chan int, 5)ch <- 1ch <- 2ch <- 3close(ch)go func() {for i := 0; i < 5; i++ {for v := range ch {fmt.Println(v)}}}()time.Sleep(2 * time.Second)
}

执行结果:

image-20240913235317638

我们向管道中只写入了1,2,3,三个数据,之后就关闭了管道。Goroutine中也只能读到三个数据,然后管道被关闭了,Goroutine的for range循环也就退出了。

双向channel和单向channel

channel根据其功能又能分为双向channel和单向channel,双向channel既可以发送数据又可以接收数据,单向channel要么是发送数据,要么是接收数据。

单向读channel

var ch = make(chan int)
type ReadChannel = <-chan int	// 给 <-chan int取个别名
var rec ReadChannel = ch

单向写channel

var ch = make(chan int)
type SendChannel = chan<- int
var sec SendChannel = ch

读channel与写channel在定义时只是<-的位置不同,读在chan关键字前,写在chan关键字后。

使用示例:

package mainimport ("fmt""time"
)type ReadChannel = <-chan int
type SendChannel = chan<- intfunc main() {var ch = make(chan int)defer close(ch)go func() {var rec ReadChannel = chv := <-recfmt.Println(v)}()go func() {var sec SendChannel = chsec <- 100}()time.Sleep(2 * time.Second)
}

执行结果:

image-20240914000632202

创建一个读channel,一个写channel,向写channel中写入100,从读channel中读取数据。

为什么有channel

Golang中有个重要的思想:不以共享内存来通信,以通信来共享内存,channel就是其特点。

也就是说,协程之间可以利用channel来传递数据,以下例子可以看出父子协程是如何通过channel通信的:

package mainimport ("fmt""time"
)func sum(nums []int, ch chan int) {cnt := 0for _, v := range nums {cnt += v}ch <- cnt
}func main() {var ch = make(chan int)defer close(ch)nums := []int{-5, 4, -3, 2, -1} // 和为-3go func() {sum(nums[:len(nums)/2], ch)}()go sum(nums[len(nums)/2:], ch)m, n := <-ch, <-chfmt.Println(m, n, m+n)time.Sleep(2 * time.Second)
}

执行结果:

image-20240914001709564

有缓冲channel和无缓冲channle

之前初始化时,我们已经说明了channel分为有缓冲和无缓冲两种。

为了协程安全,有缓冲channel和无缓冲channle的内部都有一把锁来控制并发访问。

同时,channel底层一定有一个队列来存储数据。

无缓冲channel可以理解为同步模式,即写入一个,如果消费者不消费,写入就会阻塞

有缓冲channel可以理解为异步模式,即写入消息后,即使没有被消费,只要队列没有满,就可以继续写入。

image-20240914002245442

此时如果,channel满了,异步就会退化为同步,发送还是会阻塞。如果一个channel长期处于满队列状态,就没必要使用有缓冲channel了,直接有无缓冲即可。

所以大部分情况,有缓冲的channel一般用来做异步操作。

  • 无缓冲channel:适合用于严格同步的场景,比如两个Goroutine之间进行同步,要严格确保操作顺序。

例如,两个协程循环打印A、B

package mainimport ("fmt""sync""time"
)func main() {var wg sync.WaitGroupwg.Add(2)var Ach = make(chan int)var Bch = make(chan int)defer close(Ach)defer close(Bch)go PrintA(&wg, Ach, Bch)go PrintB(&wg, Ach, Bch)Ach <- 1 // 从A启动wg.Wait()
}func PrintA(wg *sync.WaitGroup, Ach chan int, Bch chan int) {defer wg.Done()for {<-Achfmt.Println("A")time.Sleep(time.Second)Bch <- 1 // 通知打印B}
}func PrintB(wg *sync.WaitGroup, Ach chan int, Bch chan int) {defer wg.Done()for {<-Bchfmt.Println("B")time.Sleep(time.Second)Ach <- 1 // 通知打印A}
}
  • 有缓冲channel:适合一定程度的异步处理的场景,比如提高吞吐量,减少Goroutine之间的阻塞。

channel做一把锁

因为缓冲队列满了之后,再往channel中写数据就会被阻塞,所以我们可以把channel当一把锁来使用

package mainimport ("fmt""time"
)func main() {ch := make(chan bool, 1)var sum intfor i := 0; i < 1000; i++ {go add(ch, &sum)}time.Sleep(2 * time.Second)fmt.Println("sum = ", sum)
}func add(ch chan bool, sum *int) {ch <- true*sum = *sum + 1<-ch
}

执行结果:

image-20240914004020661

如果不用锁,循环次数一多就会出现并发问题。


http://www.mrgr.cn/news/26395.html

相关文章:

  • 三星ZFlip5/ZFlip4/W7024刷安卓14国行OneUI6.1系统-高级设置-韩/欧/港版
  • 【计网】从零开始使用UDP进行socket编程 --- 客户端与服务端的通信实现
  • Android 测试机
  • 链表之判空,删除
  • Android 12.0 Launcher修改density禁止布局改变功能实现
  • 微信小程序实现转盘抽奖,可以自定义编辑奖项列表
  • mysql学习教程,从入门到精通,SQL IN BETWEEN 运算符(13)
  • 51单片机快速入门之独立按键
  • Linux下抓包分析Java应用程序HTTP接口调用:基于tcpdump与Wireshark的综合示例
  • 吃透高频考点:Android中的ANR问题及其解决策略万字教程
  • 前端开发第三节课
  • 了解计算机安全性【技术、管理与法律】
  • shell 循环语句总结
  • Python之 条件与循环(Python‘s Conditions and loops)
  • 【遍历二叉树】---先,中,后,层序遍历 及 先序建立整树
  • 【Echarts】vue3打开echarts的正确方式
  • 【物联网】深入解析时序数据库TDengine及其Java应用实践
  • 每日OJ_牛客_数字统计(简单模拟)
  • 查找日志关键字
  • Linux 中System V IPC的共享内存