第五章 Go语言并发开发
一:并发开发的四种模型
1:) 多进程,这个是系统层面进行的并发模型,是开销最大的
2:)多线程,这个也是系统层面的,是我们常用的,比第一种开销小很多,但是开销依旧比较的大,并且在高并发模式下,效率有影响
3:)基于回调的非堵塞/异步IO,这个node.js实现的很好,就是调试和流程比较的复杂
4:)协程这个本质上是一种用户态线程,不需要系统来进行枪战调度,但是在真正的实现中寄存在线程中,因此系统开销极小,可以有效的提供线程的任务并发,避免多线程的缺点,使用协程的优点是编程简单,确定是需要语言支持(也叫轻量级线程) Go语言在语言层面支持协程 叫goroutine
Go在语言中支持协程 ,叫做goroutine,有GO运行时(runtime)管理
在Go中要让对象在协程中执行,只需要在对象前面添加关键字 go
package main
import "fmt"
func main() {
go Add(3,4)
}
func Add(x,y int) {
z :=x+y
fmt.Println("相加的结果是",z)
}
但是这样调用之后会发现 没有打印出信息 why,难道没有执行, 这里就要说到go的执行机制了, 在go中,程序从初始化main.package并执行main()函数开始,当main函数返回时程序就退出,并且程序并不等待其他goroutine(非主goroutine)结束,所以当main执行结束的时候,协程go 的对象还没有执行,但 go协程执行完了 main早已经执行结束了,所以屏幕不会打印信息 go有让主函数main等待所以goroutine退出后在返回的方法我们下面再说
下面这个是内存共享实现的 并发打印数据,下面的代码 共享了内存 counter因为10个goroutine是并发的所以每次修改counter必须添加锁吗,然后在主函数中使用for循环来不断检测counter的值(同样要添加锁)当达到10的时候退出, 但是这样的内存共享实现时很糟糕的,
package main
import (
"fmt"
"sync"
"runtime"
)
var counter int =0
func main() {
lock :=&sync.Mutex{}
for i :=0;i<10 ;i++ {
go Count(lock)
}
for{
lock.Lock()
c :=counter
lock.Unlock()
runtime.Gosched()
if c>=10{
break
}
}
}
func Count(lock *sync.Mutex) {
lock.Lock()
counter++
fmt.Println(counter)
lock.Unlock()
}
上面我们说的是内存共享实现并发信息共享,下面我们来看一下go实现的(通信模型:消息机制) go中的消息通信机制被称作 channel,消息机制认为,每个并发单元是自包含的,独立的,并且都要自己的变量,但是不同并发单元间的这些变量是不共享的,每个并发单元的输入和输出只有一种,那就是消息
如:
package main
import (
"fmt"
)
func Count(ch chan int) {
ch<-1
fmt.Println("Counting")
}
func main() {
chs :=make([]chan int,10)
for i:=0;i<10;i++{
chs[i]=make(chan int)
go Count(chs[i])
}
for _,ch :=range(chs){
valuw :=<-ch
fmt.Println(valuw)
}
}
输出:
Counting
1
1
1
1
Counting
1
1
1
Counting
Counting
Counting
1
1
1
一般channel的声明形式是
var channame chan ElemenType
和一般声明不同的地方就是 在类型前面添加了 chan关键字 ElemenType 是值这个channel所传递的元素类型
定义 一个 channel也很简单,直接使用内置函数make就可以啦
ch :=make(chan int)
这就声明并初始化一个int类型的名称为ch的channel
在channel的用法中,最常见的包括写入和读出,将一个数据写入(发送)到channel的语法很直观
ch <- values 如上面的ch<-1 吧1写入ch
香channel谢日数据会导致程序堵塞,直到其他goroutine从这个channel中读取数据,从channel读取数据的语法是 values := <- ch 如上面的:valuw :=<-ch
如果channel之前没有数据,那么从channel中读取数据会导致程序堵塞,直到channel中被输入数据为止,后面我们会提到如何控制channel只接受读或者写,单向的channel
二:Select 早在unix时代select机制已经被引入啦,通过调用select()函数来监控一系列的文件句柄,一旦其中一个文件句柄发生了io动作,该select()调用就会被返回,用于处理异步io问题,select的用法和switch类似,但是select中每个case语句里面必须是io操作
select {
case <- chan1:
//如果chan1成功读取到数据,就就想case处理语句
case chan2<-1:
//如果chan2成功写入数据,就进行case处理语句
default:
//如果上面都没有成功,就进入default处理
}
如:随机向ch写入0 或者1 因为是chan 所以下面拉个case不一定那个先执行完
ch := make(chan int ,1)
for i :=0;i<10 ;i++ {
select {
case ch <-0:
case ch <-1:
}
j := <-ch
fmt.Println("输出",j)
}
输出数据:
输出 0
输出 0
输出 1
输出 1
输出 0
输出 0
输出 0
输出 1
输出 0
输出 0
三:缓存机制
上面我们讲到的都是不带缓冲的channel,这种做法对于传递单个数据场景还可以接受,但是对于传递大数据的场景就有些不合适啦,下面我们介绍如果给channel带上缓冲,从而达到消息队列的效果
要重建一个带缓冲的channel其实非常的简单 如下:
c := make(chan int ,1024)
上面这个 语句调用make时将缓冲区大小作为第二个参数传入即可,比如上面创建一个大小为1023的int类型channel,即使没有读取方,写入方也可以一直往channel里写入,在缓冲区被填满之前都不会堵塞
从缓冲的channel读取数据可以使用与常规非缓冲channel完全一直的方法,但是我们可以使用range关键字实现为简便的循环读取
c :=make(chan int ,1024)
for i:=0;i<10 ;i++ {
c <- i
}
for i :=range c{
fmt.Println("缓冲读取",i)
}
缓冲读取 0
缓冲读取 1
缓冲读取 2
缓冲读取 3
缓冲读取 4
缓冲读取 5
缓冲读取 6
缓冲读取 7
缓冲读取 8
缓冲读取 9
四:超时机制
这个在并发编程中很重要,比如向channel写入数据发现channel已满,或者读取channel发现其为空,这些都会导致goroutine锁死,
但是go没有提供专门的超时机制,不过我们可以使用select机制来实现,虽然select不是专门为超时设计的,但是却能很方便的解决超时问题,因为select的特点是只要其中一个case已经完成程序就会继续往下执行,而不考虑其他的case情况
ch :=make(chan int ,1024)
timeout :=make(chan bool,1)
go func() {
time.Sleep(1000)
timeout <- true
}()
varch := <- ch
select {
case <- ch:
case <- timeout:
}
如上面代码
ch一直没有没有写入值,这样要是有读取ch的就会堵塞(varch := <- ch),但是在一秒过后timeout有写入值,这样就会执行select中的case <- timeout:,就相当于跳过了上面的堵塞
五:channel的传递
需要注意的是在go中channel也是一个原始类型,于map的类型地位一样,因此channel本身在定以后也可以通过channel传递,利用这个特性我们可以实现非常常见的管道(pipe)特性
下面就是我们实现的管道,在管道中我们传递的是一个整数
type PipData struct {
value int
handler func(int) int
next chan int
}
func handler(queue chan *PipData) {
for data :=range queue{
data.next<-data.handler(data.value)
fmt.Println("输出pipe",data.value)
}
}
六:channel的单向 顾名思义单向就是只能接收或者只能发送
单向channel定义非常的简单 如下:
var ch1 chan int //这个是普通的channel,可读取数据,可写入数据
var ch2 chan <- float64 //这个是一个可写入的单向channel,只能写入float64的数据
var ch3 <- chan int //这个是可读取单向channel,只能读取int类型数据
那么单向的channel怎么初始化那,之前我们提到过channel也是一个原始类型,因此channel不仅可以传递,还能类型转换(只有介绍了单向channel概念后,才能明白类型转化对channel的意识:就是单向channel和双向channel自己进行切换) 例如:
ch4 :=make(chan int) //普通的chan
ch5 :=<-chan int(ch4) //转化成只能读取的channel
ch6 :=chan <- int(ch4) //转化成只能写入的channel
如下面这个方法:只能读取
func wriechan(ch <- chan int) {
for varlu:=range ch {
fmt.Println("只能读取",varlu)
}
}
七:关闭channel 关闭channel很简单,直接用Go的内中函数close()就可以 如:
var ch chan int
close(ch)
那么如何看一个channel是否关闭那,这个可以使用判断map值存在不存在一样的方法
x,ok :=<-ch ,如果ok返回的是false,证明ch已经关闭
type Vector []float64
func (v Vector) DoSome(i,n int,u Vector,c chan int) {
for ;i<n ;i++ {
v[i]+=u.op(v[i])
}
c <- 1
}
const NCPU =16
func (v Vector) DoAll(u Vector) {
c :=make(chan int ,NCPU)
for i:=0;i<NCPU;i++{
go v.DoSome(i*len(v)/NCPU,(i+1)*len(v)/NCPU,u,c)
}
for i:=0;i<NCPU ;i++ {
<-c
}
}
runtime.GOMAXPROCS(16) //这一句很重要是设置Go按多少个cpu核执行,因为现在的Go还不能自能的检查cpu核心
runtime.Gosched() 主动让出时间片
七:同步锁
在Go中提供了两种锁的类型 ,
sync.Mutex{} 和
sync.RWMutex{}
其中sync.Mutex{}是最简单的一种锁,同时也是最暴力的,当使用了sync.Mutex{}后,其他的goroutine就只能乖乖的等待这个goroutine释放sync.Mutex但是sync.RWMutex就相对友好,是经典的单写多读模型,在读锁占用的情况下,会阻止写,但是不阻止读,也就是多个goroutine可以同时获取读锁(调用Rlock方法),而写锁(调用Lock)会阻止其他任何goroutine无论读写,整个锁相当于用goroutine独占,从RWMutex的实现来看,RWMutex类型其实组合了Mutex
如:
type RWMutex struct {
w sync.Mutex
writersem uint32
}
下面是使用Mutex的经典模型(注意我们有用到了defer)
var l sync.Mutex
func foo() {
l.Lock()
defer l.Unlock()
}
八:全局唯一性 对于从全局角度,只需要运行一次的代码,比如全局初始化,Go语言提供了一个once类型来保证全局唯一性操作
package main
import (
"sync"
"fmt"
)
var a string
var once sync.Once
var ch chan int
func main() {
ch=make(chan int,2)
go doprin(ch)
go doprin(ch)
for c :=range ch{
fmt.Println(c)
}
}
func setup() {
a="汽车之家——赵占龙"
fmt.Println("开始执行")
}
func doprin(ch chan int) {
once.Do(setup)
fmt.Println(a)
ch <- 1
}
输出:
开始执行
fatal error: all goroutines are asleep - deadlock!
汽车之家——赵占龙
1
汽车之家——赵占龙
1
通过输出我们可以看出:setup这个方法虽然被两个协程调用(调用了两次),但是其实,这个方法☞执行了一次,这样就保证了setup的原子性操作(once.Do)