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

导读:本篇文章讲解 【Go】并发编程之runtime包及其常用方法,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com


一、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

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/119001.html

(0)
seven_的头像seven_bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!