• Go channel同步


    channel同步

    2.7.1 channel通信与CSP并发模型
    linux系统编程中,有⼀种进程间通信的⽅式叫管道,两个进程可以借助内核开辟的缓冲区进⾏数据交
    换,形象上就像是⼀个⽔管(内核的缓冲区)把数据从⼀个进程流向另外⼀个进程。在Go语⾔当中,也
    设计了⼀款类似的通信⽅式 – channel,利⽤channel读写的特性,不光可以实现Goroutine之间精准通
    信,也可以控制Goroutine之间的同步协调。
    这个并发模型就是著名的CSP(Communicating Sequential Process),这个模型最早是上世纪70年代
    提出的。
    在Go语⾔之中,我们借助内置make函数创建channel,channel的创建可以有缓冲区,也可以⽆缓冲
    区。

    make(chan chantype)
    make(chan chantype, 5)
    
    • 1
    • 2

    对于通道,我们关键是掌握他们的读写⾏为。

    • 写⾏为
      通道缓冲区已满(⽆缓冲区),写阻塞直到缓冲区有空间(或读端有读⾏为) 通道缓冲区未满,顺利写
      ⼊,结束
    • 读⾏为

    缓冲区⽆数据(⽆缓冲区时写端未写数据),读阻塞直到写端有数据写⼊ 缓冲区有数据,顺利读数据,
    结束

    package main
    
    import (
    	"fmt"
    	"time"
    )
    
    var c chan string
    
    func reader() {
    	msg := <-c //读通道
    	fmt.Println("I am reader,", msg)
    }
    func main() {
    	c = make(chan string)
    	go reader()
    	fmt.Println("begin sleep")
    	time.Sleep(time.Second * 3) //睡眠3s为了看执⾏效果
    	c <- "hello"                //写通道
    	time.Sleep(time.Second * 1) //睡眠1s为了看执⾏效果
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    begin sleep
    I am reader, hello
    
    • 1
    • 2

    我们来是实现⼀个通过goroutine实现数字传递的例⼦,goroutine1循
    环将1,2,3,4,5传递给goroutine2,goroutine2负责将数字平⽅后传递给goroutine3,goroutine3
    负责打印接收到的数字。
    分析该应⽤,我们需要⾄少2个channel,3个goroutine,其中main函数可以直接是第三个goroutine,
    所以再创建2个就够了。

    package main
    
    import (
    	"fmt"
    	"time"
    )
    
    var c1 chan int
    var c2 chan int
    
    func main() {
    	c1 = make(chan int)
    	c2 = make(chan int)
    	//counter
    	go func() {
    		for i := 0; i < 10; i++ {
    			c1 <- i //向通道c1写⼊数据
    			time.Sleep(time.Second * 1)
    		}
    	}()
    	//squarer
    	go func() {
    		for {
    			num := <-c1     //读c1数据
    			c2 <- num * num //将平⽅写⼊c2
    		}
    	}()
    	//printer
    	for {
    		num := <-c2
    		fmt.Println(num)
    	}
    }
    
    
    • 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
    0
    1
    4
    9
    16
    25
    36
    49
    64
    81
    fatal error: all goroutines are asleep - deadlock!
    
    goroutine 1 [chan receive]:
    main.main()
            C:/Users/nlp_1/goWorkspace/src/main.go:30 +0xac
    
    goroutine 7 [chan receive]:
    main.main.func2()
            C:/Users/nlp_1/goWorkspace/src/main.go:24 +0x28
    created by main.main in goroutine 1
            C:/Users/nlp_1/goWorkspace/src/main.go:22 +0x92
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    这样执⾏完效果不太好,因为当第⼀个goroutine执⾏输出10个后,后⾯没有goroutine向通道写数据,
    这样就会出现Go语⾔不允许的情况,这种错误当然也是可预⻅的,就是代表进程被锁死了,所以Go语
    ⾔给定义的错误是Deadlock(死锁)。

    这是由于channel的知识点我们还需要知道,通道可以创建,也可以关闭,在读取的时候,也可以使⽤
    指示器变量来判断有没有问题,顺便提⼀下range在这⾥仍然可以读取channel,此时不需要“<-”。我们
    来尝试结束后关闭channel,然后优雅的结束整个进程。

    package main
    
    import (
    	"fmt"
    	"time"
    )
    
    var c1 chan int
    var c2 chan int
    
    func main() {
    	c1 = make(chan int)
    	c2 = make(chan int)
    	//counter
    	go func() {
    		for i := 0; i < 10; i++ {
    			c1 <- i //向通道c1写⼊数据
    			time.Sleep(time.Second * 1)
    		}
    		close(c1) //关闭c1
    	}()
    	//squarer
    	go func() {
    		for {
    			num, ok := <-c1 //读c1数据
    			if !ok {
    				break
    			}
    			c2 <- num * num //将平⽅写⼊c2
    		}
    		close(c2) //关闭c2
    	}()
    
    	//printer
    	for {
    		num, ok := <-c2
    		if !ok {
    			break
    		}
    		fmt.Println(num)
    	}
    }
    
    
    • 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
    • 40
    • 41
    • 42
    • 43

    特别注意,对通道的读写操作都会使goroutine阻塞,通道的关闭应该由写端来操作。此外,channel也
    可以作为函数参数,默认情况下⼀个channel是读写都可以的,为了防⽌不该写的goroutine发⽣写⾏
    为,Go语⾔设计了channel传递给函数的时候可以指定为单⽅向,读或者写!⽽这个单⽅向表述⾮常明
    确:

    chan_name chan<- chan_type //只写通道
    chan_name <-chan chan_type //只读通道
    
    • 1
    • 2

    我们将上述的例⼦改造,因为三个goroutine对channel的操作就是读或者写。

    //counter,对c1只写
     go func(out chan<- int) {
     for i := 0; i < 10; i++ {
     out <- i //向通道c1写⼊数据
     time.Sleep(time.Second * 1)
     }
     close(out)
     }(c1)
     //squarer,对c1只读,对c2只写
     go func(in <-chan int, out chan<- int) {
     for {
     num, ok := <-in //读c1数据
     if !ok {
     break
     }
     out <- num * num //将平⽅写⼊c2
     }
     close(out)
     }(c1, c2)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    2.7.2 定时器
    接下来我们来实现⼀个⽕箭发射的例⼦,准备⼀个倒数计时5秒,然后打印⼀个发射。当然这个例⼦可
    以⽤Sleep来控制每隔1s计数⼀次,不过在这⾥我们使⽤Go语⾔为我们提供的定时器来做这件事,定时
    器的关键也是channel。

    在time包中存在⼀个NewTimer,传⼊⼀个时间间隔n,获得⼀个Timer,Timer结构体中包含了⼀个
    C,这是⼀个通道类型,于是在时间n之后,C中会被写⼊时间戳。

    package main
    import (
     "fmt"
      "time"
    )
    func launch() {
     fmt.Println("发射!")
    }
    func main() {
     ticker := time.NewTicker(time.Second)
     num := 5
     for {
     <-ticker.C //读取⽆⼈接收
     fmt.Println(num)
     num--
     if num == 0 {
     break
     }
     }
     ticker.Stop()
     launch() //发射⽕箭
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    5
    4
    3
    2
    1
    发射!!
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这样可以实现⽕箭发射的功能,不过如果临时想取消发射,该如何做呢?按ctrl+c的⽅式太简单粗暴了
    ⼀下,⽐如想要按下任意键取消发射呢?

    2.7.3 多路channel监控
    我们很⾃然想到读标准输⼊就可以了,甚⾄也会⽴刻想到启动⼀个goroutine去监听标准输⼊,如果有
    输⼊,⽴即退出进程。

    func cancel() {
     data := make([]byte, 10)
     os.Stdin.Read(data) //读标准输⼊
     os.Exit(1) //退出进程
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    我们可以实现这样⼀个函数,读取标准输⼊,但是这样退出整个进程也不太优雅,我们还是想⽐较稳妥
    的退出。于是很多⼈想到,我们可以在建⽴⼀个channel,当标准输⼊有数据的时候,将数据写⼊该
    channel,在main函数中监控该channel,如果读到数据,则不执⾏后⾯的发射,直接return。但是问
    题来了,通道读都是阻塞的,我们的⽕箭发射还怎么做呢?在linux下我们知道多路IO监控可以使⽤
    select、poll、epoll等,在Go语⾔⾥,同样提供了⼀个机制对多路channel监控,这个机制的关键就是
    select-case语句。

    select 可以这也监控多个通道,当任⼀通道有数据写⼊时,select都会⽴即返回解除阻塞。完整代码如
    下:

    package main
    
    import (
    	"fmt"
    	"os"
    	"time"
    )
    
    // 监控标准输入
    func cancel(out chan<- string) {
    	buf := make([]byte, 10)
    	os.Stdin.Read(buf) //阻塞读
    	// 通道通知主控goroutine
    	out <- "stop"
    }
    
    func main() {
    	stdin_chan := make(chan string)
    	go cancel(stdin_chan)
    	ticker := time.NewTicker(time.Second)
    	num := 5
    	for num > 0 {
    		// select 可以监控多路channel 任意channel有数据写入 立即返回
    		select {
    		case <-ticker.C:
    			fmt.Println(num)
    			num--
    		case <-stdin_chan:
    			ticker.Stop()
    			return
    		}
    
    	}
    	fmt.Println("发射!!")
    	ticker.Stop()
    }
    
    
    • 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
  • 相关阅读:
    Istio 入门(六):版本控制
    Netty核心组件源码说明
    【Qt】QMainWindow |QDialog对话框
    基于Syntiant TinyML Board与Edge Impulse的LED语音控制(Arduino/C++)
    谁家的加密密钥,写死在代码里?(说的就是你)
    学生python编辑1--慢慢变大的小球
    项目开发中Maven的单向依赖-2022新项目
    神经网络在通信中的应用,神经网络及其应用
    (C语言)成绩统计
    Unity优化之Drawcall
  • 原文地址:https://blog.csdn.net/weixin_47906106/article/details/133711528