目录
Runtime 包概述
runtime 包提供了与 Go 运行时环境交互的功能,包括 goroutine 调度、内存管理、堆栈操作等。通过 runtime 包,我们可以更好地控制程序的运行行为。
Runtime 包常用函数
1. GOMAXPROCS
设置可以并行执行的最大 CPU 数,并返回之前的设置值。
fmt.Println(runtime.GOMAXPROCS(2))
2. Caller 和 Callers
runtime.Caller
和 runtime.Callers
用于获取调用栈信息。
// 返回调用栈的信息(文件名、行号等)。
// 参数:skip表示从调用Caller开始往回数多少层的调用栈信息。
// skip = 0: 返回调用runtime.Caller的函数(即直接包含runtime.Caller(0)调用的那行代码)的文件名、行号等信息。
// skip = 1: 返回调用runtime.Caller所在函数的直接调用者的文件名和行号。换句话说,就是调用了包含runtime.Caller(1)的函数的地方的调用栈信息。
// skip = 2: 返回上一步调用者的调用者信息,依此类推。每增加1,就向上追溯一个调用栈帧。
// 以此类推,随着skip值的增加,可以逐级向上追溯到更早的调用栈信息。如果指定的层级超出了实际存在的调用栈层数,则ok将为false,其他返回值(pc, file, line)将没有意义或为零值。
pc, file, line, ok := runtime.Caller(1)
if !ok {
fmt.Println("runtime.Caller() failed")
return
}
fmt.Printf("PC: %v, File: %s, Line: %d\n", pc, file, line)
3. BlockProfile 和 Stack
runtime.BlockProfile()
用于获取阻塞概述,而 runtime.Stack()
则可以打印当前 goroutine 或所有 goroutine 的堆栈跟踪信息。
理解Golang的Goroutine
Goroutine是Golang语言中的轻量级线程,能够在单一操作系统线程上运行多个Goroutine,从而提高并发编程的效率。本文将通过实际的代码示例,详细讲解Goroutine的创建、调度、性能优化以及在实际应用中的使用场景。
Goroutine的基本概念
Goroutine 是Golang语言中用于并发编程的基本单位。与传统的线程(Thread)相比,Goroutine的调度和切换成本更低,因为它们是基于Golang的协作式调度模型设计的。每个Goroutine的栈大小默认为2KB,但随着其生命周期的变化,栈大小会自动扩大以适应需求。
特点:
- 轻量级:创建和销毁Goroutine的开销非常低。
- 高并发:Golang可以轻松支持数万甚至更多的Goroutine同时运行。
- ** Channels**:Goroutine之间通过Channel进行通信,避免了共享内存带来的竞态条件问题。
Goroutine的创建与启动
Goroutine的创建非常简单,只需要在函数调用前加上go
关键字即可启动一个新的Goroutine。
示例代码
package main
import (
"fmt"
"runtime"
)
func print() {
fmt.Println("这是一个新的Goroutine。")
}
func main() {
go print() // 启动一个新的Goroutine
fmt.Println("主Goroutine结束。") // 主Goroutine继续执行
}
解释
go print()
:启动一个新的Goroutine执行print
函数。- 主Goroutine继续执行
fmt.Println("主Goroutine结束。")
。 - 需要注意的是,如果主Goroutine执行完毕,程序可能会直接退出,可能无法看到子Goroutine的输出。为了确保子Goroutine有足够的时间完成,可以通过
time.Sleep()
或者其他同步机制来控制。
Goroutine的调度
Goroutine的调度是由Golang的运行时环境负责的。与线程不同,Goroutine采用的是非抢占式调度,即只有在当前Goroutine主动让出CPU(例如调用runtime.Gosched()
)时,其他Goroutine才能获取执行机会。
Gosched的作用
runtime.Gosched()
用于让出当前Goroutine的执行权限,允许其他Goroutine运行。
示例代码
package main
import (
"fmt"
"runtime"
)
func main() {
go func() {
for i := 1; i <= 5; i++ {
runtime.Gosched()
fmt.Println(i)
}
}()
fmt.Println("主Goroutine结束。")
}
输出
主Goroutine结束。
1
2
3
4
5
解释
- 子Goroutine在循环中调用
runtime.Gosched()
,主动让出CPU时间片。 - 主Goroutine在子Goroutine开始后立即执行并输出“主Goroutine结束。”。
Goroutine的性能优化
Goroutine的性能优化主要体现在以下几个方面:
-
设置GOMAXPROCS:通过
runtime.GOMAXPROCS(n)
设置可以并行执行的最大CPU核数。合理设置这个值可以提高程序的并行度。- 示例:
runtime.GOMAXPROCS(2)
,表示最多同时使用2个CPU核心。
- 示例:
-
避免竞态条件:多个Goroutine访问共享变量时,可能会出现竞态条件。可以通过Channel或sync包中的同步原语(如Mutex、RWMutex)来避免。
-
合理分配任务:在高并发场景下,合理分配任务可以提高程序的执行效率。
示例代码
package main
import (
"fmt"
"math"
"runtime"
"sync"
"time"
)
func findPrime(num int, inChan chan int, primeChan chan int, exitChan chan bool) {
var flag bool
for {
v, ok := <-inChan
if !ok {
break
}
flag = true
for i := 2; i < int(math.Sqrt(float64(v))); i++ {
if v % i == 0 {
flag = false
break
}
}
if flag {
primeChan <- v
}
}
exitChan <- true
}
func main() {
num := 10000000 // 查找小于num的所有质数
start := time.Now()
inChan := make(chan int, num)
primeChan := make(chan int, num)
exitChan := make(chan bool, 4) // 假设使用4个Goroutine
go func() {
for i := 2; i < num; i++ {
inChan <- i
}
close(inChan)
}()
for i := 0; i < 4; i++ {
go findPrime(num, inChan, primeChan, exitChan)
}
go func() {
for i := 0; i < 4; i++ {
<-exitChan
}
close(primeChan)
}()
count := 0
for v := range primeChan {
count++
}
fmt.Printf("找到%d个质数,用时:%v\n", count, time.Since(start))
}
解释
- Channel通信:通过Channel传递数据,避免了共享变量带来的竞态条件。
- 并行计算:通过启动多个Goroutine并行计算质数,提高了程序的执行效率。
- 同步机制:通过
exitChan
等待所有Goroutine完成任务。
实际应用中的使用场景
- 并发请求处理:在Web服务器中,通过Goroutine并行处理多个HTTP请求,提高吞吐量。
- 数据处理:在数据处理任务中,通过Goroutine并行处理不同的数据片段,提高处理速度。
- I/O密集型任务:在I/O密集型任务(如文件读写、网络通信)中,Goroutine可以通过非阻塞的方式提高资源利用率。
总结
Goroutine是Golang语言中的并行编程核心,具有轻量级、高效和灵活的特点。通过合理利用Goroutine,可以显著提高程序的性能和响应速度。在实际应用中,需要注意避免竞态条件,合理分配任务,并通过Channel等方式实现Goroutine之间的安全通信。
Runtime与Routine的关系
在Go语言中,runtime
和routine
是两个密切相关但又有明确区别的概念。runtime
主要指的是Go语言的运行时环境,它为Go程序的执行提供了必要的支持,如内存管理、Goroutine调度、错误处理等。而routine
在Go语言中特指Goroutine,即轻量级的线程。
具体来说:
-
Runtime
- 定义:
runtime
是一个Go语言标准库中的包,提供了与程序运行时环境相关的功能。 - 功能:
- Goroutine调度:管理Goroutine的创建、执行和调度。
- 内存管理:负责内存的分配和回收,包括垃圾回收机制。
- 错误处理:提供了一些与错误处理相关的函数,如
runtime.Error
。 - 与操作系统交互:处理程序运行时的环境,如线程数、CPU核数等。
- 性能监控:提供了一些与性能监控相关的函数,如内存使用统计。
- 常用函数:
Gosched()
:让出当前Goroutine的执行权限,允许其他Goroutine运行。GOMAXPROCS()
:设置或获取可以并行执行用户级处理的最大CPU核数。NumCPU()
:返回当前可用的CPU核数。Goexit()
:使当前Goroutine退出,然后调度器会打印当前的栈跟踪信息。
- 定义:
-
Routine(Goroutine)
- 定义:Goroutine是Go语言中的轻量级线性,它由Go的运行时环境管理。
- 特点:
- 轻量级:创建和销毁Goroutine的开销比传统线程低。
- 高并发:可以轻松支持数万甚至更多的Goroutine同时运行。
- 非阻塞式调度:基于协作式调度,主动让出CPU时间片。
- 创建方式:通过
go
关键字启动一个新的Goroutine。 - 常见用途:
- 并发执行任务:将一个函数或方法转换为异步执行,不阻塞主Goroutine。
- 处理I/O密集型任务:在I/O操作中,释放资源让其他Goroutine使用。
- 并行计算:在多核CPU上并行执行任务以提高计算效率。
-
两者的关系
- 调度管理:
runtime
包负责管理Goroutine的调度,包括如何分配时间片、如何在多个CPU核间分配Goroutine等。 - 资源管理:通过
runtime
包,可以控制Goroutine的数量和并行度,优化程序性能。 - 协作式调度:Goroutine通过
runtime.Gosched()
主动让出CPU时间片,协作式调度依赖于Goroutine自身的配合,而不是操作系统强制切换。
- 调度管理:
补充(代码)
package main
import (
"fmt"
"math"
"runtime"
"time"
)
func PrintCallerInfo() {
//返回调用栈的信息(文件名、行号等)。
//参数:skip表示从调用Caller开始往回数多少层的调用栈信息。
// skip = 0: 返回调用runtime.Caller的函数(即直接包含runtime.Caller(0)调用的那行代码)的文件名、行号等信息。
// skip = 1: 返回调用runtime.Caller所在函数的直接调用者的文件名和行号。换句话说,就是调用了包含runtime.Caller(1)的函数的地方的调用栈信息。
// skip = 2: 返回上一步调用者的调用者信息,依此类推。每增加1,就向上追溯一个调用栈帧。
// 以此类推,随着skip值的增加,可以逐级向上追溯到更早的调用栈信息。如果指定的层级超出了实际存在的调用栈层数,则ok将为false,其他返回值(pc, file, line)将没有意义或为零值。
pc, file, line, ok := runtime.Caller(1)
if !ok {
fmt.Println("runtime.Caller() failed")
return
}
fmt.Printf("PC: %v, File: %s, Line: %d\n", pc, file, line)
}
func main() {
// 设置可以并行执行的最大CPU数,并返回之前的设置值;控制了Go程序可以同时使用的操作系统线程数量。
fmt.Println(runtime.GOMAXPROCS(2))
// 启动一个协程
go func(x int) {
//time.Sleep(1 * time.Second)
fmt.Println("Goroutine", x)
}(1)
// 终止
go func(x int) {
defer fmt.Println("Goroutine", x)
//time.Sleep(1 * time.Second)
runtime.Goexit() // 当前goroutine 退出
fmt.Println("This won't print")
}(2)
// 当前程序中活跃的gotoutines数量
fmt.Println("NumGoroutine:", runtime.NumGoroutine())
// 让出当前goroutine的处理器,允许其他等待的goroutines运行;
// 这是一个提示,而不是强制性的上下文呢切换
go func() {
for i := 1; i <= 5; i++ {
runtime.Gosched()
fmt.Println(i)
}
}()
go func(x int) {
//time.Sleep(1 * time.Second)
fmt.Println("Goroutine", x)
}(3)
go func(x int) {
//time.Sleep(1 * time.Second)
fmt.Println("Goroutine", x)
}(4)
// 其它
//runtime.BlockProfile() //获取当前的阻塞概要信息(block profile),它用于分析goroutine之间的同步问题,如锁竞争、通道阻塞等。
//runtime.Caller() //返回调用栈的信息(文件名、行号等)。
//runtime.Callers() //类似Caller,但返回的是多个调用栈帧的信息。
//runtime.GC() //:手动触发垃圾回收。通常不需要手动调用,Go的垃圾收集器会自动管理内存。
fmt.Println(runtime.GOROOT()) // 返回Go的安装目录
//runtime.KeepAlive(x any) //确保对象在其作用域结束前不会被垃圾回收。
//runtime.Stack() //将当前goroutine或所有goroutines的堆栈跟踪写入提供的缓冲区。
// runtime.Caller
//PrintCallerInfo()
//time.Sleep(3 * time.Second) // 确保协程有足够的时间完成
//高并发例子
FindPrime()
//单线程
noGoroutine()
}
func FindPrime() {
var num int
var n int
n, _ = fmt.Scanf("%d", &num)
fmt.Println(n, num)
n = num
if num > 32 { // 当num很大时,增大n(即goroutine的数量)却没有带来性能提升甚至导致耗时
n = 32
}
// 对于num=1000000
// n = 1, 即noGoroutine(),大约200ms~270ms
// n = 2,大约150ms~250ms
// n = 4,大约170ms~270ms
// n = 8,大约>220ms
// 对于num=10000000
// n = 1, 即noGoroutine(),大约5s~6s
// n = 2,大约2.7s~2.9s
// n = 4,大约1.6s~2.0s
// n = 8,大约1.8s~2s
// n = 16(开始耗时增加), 大约2.1s~2.2s
// 对于num=100000000
// n = 1, 即noGoroutine(),>100s
// n = 4,大约40s~41s
// n = 8,大约25s~26s
// n = 16, 大约22s~24s
// n = 32,大约·32s~33s
var primeChan = make(chan int, num)
var inChan = make(chan int, num)
var exitChan = make(chan bool, n)
var start = time.Now()
go inPrime(num, inChan)
for i := 0; i < n; i++ {
go primeJudge(inChan, primeChan, exitChan)
}
go func() {
for i := 0; i < n; i++ {
<-exitChan
}
close(primeChan)
}()
for {
_, ok := <-primeChan
if !ok {
break
}
//fmt.Println(res)
}
close(exitChan)
fmt.Println(time.Since(start))
}
func inPrime(num int, inChan chan int) {
for i := 2; i < num; i++ {
inChan <- i
}
close(inChan)
}
func primeJudge(inChan chan int, primeChan chan int, exitChan chan bool) {
var flag bool
for {
v, ok := <-inChan
//fmt.Println("v = ", v, "; ok = ", ok)
if !ok {
break
}
flag = true
for i := 2; i < int(math.Sqrt(float64(v))); i++ {
if v%i == 0 {
flag = false
break
}
}
if flag {
primeChan <- v
}
}
exitChan <- true
}
func noGoroutine() {
var num int
n, _ := fmt.Scanf("%d", &num)
fmt.Println(n, num)
var count = 0
var start = time.Now()
for i := 2; i <= num; i++ {
var flag = true
for j := 2; j < (int)(math.Sqrt(float64(i))); j++ {
if i%j == 0 {
flag = false
break
}
if flag {
count++
}
}
}
fmt.Println(time.Since(start))
}