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)

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

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

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

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为了看执⾏效果
}

begin sleep
I am reader, hello

我们来是实现⼀个通过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)
	}
}

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

这样执⾏完效果不太好,因为当第⼀个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)
	}
}

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

chan_name chan<- chan_type //只写通道
chan_name <-chan chan_type //只读通道

我们将上述的例⼦改造,因为三个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)

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() //发射⽕箭
}
5
4
3
2
1
发射!!

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

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

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

我们可以实现这样⼀个函数,读取标准输⼊,但是这样退出整个进程也不太优雅,我们还是想⽐较稳妥
的退出。于是很多⼈想到,我们可以在建⽴⼀个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()
}

你可能感兴趣的:(Go,golang,开发语言,后端)