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


一、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 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 的关系:

  1. 一个操作系统线程对应用户态多个 goroutine。
  2. go 程序可以同时使用多个操作系统线程。
  3. 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

参考链接

  1. runtime包
  2. 本文介绍了几个 runtime 中最基本的函数,要想了解更多,请参考文章:go-runtime
  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值