• 大道如青天,协程来通信,Go lang1.18入门精炼教程,由白丁入鸿儒,Go lang通道channel的使用EP14


    众所周知,Go lang的作用域相对严格,数据之间的通信往往要依靠参数的传递,但如果想在多个协程任务中间做数据通信,就需要通道(channel)的参与,我们可以把数据封装成一个对象,然后把这个对象的指针传入某个通道变量中,另外一个协程从这个通道中读出变量的指针,并处理其指向的内存对象。

    通道的声明与创建

      
    
    package main  
      
    import "fmt"  
      
    func main() {  
    	var a chan int  
    	if a == nil {  
    		fmt.Println("通道是空的, 不能使用,需要先创建通道")  
    		a = make(chan int)  
    		fmt.Printf("数据类型是: %T", a)  
    	}  
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    这里注意,通道声明之后还需要进行创建。

    也可以通过海象操作符声明并创建:

    package main  
      
    import "fmt"  
      
    func main() {  
    	  
    	a := make(chan int)  
      
    	fmt.Printf("数据类型是: %T", a)  
      
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    程序返回:

    数据类型是: chan int%
    
    • 1

    如此,一个类型为整形的通道就创建好了。

    此外,通道是引用数据类型

    package main  
      
    import (  
    	"fmt"  
    )  
      
    func main() {  
    	ch1 := make(chan int)  
    	fmt.Printf("%T,%p\n", ch1, ch1)  
      
    	test1(ch1)  
      
    }  
      
    func test1(ch chan int) {  
    	fmt.Printf("%T,%p\n", ch, ch)  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    程序返回:

    chan int,0x1400010e060  
    chan int,0x1400010e060
    
    • 1
    • 2

    可以看到,在test1函数内和main函数内通道的地址是一样的,所以他们指向的都是同一个通道。

    通道的使用

    通道创建之后,即可以在协程之间充当桥梁:

    package main  
      
    import "fmt"  
      
    func job(ch1 chan int) {  
      
    	ch1 <- 1  
      
    }  
      
    func main() {  
      
    	ch1 := make(chan int)  
      
    	fmt.Println(ch1)  
      
    	go job(ch1)  
      
    	data := <-ch1 // 从ch1通道中读取数据  
    	fmt.Println("data-->", data)  
    	fmt.Println("main。。over。。。。")  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    这里我们声明一个函数job,把通道作为参数传递进去,注意这里参数类型除了声明通道本身以外,还得声明通道具体的数据类型。

    随后在main函数中,可以理解为主协程,创建通道ch1,执行开启协程任务job,在job函数内,往通道内传递数字1

    接着,主协程获取通道内由job协程传递的数据:

    0x1400006a060  
    data--> 1  
    main。。over。。。。
    
    • 1
    • 2
    • 3

    藉此,就完成了数据的传递。

    这里需要注意通道的调用语法:

    data := <- a // 读取通道  
    a <- data // 写入通道
    
    • 1
    • 2

    同步阻塞

    这里需要注意的是,通道无论是写入还是读取,都是同步阻塞机制。即当有协程对通道进行操作的时候,其他协程都处于“等待”状态,说白了,就是在“排队”,在之前的一篇:并发与并行,同步和异步,Go lang1.18入门精炼教程,由白丁入鸿儒,Go lang并发编程之GoroutineEP13,我们要么通过sync.WaitGroup来阻塞主协程,或者通过time.Sleep(time.Second)方法来阻塞,就是怕主协程提前执行完,早成子协程来不及执行。

    而通道的出现,就间接帮我们实现了“阻塞”主协程的目的。

    比如,多个协程任务操作一个变量:

    package main  
      
    import (  
    	"fmt"  
    )  
      
    func job1(number int, squareop chan int) {  
    	sum := 20  
    	sum += number  
    	squareop <- sum  
    }  
      
    func job2(number int, cubeop chan int) {  
    	sum := 10  
    	sum += number  
    	cubeop <- sum  
    }  
    func main() {  
    	number := 0  
    	ch1 := make(chan int)  
    	ch2 := make(chan int)  
    	go job1(number, ch1)  
    	go job2(number, ch2)  
    	num1, num2 := <-ch1, <-ch2  
    	fmt.Println("Final output", num1+num2)  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    这里job1和job2两个协程任务同时异步执行,操作number变量,累加后往通道中写入,程序返回:

    Final output 30
    
    • 1

    理论上,如果是并发执行,返回值应该是20或者10,但由于通道的存在,造成协程任务阻塞,变回了同步执行,所以返回了30。

    同时,我们需要注意死锁问题,如果一个协程任务在一个通道上发送数据,那么其他的协程任务应该接收数据,如果这种情况不发生,那么程序将在运行时出现死锁。

    换句话说,你发送了,就得有人接收,只发不接,或者只收不发,都会变成死锁。

    此外,协程任务可以通过close(ch)方法来关闭通道:

    package main  
      
    import (  
    	"fmt"  
    )  
      
    func job(ch1 chan int) {  
    	// 发送方:3条数据  
    	for i := 0; i < 3; i++ {  
    		ch1 <- i //将i写入通道中  
    	}  
    	close(ch1) //将ch1通道关闭了。  
    }  
      
    func main() {  
    	ch1 := make(chan int)  
    	go job(ch1)  
    	/*  
    		子goroutine,写出数据3个  
    				每写一个,阻塞一次,主程序读取一次,解除阻塞  
      
    		主goroutine:循环读  
    				每次读取一个,堵塞一次,子程序,写出一个,解除阻塞  
      
    		发送发,关闭通道的--->接收方,接收到的数据是该类型的零值,以及false  
    	*/  
    	//主程序中获取通道的数据  
    	for {  
      
    		v, ok := <-ch1 //其他goroutine,显示的调用close方法关闭通道。  
    		if !ok {  
    			fmt.Println("已经读取了所有的数据,", ok)  
    			break  
    		}  
    		fmt.Println("取出数据:", v, ok)  
    	}  
      
    	fmt.Println("main...over....")  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    这里将0到2写入chl通道,然后关闭通道。主函数里有一个死循环。类似while,它轮询通道是否在发送数据后,使用变量ok进行判断。如果ok是假的,则意味着通道关闭,因此循环结束,否则将会继续进行无限轮询。

    select关键字

    select 是 Go lang里面的一个流程控制结构,和switch关键字差不多,但是select会随机执行一个可运行的通道通信,如果没有通道通信可运行,它将阻塞,直到有通道通信可运行:

    package main  
      
    import (  
    	"fmt"  
    	"time"  
    )  
      
    func job(ch1 chan int) {  
      
    	time.Sleep(2 * time.Second)  
    	ch1 <- 200  
      
    }  
      
    func main() {  
      
    	ch1 := make(chan int)  
    	ch2 := make(chan int)  
      
    	go job(ch1)  
    	go job(ch2)  
      
    	select {  
    	case num1 := <-ch1:  
    		fmt.Println("ch1中取数据。。", num1)  
    	case num2, ok := <-ch2:  
    		if ok {  
    			fmt.Println("ch2中取数据。。", num2)  
    		} else {  
    			fmt.Println("ch2通道已经关闭。。")  
    		}  
      
    	}  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    这里select会随机选择一个可运行的通道通信逻辑,可能是ch1通道,也有可能是ch2通道:

    ➜  mydemo git:(master) ✗ go run "/Users/liuyue/wodfan/work/mydemo/hello.go"  
    ch1中取数据。。 200  
    ➜  mydemo git:(master) ✗ go run "/Users/liuyue/wodfan/work/mydemo/hello.go"  
    ch1中取数据。。 200  
    ➜  mydemo git:(master) ✗ go run "/Users/liuyue/wodfan/work/mydemo/hello.go"  
    ch2中取数据。。 200  
    ➜  mydemo git:(master) ✗
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    结语

    综上,Golang的通道其实就是将协程任务进行隔离,编写并发逻辑时,关注通道即可,说白了,Golang的通道就是Python多进程通信中的管道,Golang虽然没有显性的多进程调用,但其协程调度底层就是多进程之间的通信,因为只有多进程才可能利用CPU的多核资源。

  • 相关阅读:
    C# Solidworks二次开发:枚举应用实战(第三讲)
    Py之interpret:interpret的简介、安装、案例应用之详细攻略
    C++:无法查找或打开 PDB 文件?? 如何解决呢?以及产生原因是什么?
    汽车研发项目进度管理的挑战与优化策略
    使用证书的方式登录linux 系统或者windows系统
    数据分析-numpy2
    【学习笔记01】node的认识和安装
    【ROS入门】使用 ROS 话题(Topic)机制实现消息发布与订阅及launch文件的封装
    使用Android辅助功能AccessibilityService实现微信自动聊天【外挂插件】
    U3D Addressables异步加载资源,创建大物体卡顿解决方案
  • 原文地址:https://blog.csdn.net/zcxey2911/article/details/126608246