文章目录
一、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 包几个方法:
-
Gosched():让当前线程让出 cpu 以让其它线程运行,它不会挂起当前线程,因此当前线程未来会继续执行。
-
NumCPU():返回当前系统的 CPU 核数量。
-
GOMAXPROCS():设置最大的可同时使用的 CPU 核数。
通过runtime.GOMAXPROCS函数,应用程序可以设置运行时系统中的 P 最大数量。注意,如果在运行期间设置该值的话,会引起“Stop the World”。所以,应在应用程序最早期调用,并且最好是在运行Go程序之前设置好操作程序的环境变量GOMAXPROCS,而不是在程序中调用runtime.GOMAXPROCS函数。
无论我们传递给函数的整数值是什么值,运行时系统的P最大值总会在1~256之间。
go1.8 后,默认让程序运行在多个核上,可以不用设置了。
go1.8 前,还是要设置一下,可以更高效的利用 cpu。 -
Goexit():退出当前 goroutine(但是defer语句会照常执行)。
-
NumGoroutine:返回正在执行和排队的任务总数。
runtime.NumGoroutine函数在被调用后,会返回系统中的处于特定状态的 Goroutine 的数量。这里的特定状态是指Grunnable\Gruning\Gsyscall\Gwaition。处于这些状态的Groutine即被看做是活跃的或者说正在被调度。
注意:垃圾回收所在Groutine的状态也处于这个范围内的话,也会被纳入该计数器。 -
GOOS:查看目标操作系统。很多时候,我们会根据平台的不同实现不同的操作,就可以用GOOS来查看自己所在的操作系统。
-
runtime.GC:会让运行时系统进行一次强制性的垃圾收集。
强制的垃圾回收:不管怎样,都要进行的垃圾回收。非强制的垃圾回收:只会在一定条件下进行的垃圾回收(即运行时,系统自上次垃圾回收之后新申请的堆内存的单元(也成为单元增量)达到指定的数值)。 -
GOROOT() :获取 goroot 目录。
-
runtime.LockOSThread 和 runtime.UnlockOSThread 函数:前者调用会使调用他的 Goroutine 与当前运行它的M锁定到一起,后者调用会解除这样的锁定。
二、runtime.Gosched()
让出当前协程的 CPU 时间片给其他协程。当前协程等待时间片未来继续执行。
释放时间片,先让别的协程执行,它执行完,再回来执行此协程。
package main
import (
"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 main
import (
"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 main
import (
"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 main
import (
"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 main
import (
"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 的关系:
- 一个操作系统线程对应用户态多个 goroutine。
- go 程序可以同时使用多个操作系统线程。
- goroutine 和 OS 线程是多对多的关系,即 m:n。
五、runtime.NumCPU()、runtime.GOROOT()、runtime.GOOS
package main
import (
"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
参考链接
- runtime包
- 本文介绍了几个 runtime 中最基本的函数,要想了解更多,请参考文章:go-runtime
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/119001.html