go面试题: 并发与并行的区别

18 阅读4分钟

要理解Go语言中并发(Concurrency)与并行(Parallelism)的区别,需要从基本概念Go中的实现方式核心差异三个层面展开,结合Go的特性(如goroutineGOMAXPROCSGPM模型)说明:

一、基本概念区别

并发与并行是计算机科学中的核心概念,但本质不同:

维度并发(Concurrency)并行(Parallelism)
定义逻辑上的“同时”:同一时间段内交替执行多个任务(如单核CPU上的多任务调度),同一时间点只有一个任务在执行物理上的“同时”:同一时间点多个任务在多个处理器(多核CPU)上同时执行(如多核CPU上的多线程并行)。
核心任务的调度与切换(通过时间片轮转等策略),让用户“感觉”多个任务同时进行。任务的物理并行执行(依赖多核硬件),真正提升计算效率。
比喻一个厨师同时处理多个订单(切菜、炒菜、摆盘交替进行)。多个厨师同时处理多个订单(每个厨师负责一个订单,同时工作)。

二、Go中的实现方式区别

Go语言从语言层面支持并发(通过goroutine),并通过** runtime 调度**实现并行(依赖GOMAXPROCS和多核CPU):

1. 并发的实现:goroutine(轻量级线程)

  • 定义goroutine是Go语言中的轻量级执行单元(比操作系统线程更轻量,栈初始大小仅4KB,可动态扩展),由Go runtime而非操作系统内核调度。

  • 实现方式:使用go关键字启动一个goroutine,例如:

    func printHello() { 
        fmt.Println("Hello from goroutine") 
    } 
    
    func main() { 
        go printHello() // 启动一个goroutine执行printHello 
        fmt.Println("Hello from main") 
        time.Sleep(time.Second) // 等待goroutine执行完毕 
    } 
    
  • 调度逻辑:默认情况下,Go runtime使用1个逻辑处理器(Logical Processor),所有goroutine在该逻辑处理器上交替执行(并发)。即使启动多个goroutine,同一时间点只有一个goroutine在执行(单核CPU场景)。

2. 并行的实现:GOMAXPROCS与多核CPU

  • 依赖条件:要实现并行,需要满足两个条件:

    • 多个逻辑处理器:通过runtime.GOMAXPROCS(n)设置(n为逻辑处理器数量,默认等于CPU核数);
    • 多核CPU:物理硬件支持多个核心同时工作。
  • 实现方式:当GOMAXPROCS设置为大于1时,Go runtime会将goroutine分配到多个逻辑处理器上,每个逻辑处理器对应一个操作系统线程,从而实现物理并行。例如:

    import ( 
        "fmt"
        "runtime"
        "time"
    ) 
    
    func printNumbers(id int) { 
        for i := 1; i <= 5; i++ { 
            fmt.Printf("Goroutine %d: %d\n", id, i)
            time.Sleep(100 * time.Millisecond)
        } 
    } 
    
    func main() { 
        runtime.GOMAXPROCS(2) // 设置2个逻辑处理器 
        go printNumbers(1)    // 启动goroutine 1 
        go printNumbers(2)    // 启动goroutine 2 
        time.Sleep(1 * time.Second) // 等待所有goroutine执行完毕 
    } 
    
  • 执行结果:两个goroutine会在两个逻辑处理器上同时执行(多核CPU场景),输出结果为交替或同时打印(取决于CPU核数)。

三、核心差异总结

维度并发(Go)并行(Go)
执行方式单个逻辑处理器上交替执行多个goroutine(逻辑同时)。多个逻辑处理器上同时执行多个goroutine(物理同时)。
依赖条件不需要多核CPU(单核即可),由Go runtime调度。需要多核CPU + GOMAXPROCS设置(大于1)。
默认行为Go的默认行为(GOMAXPROCS默认等于CPU核数,但单个goroutine仍为并发)。需要显式设置GOMAXPROCS(如runtime.GOMAXPROCS(2))才能开启。
资源消耗goroutine轻量(栈初始4KB),可启动10万级goroutine而不消耗过多资源。依赖操作系统线程(栈初始1-2MB),线程数量过多会导致资源消耗增大(上下文切换开销)。
设计目标提高资源利用率(如I/O密集型任务,通过goroutine切换避免阻塞)。提高计算效率(如CPU密集型任务,通过多核并行加速)。

四、关键结论(来自Go作者Rob Pike的观点)

Go语言的并发模型遵循**“并发不是并行,但并发实现了并行”**的设计哲学:

  • 并发程序的设计结构(将任务分解为独立执行的片段),例如使用goroutine分解任务;
  • 并行程序的运行时特性(依赖硬件和调度),例如通过GOMAXPROCSgoroutine在多核上并行执行;
  • Go的GPM模型GoroutineProcessorMachine)实现了goroutine到线程的多路复用,既支持高效的并发(单个线程处理多个goroutine),也支持灵活的并行(多个线程处理多个goroutine)。

总结

  • 并发是Go语言的核心特性(通过goroutine实现,轻量、高效),用于处理多任务场景(如I/O密集型应用);
  • 并行是Go语言的扩展特性(通过GOMAXPROCS和多核CPU实现),用于提升计算密集型任务的效率;
  • 理解两者的区别,才能正确设计Go程序:I/O密集型任务用并发(goroutine),CPU密集型任务用并行(GOMAXPROCS+多核)