Go语言笔记----goroutine和channel

x33g5p2x  于2022-04-06 转载在 其他  
字(4.7k)|赞(0)|评价(0)|浏览(740)

goroutine基本模型和调度设计策略

单进程时代的两个问题:

  • 单一执行流程,计算机只能一个任务一个任务的处理
  • 进程阻塞带来的cpu浪费时间

  • 多线程和多进程解决了阻塞问题,但是又遇到了新的问题

  • 进程/线程的数量越多,切换成本就越大

  • 多线程随着同步竞争(如: 锁,竞争资源冲突等),开发设计更加复杂

思考:如果我把这个线程一分为二会怎么样?

cpu只能看见内核线程

  • 一个cpu绑定的内核线程可以通过协成调度器轮询处理多个协程
  • 但是这样做有一个弊端: 如果轮询过程中在某个协程处阻塞住了,那么后面的协程执行必定受到影响

Go对协程的处理

Go对早期调度器的处理

老的调度器缺点

GMP

调度器的设计策略

复用线程

work stealing机制

hand off机制

如果M1对应处理器正在处理的G1阻塞住了,那么你猜猜P的本地队列里面的G2是等待直到阻塞结束呢?还是有什么好的办法可以让他不受阻塞影响,可以接着处理呢?

这里当然是后者了

利用并行

GOMAXPROCS可以决定使用多少个CPU

抢占策略

相当于利用了时间片机制,每个goroutine最多被cpu宠幸10ms

全局G队列

当然会先去其他队列偷,如果其他队列没有,那么才会尝试去全局队列获取,因为去全局队列拿的话,需要加锁和解锁,比较浪费时间

创建goroutine

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. //子goroutine
  7. func newTask(){
  8. i:=0
  9. for{
  10. i++
  11. fmt.Printf("new GoRoutine: i=%d\n",i)
  12. time.Sleep(1*time.Second)
  13. }
  14. }
  15. func main() {
  16. //创建一个go程去执行newTask()方法
  17. go newTask();
  18. i:=0
  19. for{
  20. i++
  21. fmt.Printf("Main GoRoutine: i=%d\n",i)
  22. time.Sleep(1*time.Second)
  23. }
  24. }

  1. package main
  2. import (
  3. "fmt"
  4. "runtime"
  5. "time"
  6. )
  7. func main() {
  8. //用go创建一个形参为空格,返回值为空的一个函数
  9. go func(){
  10. defer fmt.Println("A.defer")
  11. func(){
  12. defer fmt.Println("B.defer")
  13. //退出当前goroutine
  14. runtime.Goexit()//终止当前的goroutine
  15. fmt.Println("B")
  16. }()
  17. fmt.Println("A")
  18. }()//()表示匿名函数的调用
  19. //这里go和主线程并行执行,如果学过java线程的小伙伴都懂,如果要在两个线程之间传递数据一般需要使用一个通道或者队列来存放共享数据
  20. go func(a int,b int)bool{
  21. fmt.Println("a= ",a," b= ",b)
  22. return true
  23. }(10,20)
  24. //如果不等一下的话,主线程直接就结束了,goroutine还没来得及执行就死了
  25. time.Sleep(time.Second*3)
  26. }

Channel基本定义和使用

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func main() {
  7. //定义一个channel
  8. c:=make(chan int)
  9. go func(){
  10. defer fmt.Println("goroutine over!!!")
  11. fmt.Println("goroutine is running!!!")
  12. fmt.Println("sleeping 2 s")
  13. time.Sleep(2*time.Second)
  14. c <-666 //将666发送给c
  15. time.Sleep(2*time.Second)
  16. fmt.Println("repeat sleeping 2s")
  17. }()
  18. num:= <-c //从c中接收参数,并赋值给num
  19. fmt.Println("num= ",num)
  20. fmt.Println("main goroutine over!!!")
  21. }

显然channel的作用就是用来同步的,那么同步机制是什么呢?

还有一种情况:

channel有缓冲和无缓冲同步问题

无缓冲的channel

  • 在第 1 步,两个 goroutine 都到达通道,但哪个都没有开始执⾏发送或者接收。
  • 在第 2 步,左侧的 goroutine 将它的⼿伸进了通道,这模拟了向通道发送数据的⾏为。这时,这个 goroutine 会在通道中被锁住,直到交换完成。
  • 在第 3 步,右侧的 goroutine 将它的⼿放⼊通道,这模拟了从通道⾥接收数据。这个 goroutine ⼀样也会在通道中被锁住,直到交换完成.
  • 在第 4 步和第 5 步,进⾏交换,并最终,在第 6 步,两个 goroutine 都将它们的⼿从通道⾥拿出来,这模拟了被锁住的 goroutine 得到释放。两个 goroutine 现在都可以去做其他事情了

有缓冲的Channel

  • 在第 1 步,右侧的 goroutine 正在从通道接收⼀个值
  • 在第 2 步,右侧的这个 goroutine独⽴完成了接收值的动作,⽽左侧的 goroutine 正在发送⼀个新值到通道⾥
  • 在第 3 步,左侧的goroutine 还在向通道发送新值,⽽右侧的 goroutine 正在从通道接收另外⼀个值。这个步骤⾥的两个操作既不是同步的,也不会互相阻塞
  • 最后,在第 4 步,所有的发送和接收都完成,⽽通道⾥还有⼏个值,也有⼀些空间可以存更多的值

特点

  • 当channel已经满,再向⾥⾯写数据,就会阻塞
  • 当channel为空,从⾥⾯取数据也会阻塞

有缓冲Channel使用演示:

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func main() {
  7. //定义一个带有缓冲的Channel
  8. c:=make(chan int,3)
  9. go func(){
  10. defer fmt.Println("goroutine over!!!")
  11. fmt.Println("goroutine is running!!!")
  12. for i:=0;i<3 ;i++ {
  13. c<-i
  14. fmt.Println("发送的元素i= ",i," 通道长度len= ",len(c)," 通道容量cap= ",cap(c))
  15. }
  16. }()
  17. time.Sleep(time.Second)
  18. for i:=0;i<3 ;i++ {
  19. num:=<-c
  20. fmt.Println("num= ",num)
  21. }
  22. fmt.Println("main goroutine over!!!")
  23. }

错误示范:

  1. func main() {
  2. //定义一个带有缓冲的Channel
  3. c:=make(chan int,4)
  4. go func(){
  5. defer fmt.Println("goroutine over!!!")
  6. fmt.Println("goroutine is running!!!")
  7. for i:=0;i<5 ;i++ {
  8. c<-i
  9. fmt.Println("发送的元素i= ",i," 通道长度len= ",len(c)," 通道容量cap= ",cap(c))
  10. }
  11. }()
  12. time.Sleep(time.Second)
  13. //尝试从channel中读取第六个元素的时候会报错---因为此时没有goroutine会尝试往通道中写入数据
  14. for i:=0;i<6;i++ {
  15. num:=<-c
  16. fmt.Println("num= ",num)
  17. }
  18. fmt.Println("main goroutine over!!!")
  19. }

Channel的关闭

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. //定义一个带有缓冲的Channel
  7. c:=make(chan int)
  8. go func(){
  9. for i:=0;i<6;i++ {
  10. c<-i
  11. }
  12. //close可以关闭一个channel
  13. close(c)
  14. }()
  15. for{
  16. //ok如果为true表示channel没有关闭,如果为false表示channel已经关闭
  17. if data,ok :=<-c; ok{
  18. fmt.Println(data)
  19. }else {
  20. break
  21. }
  22. }
  23. fmt.Println("main goroutine over!!!")
  24. }

  • channel不像⽂件⼀样需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束range循环之类的,才去关闭channel
  • 关闭channel后,⽆法向channel 再发送数据(引发 panic 错误后导致接收⽴即返回零值)
  • 关闭channel后,可以继续从channel接收数据
  • 对于nil channel,⽆论收发都会被阻塞

Channel和Range

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. //定义一个带有缓冲的Channel
  7. c:=make(chan int)
  8. go func(){
  9. for i:=0;i<6;i++ {
  10. c<-i
  11. }
  12. //close可以关闭一个channel
  13. close(c)
  14. }()
  15. /*
  16. for{
  17. //ok如果为true表示channel没有关闭,如果为false表示channel已经关闭
  18. if data,ok :=<-c; ok{
  19. fmt.Println(data)
  20. }else {
  21. break
  22. }
  23. }*/
  24. //可以使用range来迭代不断操作channel
  25. //如果channel有数据就循环读取一次,直到通道关闭,才会结束读取
  26. for data:= range c{
  27. fmt.Println(data)
  28. }
  29. fmt.Println("main goroutine over!!!")
  30. }

Channel与select

单流程下⼀个go只能监控⼀个channel的状态,select可以完成监控多个channel的状态

伪代码:

以斐波那契数列为例吧:

  1. package main
  2. import "fmt"
  3. func main() {
  4. //无缓冲罐channel
  5. c:=make(chan int)
  6. quit:=make(chan int)
  7. //sub go
  8. go func(){
  9. for i:=0;i<10;i++ {
  10. //输出通道c里面的数据
  11. //如果通道此时没有数据会阻塞等待
  12. fmt.Println(<-c)
  13. }
  14. quit<-0
  15. }()
  16. //main go
  17. fibonacii(c,quit)
  18. }
  19. func fibonacii(c , quit chan int) {
  20. x,y:=1,1
  21. for{
  22. select{
  23. //如果向通道c写入数据x成功,那么会进入下面这个分支语句
  24. case c<-x:
  25. x=y
  26. y=x+y
  27. //如果quit通道成功读取到了数据,则进入该分支语句
  28. case <-quit:
  29. fmt.Println("quit")
  30. return
  31. }
  32. }
  33. }

select具备多路channel的监控状态功能

相关文章