Golang 并发简介
本文内容纲要:
-并发概要
-协程介绍
-golang并发
-实现方式
-简单示例
-并发时的缓冲
-并发时的超时
并发概要
随着多核CPU的普及,为了更快的处理任务,出现了各种并发编程的模型,主要有以下几种:
模型名称 | 优点 | 缺点 |
---|---|---|
多进程 | 简单,隔离性好,进程间几乎无影响 | 开销最大 |
多线程 | 目前使用最多的方式,开销比多进程小 | 高并发模式下,效率会有影响 |
异步 | 相比多线程而言,可以减少线程的数量 | 编码要求高,需要对流程分割合理 |
协程 | 用户态线程,不需要操作系统来调度,所以轻量,开销极小 | 需要语言支持 |
协程介绍
协程是个抽象的概念,可以映射到到操作系统层面的进程,线程等概念.
由于协程是用户态的线程,不用操作系统来调度,所以不受操作系统的限制,可以轻松的创建百万个,因此也被称为"轻量级线程".
在golang中,协程不是由库实现的,而是受语言级别支持的,因此,在golang中,使用协程非常方便.
下面通过例子演示在golang中,如何使用协程来完成并发操作.
golang并发
实现方式
golang中,通过go关键字可以非常简单的启动一个协程,几乎没有什么学习成本.
当然并发编程中固有的业务上的困难依然存在(比如并发时的同步,超时等),但是golang在语言级别给我们提供了优雅简洁的解决这些问题的途径.
理解了golang中协程的使用,会给我们写并发程序时带来极大的便利.
首先以一个简单的例子开始golang的并发编程.
packagemain
import(
"fmt"
"time"
)
funcmain(){
fori:=0;i<10;i++{
gosum(i,i+10)
}
time.Sleep(time.Second*5)
}
funcsum(start,endint)int{
varsumint=0
fori:=start;i<end;i++{
sum+=i
}
fmt.Printf("Sumfrom%dto%dis%d\n",start,end,sum)
returnsum
}
执行结果如下:(同时启动10个协程做累加运算,10个协程的执行顺序可能会不一样)
$gorunmain.go
Sumfrom0to10is45
Sumfrom6to16is105
Sumfrom7to17is115
Sumfrom2to12is65
Sumfrom8to18is125
Sumfrom1to11is55
Sumfrom9to19is135
Sumfrom3to13is75
Sumfrom4to14is85
Sumfrom5to15is95
通过go关键字启动协程之后,主进程并不会等待协程的执行,而是继续执行直至结束.
本例中,如果没有time.Sleep(time.Second*5)等待5秒的话,那么主进程不会等待那10个协程的运行结果,直接就结束了.
主进程结束也会导致那10个协程的执行中断,所以,如果去掉time.Sleep这行代码,可能屏幕上什么显示也没有.
简单示例
实际使用协程时,我们一般会等待所有协程执行完成(或者超时)后,才会结束主进程,但是不会用time.Sleep这种方式,
因为主进程并不知道协程什么时候会结束,没法设置等待时间.
这时,就看出golang中的channel机制所带来的好处了.下面用channel来改造上面的time.Sleep
packagemain
import"fmt"
funcmain(){
varch=make(chanstring)
fori:=0;i<10;i++{
gosum(i,i+10,ch)
}
fori:=0;i<10;i++{
fmt.Print(<-ch)
}
}
funcsum(start,endint,chchanstring){
varsumint=0
fori:=start;i<end;i++{
sum+=i
}
ch<-fmt.Sprintf("Sumfrom%dto%dis%d\n",start,end,sum)
}
程序执行结果和上面一样,因为是并发的缘故,可能输出的sum顺序可能会不一样.
$gorunmain.go
Sumfrom9to19is135
Sumfrom0to10is45
Sumfrom5to15is95
Sumfrom6to16is105
Sumfrom7to17is115
Sumfrom2to12is65
Sumfrom8to18is125
Sumfrom3to13is75
Sumfrom1to11is55
Sumfrom4to14is85
golang的chan可以是任意类型的,上面的例子中定义的是string型.
从上面的程序可以看出,往chan中写入数据之后,协程会阻塞在那里,直到在某个地方将chan中的值读取出来,协程才会继续运行下去.
上面的例子中,我们启动了10个协程,每个协程都往chan中写入了一个字符串,然后在main函数中,依次读取chan中的字符串,并在屏幕上打印出来.
通过golang中的chan,不仅实现了主进程和协程之间的通信,而且不用像time.Sleep那样不可控(因为你不知道要Sleep多长时间).
并发时的缓冲
上面的例子中,所有协程使用的是同一个chan,chan的容量默认只有1,当某个协程向chan中写入数据时,其他协程再次向chan中写入数据时,其实是阻塞的.
等到chan中的数据被读出之后,才会再次让某个其他协程写入,因为每个协程都执行的非常快,所以看不出来.
改造下上面的例子,加入些Sleep代码,延长每个协程的执行时间,我们就可以看出问题,代码如下:
packagemain
import(
"fmt"
"time"
)
funcmain(){
varch=make(chanstring)
fori:=0;i<5;i++{
gosum(i,i+10,ch)
}
fori:=0;i<10;i++{
time.Sleep(time.Second*1)
fmt.Print(<-ch)
}
}
funcsum(start,endint,chchanstring)int{
ch<-fmt.Sprintf("Sumfrom%dto%disstartingat%s\n",start,end,time.Now().String())
varsumint=0
fori:=start;i<end;i++{
sum+=i
}
time.Sleep(time.Second*10)
ch<-fmt.Sprintf("Sumfrom%dto%dis%dat%s\n",start,end,sum,time.Now().String())
returnsum
}
执行结果如下:
$gorunmain.go
Sumfrom4to14isstartingat2015-10-1313:59:56.025633342+0800CST
Sumfrom3to13isstartingat2015-10-1313:59:56.025608644+0800CST
Sumfrom0to10isstartingat2015-10-1313:59:56.025508327+0800CST
Sumfrom2to12isstartingat2015-10-1313:59:56.025574486+0800CST
Sumfrom1to11isstartingat2015-10-1313:59:56.025593711+0800CST
Sumfrom4to14is85at2015-10-1314:00:07.030611465+0800CST
Sumfrom3to13is75at2015-10-1314:00:08.031926629+0800CST
Sumfrom0to10is45at2015-10-1314:00:09.036724803+0800CST
Sumfrom2to12is65at2015-10-1314:00:10.038125044+0800CST
Sumfrom1to11is55at2015-10-1314:00:11.040366206+0800CST
为了演示chan的阻塞情况,上面的代码中特意加了一些time.Sleep函数.
- 每个执行Sum函数的协程都会运行10秒
- main函数中每隔1秒读一次chan中的数据
从打印结果我们可以看出,所有协程几乎是同一时间开始的,说明了协程确实是并发的.
其中,最快的协程(Sumfrom4to14…)执行了11秒左右,为什么是11秒左右呢?
说明它阻塞在了Sum函数中的第一行上,等了1秒之后,main函数开始读出chan中数据后才继续运行.
它自身运行需要10秒,加上等待的1秒,正好11秒左右.
最慢的协程执行了15秒左右,这个也很好理解,总共启动了5个协程,main函数每隔1秒读出一次chan,最慢的协程等待了5秒,
再加上自身执行了10秒,所以一共15秒左右.
到这里,我们很自然会想到能否增加chan的容量,从而使得每个协程尽快执行,完成自己的操作,而不用等待,消除由于main函数的处理所带来的瓶颈呢?
答案是当然可以,而且在golang中实现还很简单,只要在创建chan时,指定chan的容量就行.
packagemain
import(
"fmt"
"time"
)
funcmain(){
varch=make(chanstring,10)
fori:=0;i<5;i++{
gosum(i,i+10,ch)
}
fori:=0;i<10;i++{
time.Sleep(time.Second*1)
fmt.Print(<-ch)
}
}
funcsum(start,endint,chchanstring)int{
ch<-fmt.Sprintf("Sumfrom%dto%disstartingat%s\n",start,end,time.Now().String())
varsumint=0
fori:=start;i<end;i++{
sum+=i
}
time.Sleep(time.Second*10)
ch<-fmt.Sprintf("Sumfrom%dto%dis%dat%s\n",start,end,sum,time.Now().String())
returnsum
}
执行结果如下:
$gorunmain.go
Sumfrom0to10isstartingat2015-10-1314:22:14.64534265+0800CST
Sumfrom2to12isstartingat2015-10-1314:22:14.645382961+0800CST
Sumfrom3to13isstartingat2015-10-1314:22:14.645408947+0800CST
Sumfrom4to14isstartingat2015-10-1314:22:14.645417257+0800CST
Sumfrom1to11isstartingat2015-10-1314:22:14.645427028+0800CST
Sumfrom1to11is55at2015-10-1314:22:24.6461138+0800CST
Sumfrom3to13is75at2015-10-1314:22:24.646330223+0800CST
Sumfrom2to12is65at2015-10-1314:22:24.646325521+0800CST
Sumfrom4to14is85at2015-10-1314:22:24.646343061+0800CST
Sumfrom0to10is45at2015-10-1314:22:24.64634674+0800CST
从执行结果可以看出,所有协程几乎都是10秒完成的.所以在使用协程时,记住可以通过使用缓存来进一步提高并发性.
并发时的超时
并发编程,由于不能确保每个协程都能及时响应,有时候协程长时间没有响应,主进程不可能一直等待,这时候就需要超时机制.
在golang中,实现超时机制也很简单.
packagemain
import(
"fmt"
"time"
)
funcmain(){
varch=make(chanstring,1)
vartimeout=make(chanbool,1)
gosum(1,10,ch)
gofunc(){
time.Sleep(time.Second*5)//5秒超时
timeout<-true
}()
select{
casesum:=<-ch:
fmt.Print(sum)
case<-timeout:
fmt.Println("Sorry,TIMEOUT!")
}
}
funcsum(start,endint,chchanstring)int{
varsumint=0
fori:=start;i<end;i++{
sum+=i
}
time.Sleep(time.Second*10)
ch<-fmt.Sprintf("Sumfrom%dto%dis%d\n",start,end,sum)
returnsum
}
通过一个匿名函数来控制超时,然后同时启动计算sum的协程和timeout协程,在select中看谁先结束,
如果timeout结束后,计算sum的协程还没有结束的话,就会进入超时处理.
上例中,timeout只有5秒,sum协程会执行10秒,所以执行结果如下:
$gorunmain.go
Sorry,TIMEOUT!
修改time.Sleep(time.Second*5)为time.Sleep(time.Second*15)的话,就会看到sum协程的执行结果
本文内容总结:并发概要,协程介绍,golang并发,实现方式,简单示例,并发时的缓冲,并发时的超时,
原文链接:https://www.cnblogs.com/wang_yb/p/4874668.html