深入理解 Go Channel:解密并发编程中的通信机制

一、Channel管道
1、Channel说明
  • 共享内存交互数据弊端
    • 单纯地将函数并发执行是没有意义的。函数与函数间需要交互数据才能体现编发执行函数的意义
    • 虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发送静态问题
    • 为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题
  • channel好处
    • Go语言中的通道(channel)是一种特殊的类型
    • 通道像一个传送带或者队列,总是遵循先入先出(First In First Out)原则,保证收发数据的顺序
    • 每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型
    • 如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接
    • channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制
2、channel类型
  • channel是一种类型,一种引用类型
  • 声明管道类型的格式如下
Go 复制代码
var 变量 chan 元素类型
var ch1 chan int        //声明一个传递整型的管道
var ch2 chan bool        //声明一个传递布尔型的管道
var che chan []int        //声明一个传递int切片的管道
3、创建channel
  • 声明的管道后需要使用make函数初始化之后才能使用
  • 创建channel的格式如下:make(chan 元素类型,容量)
Go 复制代码
//创建一个能存储10个int类型数据的管道
ch1 := make(chan int,10)
//创建一个能存储4个bool类型数据的管道
ch2 := make(chan bool,4)
//创建一个能存储3个[]int切片类型数据的管道
ch3 := make(chan []int,3)

二、channel操作

  • 管道有发送(send)、接收(receive)和关闭(close)三种操作
  • 发送和接收使用<-符合
  • 现在我们先使用以下语句定义一个管道
Go 复制代码
ch := make(chan int,3)
1、发送(将数据放在管道内)
  • 将一个值发送到管道中
Go 复制代码
ch <- 10    //把10发送到ch中
2、接收(从管道内取值)
  • 从一个管道中接收值
Go 复制代码
x := <- ch    //从ch中接收值并赋值给变量x
<- ch        //从ch中接收值,忽略结果
3、关闭管道
  • 我们通过调用内置的close函数来关闭管道:close(ch)
  • 关于关闭管道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭管道
  • 管道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭管道不是必须的
  • 关闭后的管道有以下特点:
    • 对一个关闭的管道再发送值就是导致panic
    • 对一个关闭的管道进行接收会一直获取值直到管道为空
    • 对一个关闭的并且没有值的管道执行接收操作会得到对应类型的零值
    • 关闭一个已经关闭的管道会导致panic
4、管道阻塞
1、无缓冲的管道
  • 如果创建管道的时候没有指定容量,那么我们可以叫这个管道为无缓冲的管道
  • 无缓冲的管道有称为阻塞的管道
Go 复制代码
package main
import "fmt"

func main() {
	ch := make(chan int)
	ch <- 10
	fmt.Println("发送成功")
}
/*
-- 面这段代码能够通过编译,但是执行的时候会出现以下错误
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
	E:/_000/Go/Code/demo1.go:6 +0x5f
 */
2、有缓冲的管道
  • 解决上面问题的方法还有一种就是使用有缓冲区的管道
  • 我们可以在使用make函数初始化管道的时候为其指定管道的容量
  • 只要管道的容量大于零,那么该管道那么该管道就是有缓冲的管道,管道的容量表示管道中能存放元素的数量。
  • 就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。
Go 复制代码
package main
import "fmt"

func main() {
	ch := make(chan int, 5)
	ch <- 10
	ch <- 12
	fmt.Println("发送成功")
}

三、从 Channel 取值

1、优雅的从channel取值
  • 当通过通道发送有限的数据时,我们可以通过close函数关闭通道来告知从该通道接收值的goroutine停止等待。
  • 当通道被关闭时,往该通道发送值会引发panic,从该通道里接收的值一直都是类型零值。
  • 那如何判断一个通道是否被关闭了呢?
  • for range的方式判断通道关闭
Go 复制代码
package main

import (
	"fmt"
)

func f1(ch1 chan int) {
	for i := 0; i < 100; i++ {
		ch1 <- i
	}
	close(ch1)
}

func f2(ch1 chan int, ch2 chan int) {
	for {
		i, ok := <-ch1 // 通道关闭后再取值ok=false
		if !ok {
			break
		}
		ch2 <- i * i
	}
	close(ch2)
}

// channel 练习
func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)
	// 开启goroutine将0~100的数发送到ch1中
	go f1(ch1)
	// 开启goroutine从ch1中接收值,并将该值的平方发送到ch2中
	go f2(ch1, ch2)
	// 在主goroutine中从ch2中接收值打印
	for i := range ch2 { // 通道关闭后会退出for range循环
		fmt.Println(i)
	}
}
2、Goroutine结合Channel管道
  • 需求 1:定义两个方法,一个方法给管道里面写数据,一个给管道里面读取数据,要求同步进行。
  • 1、开启一个 fn1 的的协程给向管道 inChan 中写入 100 条数据
  • 2、开启一个 fn2 的协程读取 inChan 中写入的数据
  • 3、注意:fn1 和 fn2 同时操作一个管道
  • 4、主线程必须等待操作完成后才可以退出
  • 注:for range的方式判断通道关闭,推出程序
Go 复制代码
package main

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

var wg sync.WaitGroup
func main()  {
	intChan := make(chan int,10)
	wg.Add(2)
	go write(intChan)
	go read(intChan)
	wg.Wait()
	fmt.Println("读取完毕...")
}

func write(intChan chan int)  {
	defer wg.Done()
	for i:=0;i<10;i++{
		intChan <- i
	}
	close(intChan)
}

func read(intChan chan int)  {
	defer wg.Done()
	for v := range intChan {
		fmt.Println(v)
		time.Sleep(time.Second)
	}
}

四、单向管道

  • 有的时候我们会将管道作为参数在多个任务函数间传递
  • 很多时候我们在不同的任务函数中使用管道都会对其进行限制
  • 比如限制管道在函数中只能发送或只能接收
Go 复制代码
package main
import (
	"fmt"
)
func main() {
	//1. 在默认情况下下,管道是双向
	//var chan1 chan int //可读可写

	//2 声明为只写
	var chan2 chan<- int
	chan2 = make(chan int, 3)
	chan2<- 20
	//num := <-chan2 //error
	fmt.Println("chan2=", chan2)
	
	//3. 声明为只读
	var chan3 <-chan int
	num2 := <-chan3
	//chan3<- 30 //err
	fmt.Println("num2", num2)
}

五、Goroutine池

  • 本质上是生产者消费者模型
  • 在工作中我们通常会使用可以指定启动的goroutine数量--worker pool模式,控制goroutine的数量,防止goroutine泄漏和暴涨。
  • 一个简易的work pool示例代码如下:
Go 复制代码
package main

import (
	"fmt"
	"time"
)

func worker(id int, jobs <-chan int, results chan<- int) {
	for j := range jobs {
		fmt.Printf("worker:%d start job:%d\n", id, j)
		time.Sleep(time.Second)
		fmt.Printf("worker:%d end job:%d\n", id, j)
		results <- j * 2
	}
}


func main() {
	jobs := make(chan int, 100)
	results := make(chan int, 100)
	// 开启3个goroutine
	for w := 1; w <= 3; w++ {
		go worker(w, jobs, results)
	}
	// 5个任务
	for j := 1; j <= 5; j++ {
		jobs <- j
	}
	close(jobs)
	// 输出结果
	for a := 1; a <= 5; a++ {
		<-results
	}
}
相关推荐
奶香臭豆腐11 分钟前
C++ —— 模板类具体化
开发语言·c++·学习
晚夜微雨问海棠呀19 分钟前
长沙景区数据分析项目实现
开发语言·python·信息可视化
graceyun20 分钟前
C语言初阶习题【9】数9的个数
c语言·开发语言
小蜗牛慢慢爬行43 分钟前
如何在 Spring Boot 微服务中设置和管理多个数据库
java·数据库·spring boot·后端·微服务·架构·hibernate
波音彬要多做1 小时前
41 stack类与queue类
开发语言·数据结构·c++·学习·算法
Swift社区1 小时前
Excel 列名称转换问题 Swift 解答
开发语言·excel·swift
一道微光1 小时前
Mac的M2芯片运行lightgbm报错,其他python包可用,x86_x64架构运行
开发语言·python·macos
矛取矛求1 小时前
QT的前景与互联网岗位发展
开发语言·qt
Leventure_轩先生1 小时前
[WASAPI]从Qt MultipleMedia来看WASAPI
开发语言·qt
向宇it1 小时前
【从零开始入门unity游戏开发之——unity篇01】unity6基础入门开篇——游戏引擎是什么、主流的游戏引擎、为什么选择Unity
开发语言·unity·c#·游戏引擎