【Golang】Go调度器(scheduler)与channel

go程序的运行,以goroutine为单位,而goroutine实际运行在某个系统线程内。goroutine(可以非常多)和系统线程(相对比较少)并非一一对应。调度时,既有os调度线程,也有go调度器本身调度goroutine。简言之,go原生支持并发,go调度器负责将各个goroutine调度到不同的操作系统线程中取执行。

通过go关键字启动一个协程来运行函数:

go func()
三个定义:
  • G: goroutine,就是平常提到的go中的协程
  • P: process,处理器,有的文章说代表上下文,也可以理解为附带有上下文信息的令牌
  • M: machine,线程,就是平常提到的操作系统中的线程

Go早期是GM模型,后来因为性能问题转而使用GPM模型。

执行机制:
  • M绑定P,才可以不断去运行(不同的可执行的)G,可抢占式调度(靠sysmon)
  • P有自己的G队列(无锁访问,快);同时,程序也有一个全局的G队列
  • M执行一些系统调用的时候,可能会与P解除绑定;M也可能休眠

M, P, G 三者数量各异,M默认10000(SetMaxThreads更改,一般不用),P默认是机器CPU核数(可由GOMAXPROCS指定),G没有明确限制(通过go指令创建)。

GM到GPM

早期,GM模型有诸多问题,例如全局锁,M缓存内存占用浪费等,详见《Scalable Go Scheduler Design》。

通俗地讲,G要运行,需要绑定一个P(放在P的本地队列里),然后由与P绑定的操作系统线程M真正执行。
G切换时,只是M从G1切到G2而已,都是在用户态进行着,非常轻量,不像操作系统切换M时比较重。
P的本地队列中缺少G时,会从其他P的队列里“偷”一些或者从全局队列里取。

借助于netpoller,发起网络调用时,G阻塞,M不阻塞,切换G即可。而发起文件IO等操作时,会执行(阻塞的)系统调用,(注:现在应该实现了部分poller for os package),此时M也会等待系统调用的返回。M和G一起,会解除与P的绑定。如果P的本地队列还有其他G,就会绑定另外一个空闲的M,如果没有,则新建一个M,然后继续执行可以执行的G。

调度器实现了抢占

也就是说如果一个G执行太久,是会被切换出去的。
这样可以确保整个程序看起来是“并发”执行的,而不是一个G可以执行时就是一直执行,其他G都饿死。
但是切换点需要是函数调用。假设G中是不调函数的纯无限循环计算,还是无法被抢占。

什么时候G会被调度:
  • 被sysmon设置为抢占
  • channel阻塞或网络IO
  • mutex等同步导致阻塞
  • 使用go关键字创建goroutine
  • GC过程中各种策略导致的调度
  • runtime中,网络IO的实现采用了kqueue (MacOS), epoll (Linux)或iocp (Windows) 。
查看各种调度状态:

执行命令的时候,设置GODEBUG环境变量。例如:GODEBUG=schedtrace=1000,scheddetail=1 godoc -http=:6060

P有local队列的好处

其实好处有好几点。比较明显的是,GM模型里面,M切换G时,需要从全局队列里面取,需要加锁。GPM中,M绑定着P,M切换的G都在P的本地G队列中,不需要锁。

P默认是机器逻辑核数

因为超线程技术的存在,逻辑核数会与物理核数不同。下面的语句可以打印出逻辑核数,通过GOMAXPROCS设置时,可别弄错了。

fmt.Println(runtime.NumCPU())
M默认是10000

M对应的是sched.maxmcount,默认10000。通过SetMaxThreads可修改,如果程序使用超过这个数,会自动crash!

// 改动时也会检查,并不能随意设置值
if in > 0x7fffffff { // MaxInt32
    sched.maxmcount = 0x7fffffff
} else {
    sched.maxmcount = int32(in)
}
goroutine异常捕捉

当启动多个goroutine时,如果其中一个goroutine异常了,并且我们并没有对进行异常处理,那么整个程序都会终止,所以我们在编写程序时候最好每个goroutine所运行的函数都做异常处理,异常处理采用recover。

package main

import (
    "fmt"
    "time"
)

func addele(a []int ,i int)  {
    defer func() {    //匿名函数捕获错误
        err := recover()
        if err != nil {
            fmt.Println("add ele fail")
        }
    }()
   a[i]=i
   fmt.Println(a)
}

func main() {
    Arry := make([]int,4)
    for i :=0 ; i<10 ;i++{
        go addele(Arry,i)
    }
    time.Sleep(time.Second * 2)
}

运行结果为:

add ele fail
[0 0 0 0]
[0 1 0 0]
[0 1 2 0]
[0 1 2 3]
add ele fail
add ele fail
add ele fail
add ele fail
add ele fail
同步的goroutine

由于goroutine是异步执行的,那很有可能出现主程序退出时还有goroutine没有执行完,此时goroutine也会跟着退出。此时如果想等到所有goroutine任务执行完毕才退出,go提供了sync包和channel来解决同步问题。

使用sync包同步goroutine:

package main

import (
    "fmt"
    "sync"
)

func cal(a int , b int ,n *sync.WaitGroup)  {
    c := a+b
    fmt.Printf("%d + %d = %d\n",a,b,c)
    defer n.Done() //goroutinue完成后, WaitGroup的计数-1

}

func main() {
    var go_sync sync.WaitGroup //声明一个WaitGroup变量
    for i :=0 ; i<10 ;i++{
        go_sync.Add(1) // WaitGroup的计数加1
        go cal(i,i+1,&go_sync)  
    }
    go_sync.Wait()  //等待所有goroutine执行完毕
}

结果为:

9 + 10 = 19
2 + 3 = 5
3 + 4 = 7
4 + 5 = 9
5 + 6 = 11
1 + 2 = 3
6 + 7 = 13
7 + 8 = 15
0 + 1 = 1
8 + 9 = 17

通过channel实现goroutine之间的同步:

实现方式:通过channel能在多个groutine之间通讯,当一个goroutine完成时候向channel发送退出信号,等所有goroutine退出时候,利用for循环channe去channel中的信号,若取不到数据会阻塞原理,等待所有goroutine执行完毕,使用该方法有个前提是你已经知道了你启动了多少个goroutine。

package main

import (
    "fmt"
    "time"
)

func cal(a int , b int ,Exitchan chan bool)  {
    c := a+b
    fmt.Printf("%d + %d = %d\n",a,b,c)
    time.Sleep(time.Second*2)
    Exitchan <- true
}

func main() {

    Exitchan := make(chan bool,10)  //声明并分配管道内存
    for i :=0 ; i<10 ;i++{
        go cal(i,i+1,Exitchan)
    }
    for j :=0; j<10; j++{   
         <- Exitchan  //取信号数据,如果取不到则会阻塞
    }
    close(Exitchan) // 关闭管道
}
goroutine之间的通讯

goroutine本质上是协程,可以理解为不受内核调度,而受go调度器管理的线程。goroutine之间可以通过channel进行通信或者说是数据共享,当然你也可以使用全局变量来进行数据共享。

示例:使用channel模拟消费者和生产者模式

package main

import (
    "fmt"
    "sync"
)

func Productor(mychan chan int,data int,wait *sync.WaitGroup)  {
    mychan <- data
    fmt.Println("product data:",data)
    wait.Done()
}

func Consumer(mychan chan int,wait *sync.WaitGroup)  {
     a := <- mychan
    fmt.Println("consumer data:",a)
     wait.Done()
}

func main() {

    datachan := make(chan int, 100)   //通讯数据管道
    var wg sync.WaitGroup

    for i := 0; i < 10; i++ {
        go Productor(datachan, i,&wg) //生产数据
        wg.Add(1)
    }
    for j := 0; j < 10; j++ {
        go Consumer(datachan,&wg)  //消费数据
        wg.Add(1)
    }
    wg.Wait()
}
channel

channel俗称管道,用于数据传递或数据共享,其本质是一个先进先出的队列,使用goroutine+channel进行数据通讯简单高效,同时也线程安全,多个goroutine可同时修改一个channel,不需要加锁。

channel可分为三种类型:

  • 只读channel:只能读channel里面数据,不可写入

  • 只写channel:只能写数据,不可读

  • 一般channel:可读可写

channel使用:

var readOnlyChan <-chan int            // 只读chan
var writeOnlyChan chan<- int           // 只写chan
var mychan  chan int                     //读写channel
//定义完成以后需要make来分配内存空间,不然使用会deadlock
mychannel = make(chan int,10)

//或者
read_only := make (<-chan int,10)//定义只读的channel
write_only := make (chan<- int,10)//定义只写的channel
read_write := make (chan int,10)//可同时读写

读写数据

需要注意的是:

  • 管道如果未关闭,在读取超时会则会引发deadlock异常
  • 管道如果关闭进行写入数据会pannic
  • 当管道中没有数据时候再行读取或读取到默认值,如int类型默认值是0
ch <- "wd"  //写数据
a := <- ch //读取数据
a, ok := <-ch  //优雅的读取数据
循环管道

需要注意的是:

使用range循环管道,如果管道未关闭会引发deadlock错误。
如果采用for死循环已经关闭的管道,当管道没有数据时候,读取的数据会是管道的默认值,并且循环不会退出。

package main

import (
    "fmt"
)


func main() {
    mychannel := make(chan int,10)
    for i := 0;i < 10;i++{
        mychannel <- i
    }
    close(mychannel)  //关闭管道
    fmt.Println("data lenght: ",len(mychannel))
    for  v := range mychannel {  //循环管道
        fmt.Println(v)
    }
    fmt.Printf("data lenght:  %d",len(mychannel))
}

输出结果为:

data lenght:  10
0
1
2
3
4
5
6
7
8
9
data lenght:  0
带缓冲区channe和不带缓冲区channel

带缓冲区channel:定义声明时候制定了缓冲区大小(长度),可以保存多个数据。

不带缓冲区channel:只能存一个数据,并且只有当该数据被取出时候才能存下一个数据。

ch := make(chan int) //不带缓冲区
ch := make(chan int ,10) //带缓冲区

不带缓冲区示例:

package main

import "fmt"

func test(c chan int) {
    for i := 0; i < 10; i++ {
        fmt.Println("send ", i)
        c <- i
    }
}
func main() {
    ch := make(chan int)
    go test(ch)
    for j := 0; j < 10; j++ {
        fmt.Println("get ", <-ch)
    }
}

输出结果为:

send  0
send  1
get  0
get  1
send  2
send  3
get  2
get  3
send  4
send  5
get  4
get  5
send  6
send  7
get  6
get  7
send  8
send  9
get  8
get  9
channel实现作业池

我们创建三个channel,一个channel用于接受任务,一个channel用于保持结果,还有个channel用于决定程序退出的时候。

只读channel和只写channel

一般定义只读和只写的管道意义不大,更多时候我们可以在参数传递时候指明管道可读还是可写,即使当前管道是可读写的。

package main

import (
    "fmt"
    "time"
)

//只能向chan里写数据
func send(c chan<- int) {
    for i := 0; i < 10; i++ {
        c <- i
    }
}
//只能取channel中的数据
func get(c <-chan int) {
    for i := range c {
        fmt.Println(i)
    }
}
func main() {
    c := make(chan int)
    go send(c)
    go get(c)
    time.Sleep(time.Second*1)
}
select-case实现非阻塞channel

原理通过select+case加入一组管道,当满足(这里说的满足意思是有数据可读或者可写)select中的某个case时候,那么该case返回,若都不满足case,则走default分支。

package main

import (
    "fmt"
)

func send(c chan int)  {
    for i :=1 ; i<10 ;i++  {
     c <-i
     fmt.Println("send data : ",i)
    }
}

func main() {
    resch := make(chan int,20)
    strch := make(chan string,10)
    go send(resch)
    strch <- "wd"
    select {
    case a := <-resch:
        fmt.Println("get data : ", a)
    case b := <-strch:
        fmt.Println("get data : ", b)
    default:
        fmt.Println("no channel actvie")

    }

}

运行结果为:

get data :  wd
package main

import (
	"fmt"
	"time"
)

func send(c chan int)  {
    for i :=1 ; i<10 ;i++  {
     c <-i
     fmt.Println("send data : ",i)
    }
}

func main() {
    resch := make(chan int,20)
    strch := make(chan string,10)
    go send(resch)
	strch <- "wd"
	time.Sleep(time.Second * 2)
    select {
    case a := <-resch:
        fmt.Println("get data : ", a)
    case b := <-strch:
        fmt.Println("get data : ", b)
    default:
        fmt.Println("no channel actvie")

    }
}

以上的运行结果为:

send data :  1
send data :  2
send data :  3
send data :  4
send data :  5
send data :  6
send data :  7
send data :  8
send data :  9
get data :  1
channel频率控制

在对channel进行读写的时,go还提供了非常人性化的操作,那就是对读写的频率控制,通过time.Ticke实现

package main

import (
    "time"
    "fmt"
)

func main(){
    requests:= make(chan int ,5)
    for i:=1;i<5;i++{
        requests<-i
    }
    close(requests)
    limiter := time.Tick(time.Second*1)
    for req:=range requests{
        <-limiter
        fmt.Println("requets",req,time.Now()) //执行到这里,需要隔1秒才继续往下执行,time.Tick(timer)上面已定义
    }
}

运行结果为:

requets 1 2020-04-02 02:19:58.089127 +0800 CST m=+1.002352626
requets 2 2020-04-02 02:19:59.087781 +0800 CST m=+2.000975368
requets 3 2020-04-02 02:20:00.092095 +0800 CST m=+3.005257639
requets 4 2020-04-02 02:20:01.096952 +0800 CST m=+4.010083443
发布了324 篇原创文章 · 获赞 14 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/LU_ZHAO/article/details/105260308