GO语言基础:goroutine的概念、创建、调度、并发、协程、channel

10 阅读6分钟

Go 语言中的 Goroutine 详解

作为 Go 语言并发编程的核心,goroutine 是一种轻量级的“线程”,由 Go 运行时管理。它可以让你轻松地编写并发程序,而无需关心底层操作系统的线程调度。下面我们分几个方面详细讲解 goroutine 的概念、创建与调度,以及与并发、channel 的关系。


一、goroutine 的概念

1. 什么是 goroutine?

goroutine 是 Go 语言中实现并发的轻量级执行单元。你可以把它理解为一个独立执行的函数,但它比操作系统线程(OS thread)要小得多(初始栈大小仅 2KB,且可动态增长)。成千上万个 goroutine 可以同时运行在少量 OS 线程上,这使得 Go 程序能够轻松处理高并发任务。

2. goroutine 的特点

  • 轻量:创建成本低,栈空间小且可伸缩。
  • 并发执行:多个 goroutine 可以同时运行(如果是多核 CPU,可实现真正的并行)。
  • 非阻塞:goroutine 通过 channel 进行通信,而不是共享内存,避免了传统多线程编程中的锁竞争。
  • 由 Go 运行时调度:用户不需要直接干预线程的创建和销毁。

3. goroutine 与 OS 线程的对比

特性goroutineOS 线程
创建成本极低(几 KB 栈)高(通常 MB 级栈)
切换成本低(用户态调度)高(内核态调度)
调度器Go 运行时调度器(GMP 模型)操作系统调度器
数量可创建数十万甚至百万受系统资源限制,通常几千

二、goroutine 的创建与调度

1. 创建 goroutine

只需在函数调用前加上关键字 go,该函数就会在一个新的 goroutine 中并发执行。

package main

import (
    "fmt"
    "time"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    go say("world") // 启动一个新的 goroutine
    say("hello")    // 当前 goroutine 继续执行
}
  • go say("world") 启动了一个新的 goroutine 并发执行 say 函数。
  • 主函数 main 本身也运行在一个 goroutine 中(称为主 goroutine)。
  • 当主 goroutine 结束时,程序会退出,而不管其他 goroutine 是否执行完毕。因此通常需要同步机制(如 WaitGroup 或 channel)来等待其他 goroutine 完成。

2. goroutine 的调度模型:GMP

Go 运行时实现了一个用户态的调度器,采用 GMP 模型:

  • G(Goroutine):代表一个 goroutine,包含栈、程序计数器等信息。
  • M(Machine):代表 OS 线程(内核线程),是实际执行计算的资源。
  • P(Processor):代表调度的上下文,它维护一个本地 goroutine 队列。P 的数量由环境变量 GOMAXPROCS 控制(默认为 CPU 核心数)。

调度流程简述:

  • 每个 P 负责调度一组 goroutine 到与之绑定的 M 上执行。
  • 当 M 上运行的 goroutine 被阻塞(如系统调用、channel 操作)时,M 会释放 P,P 可以绑定到其他 M 继续执行剩余的 goroutine。
  • 如果某个 P 的本地队列为空,它会从其他 P 或全局队列中偷取(work stealing)goroutine 来执行,以实现负载均衡。

这种设计使得 Go 程序能够高效地利用多核 CPU,同时保持 goroutine 创建和切换的低开销。


三、并发、协程与 channel

1. 并发与并行

  • 并发(Concurrency):指程序结构上同时处理多个任务的能力(逻辑上的同时)。在 Go 中,通过 goroutine 和 channel 可以轻松构建并发程序。
  • 并行(Parallelism):指物理上在同一时刻执行多个任务(需要多核 CPU)。Go 的调度器可以将多个 goroutine 分配到多个 OS 线程上,从而实现并行。

并发是结构,并行是执行。Go 鼓励通过并发组合来解决问题,而不必担心并行细节。

2. 协程(Coroutine)与 goroutine

“协程”是一个更广义的概念,指用户态轻量级线程,可以主动让出(yield)控制权。goroutine 是 Go 对协程的实现,但有一些不同:

  • goroutine 是多核并发的,可以并行运行在不同线程上。
  • goroutine 的调度是抢占式的(当 goroutine 阻塞或长时间运行时,调度器会强制切换),而不是完全由用户主动 yield。
  • goroutine 之间通过 channel 通信,遵循“不要通过共享内存来通信,而应通过通信来共享内存”的理念。

3. channel:goroutine 之间的通信管道

channel 是 Go 语言提供的一种类型,用于在 goroutine 之间安全地传递数据。它类似于一个队列(FIFO),并内置了同步机制。

channel 的创建

ch := make(chan int)      // 无缓冲 channel
ch := make(chan string, 10) // 有缓冲 channel,缓冲区大小为 10

发送和接收

ch <- value   // 发送 value 到 channel
value := <-ch // 从 channel 接收值
close(ch)     // 关闭 channel(接收方可以检测到关闭)

无缓冲 channel:发送和接收操作会阻塞,直到另一端准备好,从而实现同步。 有缓冲 channel:发送仅在缓冲区满时阻塞,接收仅在缓冲区空时阻塞。

示例:使用 channel 同步 goroutine

package main

import "fmt"

func worker(done chan bool) {
    fmt.Println("working...")
    // 模拟任务
    done <- true // 发送完成信号
}

func main() {
    done := make(chan bool)
    go worker(done)
    <-done // 等待接收,阻塞直到 worker 发送完成
    fmt.Println("done")
}

示例:使用 channel 在多个 goroutine 间传递数据

package main

import "fmt"

func main() {
    ch := make(chan int)

    // 启动一个 goroutine 发送数据
    go func() {
        for i := 0; i < 5; i++ {
            ch <- i
        }
        close(ch)
    }()

    // 主 goroutine 接收数据
    for v := range ch {
        fmt.Println(v)
    }
}

当 channel 被关闭且缓冲区为空时,range 循环会自动退出。

4. select 多路复用

select 语句可以让一个 goroutine 同时等待多个 channel 操作,实现超时、非阻塞等机制。

select {
case msg1 := <-ch1:
    fmt.Println("received from ch1:", msg1)
case msg2 := <-ch2:
    fmt.Println("received from ch2:", msg2)
case <-time.After(1 * time.Second):
    fmt.Println("timeout")
default:
    fmt.Println("no activity")
}

四、总结

  • goroutine 是 Go 并发编程的基本单位,轻量且高效。
  • 通过 go 关键字轻松创建,由 Go 运行时调度(GMP 模型)。
  • 并发是程序结构,并行是执行;goroutine 可以并行运行在多核上。
  • channel 是 goroutine 之间通信的首选方式,遵循 CSP 模型(Communicating Sequential Processes)。
  • 使用 select 可以处理多个 channel 操作,实现复杂的并发控制。

给初学者的建议

  1. 尝试用 goroutine 编写简单的并发程序,如并发下载、并发计算。
  2. 理解 channel 的阻塞特性,学会使用无缓冲 channel 同步,有缓冲 channel 解耦。
  3. 注意避免死锁(例如 goroutine 互相等待对方发送/接收)。
  4. 使用 sync.WaitGroup 等工具等待多个 goroutine 完成,或者用 channel 本身进行同步。