深入理解 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
	}
}
相关推荐
尚学教辅学习资料6 分钟前
基于SpringBoot的医药管理系统+LW示例参考
java·spring boot·后端·java毕业设计·医药管理
明月看潮生18 分钟前
青少年编程与数学 02-003 Go语言网络编程 15课题、Go语言URL编程
开发语言·网络·青少年编程·golang·编程与数学
明月看潮生23 分钟前
青少年编程与数学 02-003 Go语言网络编程 14课题、Go语言Udp编程
青少年编程·golang·网络编程·编程与数学
南宫理的日知录29 分钟前
99、Python并发编程:多线程的问题、临界资源以及同步机制
开发语言·python·学习·编程学习
逊嘘1 小时前
【Java语言】抽象类与接口
java·开发语言·jvm
Half-up1 小时前
C语言心型代码解析
c语言·开发语言
Source.Liu1 小时前
【用Rust写CAD】第二章 第四节 函数
开发语言·rust
monkey_meng1 小时前
【Rust中的迭代器】
开发语言·后端·rust
余衫马1 小时前
Rust-Trait 特征编程
开发语言·后端·rust
monkey_meng1 小时前
【Rust中多线程同步机制】
开发语言·redis·后端·rust