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

[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 main

import (
	"fmt"
	"time"
)

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

执行结果:

image-20240913234519220

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

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

一般采用:

1.判断读取:

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int, 5)
	ch <- 1
	close(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 main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int, 5)
	ch <- 1
	ch <- 2
	ch <- 3
	close(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 main

import (
	"fmt"
	"time"
)

type ReadChannel = <-chan int
type SendChannel = chan<- int

func main() {
	var ch = make(chan int)
	defer close(ch)
	go func() {
		var rec ReadChannel = ch
		v := <-rec
		fmt.Println(v)
	}()
	go func() {
		var sec SendChannel = ch
		sec <- 100
	}()

	time.Sleep(2 * time.Second)
}

执行结果:

image-20240914000632202

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

为什么有channel

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

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

package main

import (
	"fmt"
	"time"
)

func sum(nums []int, ch chan int) {
	cnt := 0
	for _, v := range nums {
		cnt += v
	}
	ch <- cnt
}

func main() {
	var ch = make(chan int)
	defer close(ch)
	nums := []int{-5, 4, -3, 2, -1} // 和为-3
	go func() {
		sum(nums[:len(nums)/2], ch)
	}()
	go sum(nums[len(nums)/2:], ch)
	m, n := <-ch, <-ch
	fmt.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 main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	var wg sync.WaitGroup
	wg.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 {
		<-Ach
		fmt.Println("A")
		time.Sleep(time.Second)
		Bch <- 1 // 通知打印B
	}
}

func PrintB(wg *sync.WaitGroup, Ach chan int, Bch chan int) {
	defer wg.Done()
	for {
		<-Bch
		fmt.Println("B")
		time.Sleep(time.Second)
		Ach <- 1 // 通知打印A
	}
}
  • 有缓冲channel:适合一定程度的异步处理的场景,比如提高吞吐量,减少Goroutine之间的阻塞。

channel做一把锁

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

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan bool, 1)

	var sum int
	for 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.kler.cn/a/306675.html

相关文章:

  • 实战指南:理解 ThreadLocal 原理并用于Java 多线程上下文管理
  • 出海攻略,如何一键保存Facebook视频素材
  • 物联网(RFID)全景:被装信息化监控应用与挑战
  • 卸载一直显示在运行的应用
  • JavaScript数组去重的实用方法汇总
  • git status 命令卡顿的排查
  • Sourcetree安装教程及使用
  • C8T6超绝模块--DMA
  • 【面向对象】
  • 玄机科技浪漫绘情缘:海神缘下,一吻定情
  • 门检测系统源码分享
  • Java笔记 【1】docker introduction
  • MySQl篇(SQL - 基本介绍)(持续更新迭代)
  • 嵌入式硬件基础知识
  • 微信小程序中巧妙使用 wx:if 和 catchtouchmove 实现弹窗禁止页面滑动功能
  • 安卓玩机工具-----无需root权限 卸载 禁用 删除当前机型app应用 ADB玩机工具
  • 用 defineAsyncComponent 实现高效异步组件加载
  • 使用 SSM 框架编写的解决方案
  • Python应用指南:获取行政区最小外接矩形
  • ubuntu 安装 chrome 及 版本匹配的 chromedriver
  • vue3+vite项目中使用阿里图标库(svg)图标
  • NX CAM二次开发-创建程序组
  • Linux套接字
  • Python Web 开发中的性能优化策略(一)
  • Java多线程面试精讲:源于技术书籍的深度解读
  • uniapp+vue3 使用canvas,并保存图片(图片是空白的问题)