当前位置: 首页 > article >正文

【Go】并发编程之runtime包及其常用方法

文章目录

  • 一、runtime 包
    • 1. runtime 包是干什么用的?
    • 2. runtime 包内的一些方法简介
  • 二、runtime.Gosched()
  • 三、runtime.Goexit()
  • 四、runtime.GOMAXPROCS()
  • 五、runtime.NumCPU()、runtime.GOROOT()、runtime.GOOS
  • 参考链接


一、runtime 包

1. runtime 包是干什么用的?

我的上篇文章【Go】并发编程 中提到过,Go 语言的 goroutine 是由 运行时(runtime)调度和管理的。这篇文章我们来详细介绍 runtime 调度器的知识。

尽管 Go 编译器产生的是本地可执行代码,这些代码仍旧运行在 Go 的 runtime(这部分的代码可以在 runtime 包中找到)当中。Go 语言的 runtime 类似 Java 和 .NET 语言所用到的虚拟机,它负责管理包括内存分配、垃圾回收(第 10.8 节)、栈处理、goroutine、channel、切片(slice)、map 和反射(reflection)等等。

2. runtime 包内的一些方法简介

runtime 调度器是个非常有用的东西,关于 runtime 包几个方法:

  1. Gosched():让当前线程让出 cpu 以让其它线程运行,它不会挂起当前线程,因此当前线程未来会继续执行。

  2. NumCPU():返回当前系统的 CPU 核数量。

  3. GOMAXPROCS():设置最大的可同时使用的 CPU 核数。
    通过runtime.GOMAXPROCS函数,应用程序可以设置运行时系统中的 P 最大数量。注意,如果在运行期间设置该值的话,会引起“Stop the World”。所以,应在应用程序最早期调用,并且最好是在运行Go程序之前设置好操作程序的环境变量GOMAXPROCS,而不是在程序中调用runtime.GOMAXPROCS函数。
    无论我们传递给函数的整数值是什么值,运行时系统的P最大值总会在1~256之间。
    go1.8 后,默认让程序运行在多个核上,可以不用设置了。
    go1.8 前,还是要设置一下,可以更高效的利用 cpu。

  4. Goexit():退出当前 goroutine(但是defer语句会照常执行)。

  5. NumGoroutine:返回正在执行和排队的任务总数。
    runtime.NumGoroutine函数在被调用后,会返回系统中的处于特定状态的 Goroutine 的数量。这里的特定状态是指Grunnable\Gruning\Gsyscall\Gwaition。处于这些状态的Groutine即被看做是活跃的或者说正在被调度。
    注意:垃圾回收所在Groutine的状态也处于这个范围内的话,也会被纳入该计数器。

  6. GOOS:查看目标操作系统。很多时候,我们会根据平台的不同实现不同的操作,就可以用GOOS来查看自己所在的操作系统。

  7. runtime.GC:会让运行时系统进行一次强制性的垃圾收集。
    强制的垃圾回收:不管怎样,都要进行的垃圾回收。非强制的垃圾回收:只会在一定条件下进行的垃圾回收(即运行时,系统自上次垃圾回收之后新申请的堆内存的单元(也成为单元增量)达到指定的数值)。

  8. GOROOT() :获取 goroot 目录。

  9. runtime.LockOSThread 和 runtime.UnlockOSThread 函数:前者调用会使调用他的 Goroutine 与当前运行它的M锁定到一起,后者调用会解除这样的锁定。


二、runtime.Gosched()

让出当前协程的 CPU 时间片给其他协程。当前协程等待时间片未来继续执行。

释放时间片,先让别的协程执行,它执行完,再回来执行此协程。

package mainimport ("fmt""runtime"
)func main() {go func(s string) {for i := 0; i < 2; i++ {fmt.Println(s)}}("world")// 主协程for i := 0; i < 2; i++ {runtime.Gosched()    //主协程释放CPU时间片,此时上面的协程得以执行fmt.Println("hello") //CPU时间片回来后继续执行}
}

输出结果:

hello
world
hello
world

或:

world
world
hello
hello

第一个结果解释:进入主协程的第一轮 for 循环,主协程让出CPU时间片时,上面的协程还没创建好,因此没有其他协程可以使用时间片,那么主协程继续执行,先打印了hello。进入主协程的第二轮 for 循环,主协程让出CPU时间片时,上面的协程打印了world,然后主协程又得到时间片打印了hello,在主协程结束进程之前,上面的协程打印了world。

第二个结果解释:进入主协程的第一轮 for 循环,主协程让出CPU时间片时,上面的协程已经创建好,并打印了两个world,然后主协程继续执行,打印了一个hello。进入主协程的第二轮 for 循环,主协程让出CPU时间片时,已经没有协程正在等待执行,所以主协程继续打印了一个hello,然后结束。


三、runtime.Goexit()

退出当前协程,但是 defer 语句会照常执行。

package mainimport ("fmt""runtime"
)func main() {go func() {defer fmt.Println("A.defer")func() {defer fmt.Println("B.defer")runtime.Goexit() // 结束当前协程defer fmt.Println("C.defer")fmt.Println("B")}()fmt.Println("A")}()fmt.Println("main")
}

输出结果:

main
B.defer
A.defer

main
B.defer

main

在我们自己的协程结束之前,是会打印已定义的 B.defer 和 A.defer 的,这说明:

如果我们用 runtime.Goexit() 结束协程,仍然会执行 defer 语句。

第一个结果解释:主协程打印了main,在主协程结束之前的一小段时间,我们的协程抓紧时间执行了defer语句:打印了 B.defer 和 A.defer。

第二个结果解释:主协程打印了main,在主协程结束之前的一小段时间,我们的协程虽然抓紧时间,但只打印了 B.defer,没来得及打印A.defer。

第三个结果解释:主协程打印了main,这次我们的协程虽然紧赶慢赶,但没能赶上执行 defer 语句,一切都结束了。

为了充分说明 如果我们用 runtime.Goexit() 结束协程,仍然会执行 defer 语句 ,我们可以让主协程延迟结束:

package mainimport ("fmt""runtime""time"
)func main() {go func() {defer fmt.Println("A.defer")func() {defer fmt.Println("B.defer")runtime.Goexit() // 结束当前协程defer fmt.Println("C.defer")fmt.Println("B")}()fmt.Println("A")}()time.Sleep(time.Second) //睡一会儿,不让主协程很快结束
}

输出结果:

B.defer
A.defer

四、runtime.GOMAXPROCS()

Golang 默认所有任务都运行在一个 cpu 核里,如果要在 goroutine 中使用多核,可以使用 runtime.GOMAXPROCS 函数修改,当参数小于 1 时使用默认值。

Go运行时的调度器使用 GOMAXPROCS 参数来指定需要使用多少个 OS 线程来同时执行 Go 代码。默认值是机器上的 CPU 核心数。例如在一个 8 核心的机器上,调度器会把 Go 代码同时调度到 8 个 OS 线程上( GOMAXPROCS 是m:n调度中的n)。

Go语言中可以通过 runtime.GOMAXPROCS() 函数设置当前程序并发时占用的 CPU 逻辑核心数。

Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的 CPU 逻辑核心数。

我们可以通过将任务分配到不同的 CPU 逻辑核心上实现并行的效果,这里举个例子:

package mainimport ("fmt""runtime""time"
)func a() {for i := 1; i < 10; i++ {fmt.Println("A:", i)}
}func b() {for i := 1; i < 10; i++ {fmt.Println("B:", i)}
}func main() {runtime.GOMAXPROCS(1)go a()go b()time.Sleep(time.Second)  //睡一会儿,不让主协程结束
}

上例中,两个任务只有一个逻辑核心,此时是做完一个任务再做另一个任务。 将逻辑核心数设为2,此时两个任务并行执行,代码如下:

package mainimport ("fmt""runtime""time"
)func a() {for i := 1; i < 10; i++ {fmt.Println("A:", i)}
}func b() {for i := 1; i < 10; i++ {fmt.Println("B:", i)}
}func main() {runtime.GOMAXPROCS(2)go a()go b()time.Sleep(time.Second)
}

Go语言中的操作系统线程和 goroutine 的关系:

  1. 一个操作系统线程对应用户态多个 goroutine。
  2. go 程序可以同时使用多个操作系统线程。
  3. goroutine 和 OS 线程是多对多的关系,即 m:n。

五、runtime.NumCPU()、runtime.GOROOT()、runtime.GOOS

package mainimport ("fmt""runtime"
)func main() {//获取cpu核数量fmt.Println("cpus:", runtime.NumCPU())//获取goroot目录:fmt.Println("goroot:", runtime.GOROOT())//获取操作系统fmt.Println("archive:", runtime.GOOS)
}

输出结果:

cpus: 4
goroot: D:\Go
archive: windows

参考链接

  1. runtime包
  2. 本文介绍了几个 runtime 中最基本的函数,要想了解更多,请参考文章:go-runtime
http://www.lryc.cn/news/2420190.html

相关文章:

  • UML建模工具安装详细教程(StarUML 5.0.2)
  • 个人MSSQL总结及收集
  • idea打包生成generated文件,强迫症就很烦
  • MySQL索引基础到原理讲解,一篇文章就够了!
  • solr快速上手:solr简介、安装并设置开机自启(一)
  • 22款超好用的CLI工具
  • #MATLAB绘图--------气泡图绘制#
  • NVL函数,NVL2函数的使用,查询日期天数
  • 有趣的HTML实例(二) 404页面
  • Jetty9容器安装与使用
  • 小白必看的Ubuntu20.04安装教程(图文讲解)
  • CentOS安装桌面环境GNOME
  • 图形图像基础 之 jpeg介绍
  • TRUNC() 函数的使用
  • 如何查看电脑配置信息
  • reactor模式学习
  • springboot配置Hikari连接池
  • DECODE()函数
  • Cassandra数据模型与数据分区
  • NIST数字测试套件使用说明
  • c语言中instr函数,InStr 函数
  • C++:strcpy、strncpy、strcpy_s、strncpy_s区别
  • Iceberg写入过程
  • Java多线程(吐血超详细整理)
  • AOP与OOP有什么区别
  • Java发送Http请求(HttpClient)
  • sm2和sm4加密算法浅析
  • 【C语言】动态内存管理 - malloc等函数详解
  • Loki 学习总结(1)—— Loki 中小项目日志系统的不二之选
  • 事物(Transaction)