Golang中线程与核心的深入解析

Golang中线程与核心的深入解析 不同的运行线程是否会在不同的处理器核心中执行?例如,处理器有四个核心,我们启动了三个线程。这是否意味着每个线程都会在各自独立的处理器核心上运行,而最后一个核心不会被使用?

另一方面,如果线程数量超过核心数量会发生什么?是一个线程对应一个核心,其他线程会分配到所有核心上?还是所有线程都会分配到所有可用核心上?

4 回复

这是一篇关于 Go 调度器以及 goroutine 如何与线程和核心相关的有趣读物:https://morsmachine.dk/go-scheduler

更多关于Golang中线程与核心的深入解析的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


不。Go 在编译 Go 协程时使用了并发机制。你可以查看这篇文章获取更多信息(https://medium.com/rungo/achieving-concurrency-in-go-3f84cbf870ca)。

处理器核心、线程和协程彼此之间是完全解耦的。

Go语言默认会尝试为每个核心至少运行一个线程。你最终可能会拥有比核心数更多的线程:例如当你读取文件时,会有一个被阻塞的读取线程。这个线程不会占用核心资源:操作系统会将其"挂起",并释放该核心以执行其他任务。

线程在哪个核心上运行是由操作系统调度的:Go语言的策略是在可用线程上尽可能多地运行协程,而不是创建新线程(这是一个相对昂贵的操作)。

通常情况下,你不需要关心核心或线程的问题。

但当你的代码执行CPU密集型操作时(即不涉及I/O或与外部交互的情况),你就需要关注了:此时可以修改Go进程的"任务亲和性"(在Linux中使用taskset命令)来将线程固定在特定CPU上。可能你还需要降低进程的友好值。这两种操作可能会加速你的程序,但代价是同时运行的其他程序会变慢。

在Go语言中,线程(通常指Goroutine)与处理器核心的调度是由Go运行时(runtime)管理的,而不是直接由操作系统线程一对一绑定到核心。Go使用M:N调度模型,其中M个Goroutine映射到N个操作系统线程,这些线程由Go调度器分配到可用的处理器核心上。下面我将详细解释你的问题,并提供示例代码说明。

问题1:不同的运行线程是否会在不同的处理器核心中执行?

在Go中,当你启动多个Goroutine时,Go运行时会将它们分配到可用的操作系统线程上,这些线程可能在不同的处理器核心上并行执行。但这不是严格的一对一映射;调度器会根据系统负载和核心可用性动态分配。

例如,如果你的处理器有四个核心,你启动了三个Goroutine,它们可能被分配到三个不同的核心上运行,而第四个核心可能空闲或用于其他系统任务。但这不是绝对的:如果核心负载不均衡,调度器可能将多个Goroutine分配到同一个核心上。

示例代码:启动三个Goroutine并观察它们可能在不同核心上运行(注意:实际核心分配由运行时决定,不可直接控制)。

package main

import (
    "fmt"
    "runtime"
    "sync"
    "time"
)

func main() {
    // 设置使用所有可用核心(默认行为,但可显式设置)
    runtime.GOMAXPROCS(runtime.NumCPU()) // 例如,如果有4个核心,则使用4个

    var wg sync.WaitGroup
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            fmt.Printf("Goroutine %d started, potentially on core %d\n", id, getCPUID())
            time.Sleep(1 * time.Second) // 模拟工作负载
        }(i)
    }
    wg.Wait()
}

// 注意:Go不提供直接获取当前核心ID的API,这里仅模拟;实际中核心分配是透明的。
// 此函数仅为示例,返回一个伪随机数模拟核心ID。
func getCPUID() int {
    return time.Now().Nanosecond() % runtime.NumCPU()
}

输出示例(实际输出可能因运行环境而异):

Goroutine 0 started, potentially on core 2
Goroutine 1 started, potentially on core 1
Goroutine 2 started, potentially on core 0

在这个例子中,三个Goroutine可能被分配到不同的核心,但最后一个核心(例如核心3)可能未被使用,因为只有三个活跃Goroutine。

问题2:如果线程数量超过核心数量会发生什么?

当Goroutine数量超过处理器核心数量时,Go调度器会使用时间分片(time-slicing)和上下文切换(context switching)来管理它们。多个Goroutine会共享可用的核心,而不是每个核心固定一个Goroutine。调度器会尽可能在所有可用核心上均衡分配Goroutine,以实现并行和并发执行。

具体行为:

  • 如果Goroutine数量大于核心数(例如,8个Goroutine在4个核心上),调度器会将Goroutine分配到所有核心上,并通过快速切换模拟并行。
  • 这不是一对一分配:所有Goroutine都有机会在所有核心上运行,但同一时间每个核心最多运行一个Goroutine(假设没有阻塞操作)。
  • 如果Goroutine阻塞(如I/O操作),调度器可能将其他Goroutine移动到空闲核心上,以最大化利用率。

示例代码:启动8个Goroutine在4个核心系统上运行。

package main

import (
    "fmt"
    "runtime"
    "sync"
    "time"
)

func main() {
    runtime.GOMAXPROCS(4) // 假设系统有4个核心,显式设置最大并行度

    var wg sync.WaitGroup
    numGoroutines := 8
    for i := 0; i < numGoroutines; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            fmt.Printf("Goroutine %d is running, may share cores\n", id)
            // 模拟CPU密集型工作,使调度行为更明显
            sum := 0
            for j := 0; j < 1000000; j++ {
                sum += j
            }
        }(i)
    }
    wg.Wait()
    fmt.Println("All goroutines completed.")
}

输出示例(顺序可能随机):

Goroutine 0 is running, may share cores
Goroutine 3 is running, may share cores
Goroutine 1 is running, may share cores
Goroutine 7 is running, may share cores
Goroutine 2 is running, may share cores
Goroutine 5 is running, may share cores
Goroutine 4 is running, may share cores
Goroutine 6 is running, may share cores
All goroutines completed.

在这个例子中,8个Goroutine在4个核心上运行,调度器会在核心间切换它们,而不是固定分配。所有可用核心都会被使用,但Goroutine会共享核心资源。

总结:Go的调度器设计用于高效利用多核处理器,核心分配是动态的,不保证一对一映射。Goroutine数量超过核心时,调度器通过共享核心实现并发,而不是让额外线程闲置。

回到顶部