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
在设计高并发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号