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()
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
本书作者带你一步一步深入这些方法。你将理解 Go语言为何选定这些并发模型,这些模型又会带来什么问题,以及你如何组合利用这些模型中的原语去解决问题。学习那些让你在独立且自信的编写与实现任何规模并发系统时所需要用到的技巧和工具。 理解Go语言如何解决并发难以编写正确这一根本问题。 学习并发与并行的关键性区别。 深入到Go语言的内存同步原语。 利用这些模式中的原语编写可维护的并发代码。 将模式组合成为一系列的实践,使你能够编写大规模的分布式系统。 学习 goroutine 背后的复杂性,以及Go语言的运行时如何将所有东西连接在一起。 作者简介 · · · · · · Katherine Cox-Buday是一名计算机科学家,目前工作于 Simple online banking。她的业余爱好包括软件工程、创作、Go 语言(igo、baduk、weiquei) 以及音乐,这些都是她长期的追求,并且有着不同层面的贡献。 目录 · · · · · · 前言 1 第1章 并发概述 9 摩尔定律,Web Scale和我们所陷入的混乱 10 为什么并发很难? 12 竞争条件 13 原子性 15 内存访问同步 17 死锁、活锁和饥饿 20 确定并发安全 28 面对复杂性的简单性 31 第2章 对你的代码建模:通信顺序进程 33 并发与并行的区别 33 什么是CSP 37 如何帮助你 40 Go语言的并发哲学 43 第3章 Go语言并发组件 47 goroutine 47 sync包 58 WaitGroup 58 互斥锁和读写锁 60 cond 64 once 69 池 71 channel 76 select 语句 92 GOMAXPROCS控制 97 小结 98 第4章 Go语言的并发模式 99 约束 99 for-select循环103 防止goroutine泄漏 104 or-channel 109 错误处理112 pipeline 116 构建pipeline的最佳实践 120 一些便利的生成器 126 扇入,扇出 132 or-done-channel 137 tee-channel 139 桥接channel模式 140 队列排队143 context包 151 小结 168 第5章 大规模并发 169 异常传递169 超时和取消 178 心跳 184 复制请求197 速率限制199 治愈异常的goroutine 215 小结 222 第6章 goroutine和Go语言运行时 223 工作窃取223 窃取任务还是续体 231 向开发人员展示所有这些信息 240 尾声 240 附录A 241

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

季布,

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值