一、前言:Go 语言的核心优势 —— 原生并发

Go 被称为21 世纪的 C 语言,两大核心亮点:

  1. 语法设计简洁、上手门槛低
  2. 语言层面原生支持并发,内置 GC 自动垃圾回收,无需手动管理内存

Go 并发设计哲学

不同于传统多线程靠实现同步,Go 采用通信的方式解决并发竞争:

不要通过共享内存来通信,而要通过通信来共享内存

二、基础概念:并发 vs 并行

1. 并行(Parallelism)

同一时刻,多条指令在不同 CPU 核心同时执行,必须依赖多核硬件。

2. 并发(Concurrency)

同一时刻只有一条指令在 CPU 执行,系统通过时间片轮转快速切换多个任务;宏观看起来多个任务同时运行,微观是交替执行,单核 CPU 即可实现。

举例:单核 CPU 处理 3 个 1s 任务,靠时间片切换实现并发,在有限硬件资源下提升整体处理效率。

核心区分总结

  • 并发:同时处理多件事(单核、任务切换)
  • 并行:同时做多件事(多核、真正同时执行)

三、Goroutine(协程):Go 并发核心

1. 什么是 Goroutine

Goroutine 是 Go 内置的轻量级协程,可简单理解为轻量任务,是 Go 并行调度的核心:

  • 比操作系统线程体积更小、开销极低;
  • 底层采用 M:N 调度:成千上万个 goroutine 会复用少量 OS 线程;
  • Go 运行时自动管理 goroutine 内存共享,调度成本远低于线程;
  • 优势:易用、高效、资源占用极少。

2. 创建 Goroutine

语法:在函数 / 匿名函数前加 go 关键字,即可新建子协程。

go

// 普通函数开启协程
go newTask()

// 匿名函数协程(必须加()执行)
go func() {
    // 协程业务逻辑
}()

3. 核心规则:主协程退出,所有子协程直接销毁

main 函数本身是主协程,一旦 main 执行完毕,整个进程直接终止,所有未执行完成的子协程会被强制杀死,不会等待。

示例 1:子协程来不及执行

go

package main
import "time"

func newTask() {
    for {
        fmt.Println("子协程运行中")
        time.Sleep(time.Second)
    }
}

func main() {
    // 开启子协程
    go newTask()
    // 主协程循环2次就退出
    i := 0
    for {
        i++
        fmt.Println("主协程 i = ", i)
        time.Sleep(time.Second)
        if i == 2 {
            break // main结束,子协程直接销毁,无任何输出
        }
    }
}
示例 2:死循环阻塞主协程(测试用)

写无限for{}阻塞主协程,防止程序提前退出,方便观察子协程输出:

go

func main() {
    go newTask()
    // 死循环阻塞主线程
    for {}
}

四、runtime 包:协程调度控制工具

1. runtime.Gosched ():主动让出 CPU 时间片

作用:当前 goroutine 主动放弃执行权,调度器切换其他就绪协程运行,等其他协程执行完毕后再切回当前协程。

示例:不加 Gosched 时,主协程会优先跑完,子协程无执行机会

go

package main
import (
    "fmt"
    "runtime"
)

func main() {
    go func() {
        for i := 0; i < 5; i++ {
            fmt.Println("子协程 go")
        }
    }()

    for i := 0; i < 2; i++ {
        fmt.Println("主协程 hello")
        runtime.Gosched() // 主动让出时间片,让子协程先执行
    }
}

2. runtime.Goexit ():终止当前协程

作用:立即结束当前所在 goroutine,不会影响其他协程;会优先执行当前协程所有 defer 延迟函数,普通return仅终止当前函数,不会退出协程。

go

package main
import (
    "fmt"
    "runtime"
)

func test() {
    defer fmt.Println("执行defer延迟语句")
    // runtime.Goexit() // 终止整个子协程
    return // 仅退出test函数,协程继续往下执行
    fmt.Println("该行永远不会打印")
}

func main() {
    go func() {
        fmt.Println("子协程开始")
        test()
        fmt.Println("子协程结束") // Goexit()时该行不会执行
    }()
    // 死循环阻塞主协程
    for {}
}

3. runtime.GOMAXPROCS (n):设置并行 CPU 核心上限

  • 功能:设置可并行执行 goroutine 的 OS 线程最大数量,返回修改前的旧值;
  • 规则:
    1. n=1:单核运行,无真正并行,仅时间片切换并发;
    2. n 等于 CPU 物理核数:最大化多核并行能力;
    3. Go1.5 + 默认值为机器 CPU 核心数;
  • 效果:核数越大,多协程交替输出越混乱,并行效率越高。

go

package main
import (
    "fmt"
    "runtime"
)

func main() {
    // 设置仅使用1核CPU,返回旧配置
    old := runtime.GOMAXPROCS(1)
    fmt.Println("修改前CPU并行数:", old)
    
    for {
        go fmt.Print(1)
        fmt.Print(0)
    }
}

五、多协程资源竞争问题

场景模拟:多个协程共用同一个打印机(公共资源)

多个 goroutine 同时操作同一共享资源,会出现输出错乱、数据混乱,即并发竞争,需要同步机制解决。

go

package main
import "fmt"

// 公共打印机资源
func Printer(str string) {
    for _, data := range str {
        fmt.Print(string(data))
    }
    fmt.Println()
}

func person1() {
    Printer("hello")
}

func person2() {
    Printer("world")
}

func main() {
    // 两个协程同时抢占打印机,输出错乱
    go person1()
    go person2()
    // 阻塞主协程观察效果
    for {}
}

解决方案预告

Go 官方推荐使用channel 通道实现 goroutine 同步与通信,解决资源竞争,下一章详细讲解。

六、全文总结(面试 / 复习速记)

  1. Go 核心优势:语言原生并发,goroutine 轻量高效,GC 自动回收;
  2. 并发:单核时间片切换;并行:多核同时执行;
  3. goroutine 创建:go + 函数,主协程退出则所有子协程销毁;
  4. runtime 三大工具:
    • Gosched():主动让出 CPU;
    • Goexit():终止当前协程,执行 defer;
    • GOMAXPROCS():控制并行 CPU 核心数;
  5. 多协程访问公共资源会产生竞争,使用 channel 通信实现同步。
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐