goroutine来历
Go语言快一方面就在于它处理多任务的时候做了些优化,比如处理协程的时候
正常的内核与用户空间的关系是这样的

内核中的线程通过协程调度器对协程进行处理,哪个语言的协程调度器比较厉害,那么这个语言就比较快。至于协程是什么?为什么要用协程?多任务是什么?还不知道的可以参考这篇
go对协程的处理
go把co-routine改了个名字改为了goroutine,并且把内存改为了仅占几kb,可以灵活调度大量的存在,要想让goroutine灵活调动,就体现在了调度器上了
早期调度器处理
早期是把每个G加入全局队列中并且加锁,M执行时先获取锁取出其中一个G,执行G执行完再还锁,并且把G放回队列中

缺点
1 .创建,执行,销毁G都需要每个M获取锁,造成了激烈的锁竞争
2. M转移G会造成额外的系统负载和延迟(试想一下,如果M在执行G中,这个G自己又创建了一个G,那么为了保证同时执行,系统就会把新被创建的G丢给另一个M执行,这样就损失了局部性)
3.cpu在 M之间频繁切,线程频繁的阻塞与非阻塞,换导致了系统开销增加
GO对调度器的优化
GMP

这个G和M在之前就介绍过了,这个P时用来处理G的,它包含了可运行的G队列。

M想要执行某个P中的G,那么就得先获取P,然后P把它内部的G队列拿出来交给M执行。可以通过设置GOMAXPROCS的个数来设定有几个P。
每个P同时只能执行一个G所以最高同时并行的G的个数取决于有几个P。
调度器的设计策略
1.复用线程
work stealing机制
偷取机制,就是自己没有,去别的地方偷,别的P中也偷不到G了,那就去全局队列偷。

这个M2中的P里时啥也没有的所以它会去M1里的P把G3偷过来执行
handing off机制
分离机制,就是说如果M1的一个协程阻塞了,就会新开一个M3或者唤醒一个M3 ,并且把之前M1和阻塞的协程留在原地,P和队列中的G全部移给M3,如果一段时间后G1还是没反应,那么这个线程M1就会被销毁或者睡眠

2.利用并行
就是可以通过设置GOMAXPROCS来告诉操作系统使用几个cpu来运行程序。
3.抢占
和之前的普通调度器co-routine是主动释放cpu的,直到它主动释放这个cpu才能给其他程序运行。而goroutine是强制释放的,时间到了必须与这个cpu解绑。

4.全局队列
除了每个P管理的本地队列外,还提供了加锁的全局队列,当P中也没有可以被偷取的G时,就会从全局队列偷
创建goroutine
使用关键字go
package main
import (
"fmt"
"time"
)
//子goroutine
func newTask() {
for {
fmt.Println("子程序正在运行")
time.Sleep(1 * time.Second)
}
}
func main() {
go newTask() //通过go创建一个goroutine
for {
fmt.Println("主程序运行")
time.Sleep(1 * time.Second) //要在主程序里阻塞,因为这个子goroutine时依赖与主程序的,就和线程依赖于进程一样
}
}
退出goroutine
我们可以通过return来退出一个函数,但是如果一个函数里还有一个子函数,就不能在子函数里return了,此时return只能退出子函数而不能退出goroutine。这时候就需要 runtime.Goexit()
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
go func() {
fmt.Println("AAA")
func() {
runtime.Goexit()
fmt.Println("BBB")
}()
}()
for {
time.Sleep(1 * time.Second)
}
}
执行一遍会发下只打印了AAA,而没有打印BBB,说明在打印之前,已经退出了。
主goroutine得到子goroutine的返回值
想象以下如果想在main中得到它的子goroutine的返回值可以这么做吗?直接把返回值赋值给一个变量然后获取
func main() {
go_back:=go func task() { //直接把返回值赋值给go_back?
fmt.Println("AAA")
return 111
}()
for {
time.Sleep(1 * time.Second)
}
}
显然时不行的,因为主程序和子程序它们两个时异步操作,在同时运行,所以无法通过这种赋值的写法获得子goroutine的返回值
这时候就该channel上场了,通过它来保证goroutine之间的通信
channel
channel本身就是一种内置的数据类型,还记得go可以通过什么分配类型吗?忘了的可以去看看Golang入门-基础语法
可以通过make来定义一个channel
无缓冲
package main
import "fmt"
func main() {
c := make(chan int)
go func() {
c <- 666 //把666放进c //向通道里写数据
}()
num := <-c //从c中拿出num 拿数据,如果之后不用也可以直接<-c,也可以num,ok <-c ,这个ok可以检查管道是否关闭或者是否为空
fmt.Println(num)
}
通道会保证两个程序可以正常的发送和接受数据,当main里的程序已经走到num := <-c,而此时666还没传入c中,main程序就会一直阻塞,直到c里面有数据。同理,当子程序已经运行到c <- 666而主程序还没运行到num := <-c,子程序也会发送阻塞。总之管道两边必须有两只手同时伸进去才能握手交流信息。


有缓冲

有缓冲阻塞的情况就是管道中的空间被放满了。等右边读取了一个有位置了才能被放进去。或者时管道里是空的,去取数据也会阻塞,直到管道里有东西
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int, 3) //创建一个有缓冲且空间为3的通道
go func() {
defer fmt.Println("子go结束")
for i := 0; i < 3; i++ {
c <- i
fmt.Println("发送元素", i, "长度和容量分别是", len(c), cap(c))
}
}()
time.Sleep(2 * time.Second) //停两秒防止管道里的东西不足3个
for i := 0; i < 3; i++ {
num := <-c
fmt.Println(num)
}
}
这是运行结果

可以试试给通道make 3个容量,但是发送4个元素,就会发现子程序程序是先发送三个元素,主程序读取三个。然后子程序继续发送多出来的一个。
channel的关闭
比如子程序给通道发3个数据,但是主程序一直在无限循环读取,这时候如果不主动关闭通道的话,编译器就会报错,因为主程序会一直阻塞在那里。
通道关闭使用close()的函数的,传递的参数是要关闭的通道变量
package main
import "fmt"
func main() {
c := make(chan int)
go func() {
for i := 0; i < 3; i++ {
c <- i
}
close(c) //用来关闭通道
}()
for {
if data, ok := <-c; ok { //分号后面的就是判断的标准为布尔类型,前面的式子先执行一遍得到两个参数
fmt.Println(data) //如果通道没关闭,那么ok返回值就为true
} else {
break
}
}
}
可以尝试一下在子go中,把close(c)去掉,看看会报什么错。
通道关闭注意事项

channel与range
range可以用来迭代操作代替上面代码里的for循环里的各种判断条件。
for data:=range(c){
fmt.PrintIn(data)
}
channel与select
之前我们在一个go下只能监听一个channel,因为如果一个channel没数据的话就会阻塞,select可以完成监听多个channel的状态
package main
import "fmt"
func sub(c, quit chan int) {
a, b := 1, 1
for {
select {
case c <- a: //如果c可写
a += b
case <-quit: //如果cquit可读
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 5; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
sub(c, quit)
}
主go执行sub函数,传入两个通道,子go执行5次循环,每次循环打印一遍c通道里的值,循环结束后quit赋值为0。
sub函数用select和case判断,如果c可写就进入执行a=a+b ,啥时候c不可写呢?
就是当c子go里面不读取c的时候,这时候会就会跳过 case c <- a,这个判断。
循环结束后quit <- 0向quit通道里写了数据,那就说明quit这时候可读,进入这个判断
打印出quit并且return主程序结束。
事实上,与case平行的判断还可以加个default
如果上面的case都没成功就会进入default中
还记得我在基础语法里说的吗?channel传递时直接传入引用的而不是拷贝,所以函数里不需要写指针
不清楚的可以去看Golang入门-基础语法