一、协程goroutine
①goroutine的基本模型
单进程时代的两个问题?
1、单一执行流程、计算机只能一个任务一个任务处理
2、进程阻塞所带来的的CPU浪费时间
多进程/多线程解决了阻塞问题,那么它是如何工作的,如下图:
但是多进程/多线程同时也面临着新的问题:
需要大量的切换成本:进程/线程的数量越多,切换成本也越大,也就越浪费。CPU可能60%在执行程序,40%在切换进程中,所以提高CPU的利用率是一个问题。
那我们如何去处理这个问题,首先看下在操作系统中一个线程的结构是怎么样的,如下图:
那么我们可以通过这两个空间把线程一分为二,切换成用户线程和内核线程,并且对两者之间进行一个绑定。
然后分别对他们取个别名,内核线程就叫线程(thread),用户线程就叫协程(co-routine)。
于是我们可以对这个模型进行一个优化,如下图,协程和线程是一个M:N的关系,这样可以避免进程频繁的切换,也可以避免某一个协程阻塞导致进程阻塞。
②goroutine的使用
//主goroutine
func main(){
//创建一个协程去执行newTask()任务
go newTask()
//睡眠五秒后主goroutine退出
i:=0
for{
i++
if i>5{
break
}
fmt.Printf("main goroutine i=%d\n",i)
time.Sleep(time.Second)
}
}
//子goroutine
func newTask(){
i:=0
for {//死循环,每隔一秒进行打印
i++
fmt.Printf("new goroutine i=%d\n",i)
time.Sleep(1*time.Second)
}
}
运行结果:
通过以上代码我们可以发现:
1.使用关键字go去创建一个子协程
2.main函数是一个主协程,主协程和子协程并发运行
3.一旦主协程退出,子协程立刻退出
协程的退出
//主goroutine
func main(){
go func(){
defer fmt.Println("A.defer")
func(){
defer fmt.Println("B.defer")
runtime.Goexit()//退出当前的协程
fmt.Println("B")
}()
fmt.Println("A")
}()
for {
time.Sleep(time.Second)
}
}
二、管道channel
①goroutine的通信机制
②基本使用
无缓存的channel
//主goroutine
func main(){
//定义一个channel管道
c:=make(chan int)
go func() {
defer fmt.Println("子goroutine结束")
fmt.Println("子goroutine正在运行")
c<-666//将666发送给c
}()
num:=<-c//从c中接收数据,并赋值给管道
fmt.Println("num=",num)
fmt.Println("main goroutine结束...")
}
带有缓存的channel
c:=make(chan int,3)//带有缓存的channel
fmt.Println("len(c)=",len(c),"cap(c)=",cap(c))
go func() {
defer fmt.Println("子协程结束")
for i:=0;i<4;i++{
c<-i
fmt.Println("子协程正在运行,发送的元素=",i,"len(c)=",len(c),"cap(c)=",cap(c))
}
}()
time.Sleep(2*time.Second)
for i:=0;i<4;i++{
num:=<-c
fmt.Println("主协程正在读取num=",num)
}
fmt.Println("mian 结束")
运行结果:
解读:
channel最多可以存储三个数据,如果通道已满,在向里面写数据,就会阻塞。 当channel为空,从里面取数据也会阻塞
channel的关闭
//主goroutine
func main(){
c:=make(chan int)
go func(){
for i:=0;i<5;i++{
c<-i
}
//关闭通道
close(c)
}()
for {
//ok为true的时候表示channel没有关闭,如果为false表示channel已经关闭
if data,ok:=<-c;ok{
fmt.Println(data)
}else{
break
}
}
fmt.Println("main finished...")
}
注意:
channel不像文件一样需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束range循环之类的,才去关闭channel;
关闭channel/后,无法向channel再发送数据(引发panic错误后导致接收立即返回零值)
关闭channel/后,可以继续从channel接收数据; 对于nil channel,无论收发都会被阻塞。
③channel与range
func main(){
c:=make(chan int)
go func(){
for i:=0;i<5;i++{
c<-i
}
close(c)
}()
for data:=range c{
fmt.Println(data)
}
fmt.Println("main finished...")
}
④channel与select
单流程下一个go只能监控一个channel的状态,selecti可以完成监控多个channel的状态
//主goroutine
func main(){
c:=make(chan int)
quit:=make(chan int)
go func(){
for i:=0;i<10;i++{
fmt.Println(<-c)//从管道c中读取数据
}
//如果操作完毕,给quit管道赋值,表示可以退出
quit<-0
}()
fab(c,quit)
}
func fab(c,quit chan int){
x,y:=1,1
for {
select {
case c<-x://如果管道c可以写入的话,进行赋值
x=y
y=x+y
case <-quit://如果结束管道可读的话,就退出
fmt.Println("quit")
return
}
}
}
参考资料:
https://www.yuque.com/aceld/mo95lb
https://www.bilibili.com/video/BV1gf4y1r79E?p=27