go语言 - Scheduler原理以及查看Goroutine执行

幸运草
幸运草
幸运草
896
文章
3
评论
2020年4月22日21:59:03 评论 94

最近看了看go scheduler的基本原理,本文介绍go语言scheduler的基本原理以及如何查看go代码中的go routine的执行情况。

0)Scheduler(调度器)

熟悉go语言的小伙伴应该都使用过goroutine。goroutine就是Go语言提供的一种用户态线程。Scheduler是调度goroutine的调度器。

go语言 - Scheduler原理以及查看Goroutine执行

Go的调度器内部有三个重要概念:M,P,G。
M (machine): 代表真正的内核操作系统里面的线程,和POSIX里的thread差不多,也是真正执行goroutine逻辑的部分。
G (Goroutine): 代表一个goroutine。
P (Processor): 代表调度的上下文,可以理解成一个局部调度器。

Go语言实现了多个Goroutine到多个Processor的映射(调度)。注意的是,针对X个Processor,Scheduler可能创建多于X个M(有些M可能会暂时被block)。还需要理解额外两个概念:GRQ(Global Running Queue)以及 LRQ(Local Running Queue)。未指定Processor的Goroutine会存放在GRQ上,在调度到合适的Processor后,会将一个Goroutine从GRQ移动到LRQ。

Go程序中发生了四类事件,允许调程序做出调度决策。

a. 使用关键字go b. 垃圾收集 c. 系统调用 d. 同步

1)Processor的个数

Processor的个数可以通过GOMAXPROCS环境变量设置。GOMAXPROCS默认值是CPU的核数。Processor的个数可以通过如下的go代码进行查询:

package main

import (

"fmt"

"runtime"

)

func main() {

// NumCPU returns the number of logical

// CPUs usable by the current process.

fmt.Println(runtime.NumCPU())

}

也就是通过runtime.NumCPU函数可以获得Processor的个数。查看go语言的源代码(runtime/os_linux.c),NumCPU函数的实现函数如下:

 func getproccount() int32 {

const maxCPUs = 64 * 1024

var buf [maxCPUs / 8]byte

r := sched_getaffinity(0, unsafe.Sizeof(buf), &buf[0])

if r < 0 {

return 1

}

n := int32(0)

for _, v := range buf[:r] {

for v != 0 {

n += int32(v & 1)

v >>= 1

}

}

if n == 0 {

n = 1

}

return n

}

在linux操作系统中,通过调用sched_getaffinity函数获取cpu_set_t的信息,从而推算出CPU的个数。

2)查看Processor以及Goroutine的调度情况

在go程序命令行加上GODEBUG=schedtrace=1000,scheddetail=1选项,可以实时打印Goroutine的调度情况:schedtrace参数指定打印的时间间隔,scheddetail参数指定是否打印更多细节。

以下面的代码为例:

package main

import "fmt"

import "time"

func f(from string, index int) {

fmt.Println("index: ", index)

var i int=0

for true {

i  = i + 1

//time.Sleep(1 * time.Second)

}

}

func main() {

var i int= 0

for true {

go f("goroutine", i)

i+=1

time.Sleep(1 * time.Second)

}

}

这段go程序,不停的创建Goroutine,每个Goroutine执行f函数。注意,f函数中也是一个for循环(默认的情况下Sleep函数不起作用)。

如果只用schedtrace=1000参数,在MAC笔记本上的输出结果如下:

index:  0

index:  1

SCHED 0ms: gomaxprocs=4 idleprocs=2 threads=5 spinningthreads=0 idlethreads=1 runqueue=0 [0 0 0 0]

index:  2

SCHED 1009ms: gomaxprocs=4 idleprocs=1 threads=6 spinningthreads=0 idlethreads=1 runqueue=0 [0 0 0 0]

index:  3

SCHED 2019ms: gomaxprocs=4 idleprocs=0 threads=6 spinningthreads=0 idlethreads=0 runqueue=0 [0 0 0 0]

SCHED 3020ms: gomaxprocs=4 idleprocs=0 threads=6 spinningthreads=0 idlethreads=1 runqueue=1 [0 0 0 0]

SCHED 4021ms: gomaxprocs=4 idleprocs=0 threads=6 spinningthreads=0 idlethreads=1 runqueue=1 [0 0 0 0]

SCHED 5030ms: gomaxprocs=4 idleprocs=0 threads=6 spinningthreads=0 idlethreads=1 runqueue=1 [0 0 0 0]

一行SCHED开头的log打印出了当前时刻Scheduler的状态:

gomaxprocs:多少个Processor

idleProcs:多少个空闲的Processor

threads:多少个M

idlethreads:多少个空闲thread

runqueue:GRQ以及LRQ的状态,第一个数字是GRQ中的Goroutine的数量,[ ] 中的数字是各个LRQ中的Goroutine的数量。

你会发现上述的go代码中只跑了4个Goroutine,不会创建新的Goroutine,而且GRQ中一直有个Goroutine永远调度不到。原因是f函数是个死循环,不会释放Processor。

如果在f函数中加入延时,则会不停的创建以及调度Goroutine。

如果使用scheddetail=1,会打印出具体的Thread的执行信息。

总结:goroutine就是Go语言提供的一种用户态线程。Scheduler是调度goroutine的调度器。Go的调度器内部由三部分组成:M(Machine),P(Processor),G(Goroutine)。在go程序命令行加上GODEBUG=schedtrace=1000,scheddetail=1选项,可以实时打印Goroutine的调度情况。

特别声明:以上文章内容仅代表作者本人观点,不代表变化吧观点或立场。如有关于作品内容、版权或其它问题请于作品发表后的30日内与变化吧联系。

转载请注明:{{title}}-变化吧
  • 赞助本站
  • 微信扫一扫
  • weinxin
  • 赞助本站
  • 支付宝扫一扫
  • weinxin
幸运草
Go语言接口规则 前端框架

Go语言接口规则

Go语言接口规则 接口是一个或多个方法签名的集合。任何类型的方法集中只要拥有该接口对应的全部方法签名。就表示它 "实现" 了该接口,无须在该类型上显式声明实现了哪个接口。对应方法,是指有相同名称、参数列表 (不包括参数名) 以及返回值,该类型也可以有其他方法。 接口赋值 对象赋值给接口时,会发生拷贝,而接口内部存储的是指向这个复制品的指针,既无法修改复制品的状态,也无法获取指针。 package main import "fmt" type User struct {     id   int     name string } func main() {     u := User{18, "oldboy"}     var i interface{} = u     u.id = 20     u.name = "Golang"     fmt.Printf("u : %vn", u)     fmt.Printf("i.(User) : %vn", i.(User)) } 运行结果: u : {20 Golang} i.(User) : {18 oldboy} 接口转型返回临时对象,只有使用指针才能修改其状态。 package main import "fmt" type User struct {     id   int     name string } func main() {     u := User{18, "oldboy"}     var vi, pi interface{} = u, &u     // vi.(User).name = "Golang"     pi.(*User).name = "Golang"     fmt.Printf("vi.(User) : %vn", vi.(User))     fmt.Printf("pi.(*User) : %vn", pi.(*User)) } 空接口 只有当接口存储的类型和对象都为nil时,接口才等于nil。 package main import (     "fmt" ) func main() {     var i interface{}     fmt.Printf("i => %vn", i)     fmt.Printf("(i == nil) => %vn", i == nil)     var p *int = nil     // i 指向 p,指向的对象是个nil,但是存在类型不是nil,是个指针     i = p     fmt.Printf("i => %vn", i)     fmt.Printf("(i == nil) => %vn", i == nil) } 运行结果: i => <nil> (i == nil) => true i => <nil> (i == nil) => false 接口实现 接口只有方法声明,没有数据字段,没有实现,也不需要显示的实现。只要一个变量,含有接口类型中的所有方法,那么这个变量就实现这个接口。 package main import (     "fmt" ) type Info interface {     GetAge() int     GetName() string } type User struct {     name string     age  int } func (u User) GetAge() int {     return u.age } func (u User) GetName() string {     return u.name } func main() {     var user Info = User{"oldboy", 18}     age := user.GetAge()     name := user.GetName()     fmt.Println(age, name) } 如果一个变量含有了多个interface类型的方法,那么这个变量就实现了多个接口。 package main import (     "fmt" ) type Age interface {     GetAge() int } type Name interface {     GetName() int } type User struct {     name string...
Go语言中处理 HTTP 服务器 前端框架

Go语言中处理 HTTP 服务器

1 概述 包 net/http 提供了HTTP服务器端和客户端的实现。本文说明关于服务器端的部分。 快速开始: package main import (   "log"   "net/http" ) func main() {   // 设置 路由   http.HandleFunc("/", IndexAction)   // 开启监听   log.Fatal(http.ListenAndServe(":8888", nil)) } func IndexAction(w http.ResponseWriter, r *http.Request) {  w.Write(byte(`<h1 align="center">来自变化吧的问候</h1>`)) } 运行程序,在浏览器上请求: localhost:8888,你会看到我们的结果 Go语言构建HTTP服务器还是很容易的。深入说明。 2 http.Server 类型 HTTP 服务器在 Go 语言中是由 http.Server 结构体对象实现的。参考 http.ListenAndServe() 的实现: // 文件:src/net/http/server.go // ListenAndServe always returns a non-nil error. func ListenAndServe(addr string, handler Handler) error {   server := &Server{Addr: addr, Handler: handler}   return server.ListenAndServe() } 可见过程是先实例化 Server 对象,再完成 ListenAndServe 。其中 Serve 对象就是表示 HTTP 服务器的对象。其结构如下 : // 文件:src/net/http/server.go type Server struct {   Addr    string  // TCP 监听地址, 留空为:":http"   Handler Handler // 调用的 handler(路由处理器), 设为 nil 表示 http.DefaultServeMux   TLSConfig *tls.Config // TLS 配置对象   ReadTimeout time.Duration // 请求超时时长   ReadHeaderTimeout time.Duration // 请求头超时时长   WriteTimeout time.Duration // 响应超时时长   IdleTimeout time.Duration // 请求空闲时长(keep-alive下两个请求间)   MaxHeaderBytes int // 请求头的最大长度   TLSNextProto mapfunc(*Server, *tls.Conn, Handler) // NPN 型协议升级出现时接管TLS连接的处理器函数映射表   ConnState func(net.Conn, ConnState) // 状态转换事件处理器   ErrorLog *log.Logger // 日志记录对象   disableKeepAlives int32     // accessed atomically.   inShutdown        int32     // accessed atomically (non-zero means we're in Shutdown)   nextProtoOnce     sync.Once // guards setupHTTP2_* init   nextProtoErr      error     // result of http2.ConfigureServer if used   mu         sync.Mutex   listeners  mapstruct{}   activeConn mapstruct{}   doneChan   chan struct{}   onShutdown func() } 可见 Server 定义了服务器需要的信息。 实例化了 Server 对象后,调用其 (srv *Server) ListenAndServe() error 方法。该方法会监听 srv.Addr 指定的 TCP 地址,并通过 (srv *Server) Serve(l net.Listener) error 方法接收浏览器端连接请求。Serve 方法会接收监听器 l 收到的每一个连接,并为每一个连接创建一个新的服务进程。 该 go...
go语言动态库的编译和使用 前端框架

go语言动态库的编译和使用

本文主要介绍go语言动态库的编译和使用方法,以linux平台为例,windows平台步骤一样,具体环境如下: $ echo $GOPATH /media/sf_share/git/go_practice $ echo $GOROOT /usr/lib/golang/ $ tree $GOPATH/src /media/sf_share/git/go_practice/src |-- demo |   `-- demo.go `-- main.go 1 directory, 2 files 在$GOPATH/src目录,有demo包和使用demo包的应用程序main.go,main.go代码如下: package main import "demo" func main() {    demo.Demo() } demo包中的demo.go代码如下: package demo import "fmt" func Demo() {    fmt.Println("call demo ...") } 由于demo.go是$GOPATH/src目录下的一个包,main.go在import该包后,可以直接使用,运行main.go: $ go run main.go call demo ... 现在,需要将demo.go编译成动态库libdemo.so,让main.go以动态库方式编译,详细步骤如下: 1 将go语言标准库编译成动态库 $ go install -buildmode=shared -linkshared  std 在命令行运行go install -buildmode=shared -linkshared  std命令,-buildmode指定编译模式为共享模式,-linkshared表示链接动态库,成功编译后会在$GOROOT目录下生标准库的动态库文件libstd.so,一般位于$GOROOT/pkg/linux_amd64_dynlink目录: $ cd $GOROOT/pkg/linux_amd64_dynlink $ ls libstd.so libstd.so 2 将demo.go编译成动态库 $ go install  -buildmode=shared -linkshared demo $ cd $GOPATH/pkg $ ls linux_amd64_dynlink/ demo.a  demo.shlibname  libdemo.so 成功编译后会在$GOPATH/pkg目录生成相应的动态库libdemo.so。 3 以动态库方式编译main.go $ go...
Go语言常见排序算法 前端框架

Go语言常见排序算法

Go语言常见排序算法 冒泡排序 思路分析:在要排序的切片中,对当前还未排好的序列,从前往后对相邻的两个元素依次进行比较和调整,让较大的数往下沉,较小的往上冒。即,每当两相邻的元素比较后发现它们的排序与排序要求相反时,就将它们互换。 代码实现: package main import (     "fmt" ) var sli = int{1, 43, 54, 62, 21, 66, 32, 78, 36, 76, 39} func bubbleSort(sli int) int {     len := len(sli)     //该层循环控制 需要冒泡的轮数     for i := 1; i < len; i++ {         //该层循环用来控制每轮 冒出一个数 需要比较的次数         for j := 0; j < len-1; j++ {             if sli < sli {                 sli, sli = sli, sli             }         }     }     return sli } func main() {     res := bubbleSort(sli)     fmt.Println(res) } 选择排序 思路分析:在要排序的切片中,选出最小的一个元素与第一个位置的元素交换。然后在剩下的元素当中再找最小的与第二个位置的元素交换,如此循环到倒数第二个元素和最后一个元素比较为止。 代码实现: package main import (     "fmt" ) var sli = int{1, 43, 54, 62, 21, 66, 32, 78, 36, 76, 39} func selectSort(sli int) int {     //双重循环完成,外层控制轮数,内层控制比较次数     len := len(sli)     for i := 0; i < len-1; i++ {         //先假设最小的值的位置         k := i         for j := i + 1; j < len; j++ {             //sli 是当前已知的最小值             if sli > sli {                 //比较,发现更小的,记录下最小值的位置;并且在下次比较时采用已知的最小值进行比较。                 k = j             }         }         //已经确定了当前的最小值的位置,保存到 k 中。如果发现最小值的位置与当前假设的位置 i 不同,则位置互换即可。         if k != i {             sli, sli = sli, sli         }     }     return sli } func main() {     res := selectSort(sli)     fmt.Println(res) } 快速排序 思路分析:选择一个基准元素,通常选择第一个元素或者最后一个元素。通过一趟扫描,将待排序列分成两部分,一部分比基准元素小,一部分大于等于基准元素。此时基准元素在其排好序后的正确位置,然后再用同样的方法递归地排序划分的两部分。 代码实现: package main import (     "fmt" ) var sli = int{1, 43, 54, 62, 21, 66, 32, 78, 36, 76, 39} func quickSort(sli int) int {     //先判断是否需要继续进行     len := len(sli)     if len <= 1 {         return sli     }     //选择第一个元素作为基准     base_num := sli     //遍历除了标尺外的所有元素,按照大小关系放入左右两个切片内     //初始化左右两个切片     left_sli := int{}  //小于基准的     right_sli := int{} //大于基准的     for i := 1; i < len; i++ {         if base_num > sli {             //放入左边切片             left_sli = append(left_sli, sli)         } else {             //放入右边切片             right_sli = append(right_sli, sli)         }     }     //再分别对左边和右边的切片进行相同的排序处理方式递归调用这个函数     left_sli = quickSort(left_sli)     right_sli = quickSort(right_sli)     //合并     left_sli = append(left_sli, base_num)     return append(left_sli, right_sli...) } func main() {     res := quickSort(sli)     fmt.Println(res) } 插入排序 思路分析:在要排序的一切片中,假设前面的元素已经是排好顺序的,现在要把第n个元素插到前面的有序切片中,使得这n个元素也是排好顺序的。如此反复循环,直到全部排好顺序。...