
go语言通过其轻量级的并发原语goroutine,实现了高效的并发编程。goroutine并非直接映射到操作系统线程,而是由go运行时(runtime)调度器进行管理,将大量的goroutine多路复用(multiplexing)到少量底层操作系统(os)线程上。这种m:n的调度模型使得go程序能够以极低的开销创建数以万计的并发任务。然而,理解goroutine如何与os线程交互,以及何时会创建新的os线程,对于编写高性能、高并发的go应用至关重要。
GOMAXPROCS是一个环境变量或通过runtime.GOMAXPROCS()函数设置的参数,它决定了Go程序同时可以并行执行Go代码的OS线程的最大数量。更准确地说,它控制了Go调度器可以同时使用的P(Processor,逻辑处理器)的数量。每个P绑定一个M(Machine,OS线程),而M负责执行G(Goroutine)。
例如,如果GOMAXPROCS设置为1,即使系统有多个CPU核心,Go调度器也只会在一个OS线程上运行Go代码。这意味着,如果一个Goroutine正在执行CPU密集型任务,其他Go代码(包括其他Goroutine)将不得不等待该线程空闲。增加GOMAXPROCS的值可以提高Go程序的并行度,使其能够充分利用多核CPU资源。通常,GOMAXPROCS的默认值等于机器的CPU核心数,这在大多数情况下是最佳实践。
需要强调的是,GOMAXPROCS仅限制了Go调度器用于执行Go代码的线程数量,它并不限制Go程序可以创建的OS线程总数。Go程序在特定情况下,即使GOMAXPROCS设置为1,也可能创建超出此限制的OS线程。
尽管Go调度器能够高效地管理Goroutine,但在某些特定情况下,当一个Goroutine执行阻塞操作时,它会阻塞其所绑定的OS线程。为了不影响其他可运行的Goroutine的执行,Go运行时会采取措施,包括但不限于创建新的OS线程或从线程池中获取空闲线程,以确保GOMAXPROCS所设定的并行度得以维持。这些导致额外OS线程创建的主要场景是:
示例:阻塞系统调用导致的线程增加
以下代码演示了当多个Goroutine同时执行阻塞文件读取(系统调用)时,即使GOMAXPROCS设置为1,也可能观察到OS线程数量的增加:
package main
import (
"fmt"
"io/ioutil"
"os"
"runtime"
"sync"
"time"
)
func main() {
// 将GOMAXPROCS设置为1,以凸显系统调用对线程数的影响
runtime.GOMAXPROCS(1)
fmt.Printf("GOMAXPROCS 已设置为: %d\n", runtime.GOMAXPROCS(-1))
var wg sync.WaitGroup
numGoroutines := 10 // 创建10个Goroutine
fmt.Println("启动执行阻塞文件读取的Goroutine...")
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fileName := fmt.Sprintf("temp_file_%d.txt", id)
// 创建一个临时文件供读取
err := ioutil.WriteFile(fileName, []byte(fmt.Sprintf("Hello from goroutine %d", id)), 0644)
if err != nil {
fmt.Printf("Goroutine %d: 写入文件错误: %v\n", id, err)
return
}
defer os.Remove(fileName) // 确保文件被清理
fmt.Printf("Goroutine %d: 尝试读取文件 %s\n", id, fileName)
// ioutil.ReadFile 是一个阻塞的系统调用
_, err = ioutil.ReadFile(fileName)
if err != nil {
fmt.Printf("Goroutine %d: 读取文件错误: %v\n", id, err)
} else {
fmt.Printf("Goroutine %d: 完成读取文件 %s\n", id, fileName)
}
// 稍作延迟,给其他Goroutine执行的机会
time.Sleep(100 * time.Millisecond)
}(i)
}
// 给予Goroutine启动并可能创建线程的时间
time.Sleep(2 * time.Second)
fmt.Println("--------------------------------------------------")
fmt.Println("请在此处使用 'ps -efL | grep <你的进程名>' 或 'htop -t' 观察OS线程数量。")
fmt.Println("--------------------------------------------------")
wg.Wait()
fmt.Println("所有Goroutine执行完毕。")
}运行上述代码,并在程序输出提示时,打开另一个终端窗口执行 ps -efL | grep your_go_program_name (Linux/macOS) 或使用 htop -t,你将观察到即使GOMAXPROCS设置为1,Go进程的OS线程数也可能远超1个,因为多个Goroutine同时阻塞在ioutil.ReadFile这个系统调用上。
并非所有阻塞操作都会导致Go运行时创建新的OS线程。Go运行时对一些常见的阻塞原语进行了特殊优化,这些操作在Goroutine阻塞时不会阻塞其底层的OS线程,而是由Go调度器进行异步处理:
示例:Go运行时管理的阻塞操作
原始问题中提供的Vector.DoSome函数是一个很好的例子:
type Vector []float64
// Apply the operation to n elements of v starting at i.
func (v Vector) DoSome(i, n int, u Vector, c chan int) {
for ; i < n; i++ {
v[i] += u.Op(v[i]) // CPU密集型计算
}
c <- 1; // signal that this piece is done (通道操作)
}在这个函数中,v[i] += u.Op(v[i]) 是一个CPU密集型操作。当多个Goroutine执行此操作时,GOMAXPROCS将直接决定并行执行的Goroutine数量。而c <- 1是一个通道发送操作。如果通道是无缓冲的且没有接收者,或者是有缓冲通道已满,发送操作会阻塞当前Goroutine。但此时,Go调度器会将该Goroutine挂起,并立即将底层的OS线程释放,用于执行其他Goroutine,而不会创建新的OS线程。
在设计高并发Go应用时,理解这些机制至关重要。尽量利用Go运行时优化的并发原语,避免在Goroutine中直接执行大量阻塞的系统调用,尤其是在GOMAXPROCS设置较低的情况下,以防止创建过多OS线程,从而增加上下文切换开销,影响程序性能。如果必须进行阻塞系统调用,应合理设计并发模型,例如使用有限的Goroutine池来执行这些操作,以控制OS线程的数量。
以上就是Go Goroutines与操作系统线程:深度解析GOMAXPROCS与线程创建机制的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号