Go语言并发:goroutine

goroutine是Go语言并发设计的核心。它是一种轻量级的线程,由Go运行时管理,创建成本极低(初始栈仅几KB)。通过在函数调用前加上go关键字,就可以启动一个并发的goroutine。借助goroutine和channel,Go能够轻松处理高并发场景。

核心特点:goroutine多路复用操作系统线程,用户态调度,切换成本低;使用go关键字即可启动,语言原生支持。

1. 启动一个goroutine

只需在函数调用前添加go关键字。下面的示例会同时打印“hello”和“world”,但顺序不确定。

基本用法
package main

import (
    "fmt"
    "time"
)

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

func main() {
    go say("world")   // 启动新goroutine
    say("hello")      // 主goroutine继续运行
}

输出可能交错(如“hello world hello world hello world”)。由于主函数返回时会立即终止所有goroutine,因此可能需要等待(后面介绍WaitGroup)。

2. 使用匿名函数启动goroutine

func main() {
    go func() {
        fmt.Println("匿名goroutine执行")
    }()
    time.Sleep(time.Millisecond) // 简单等待(不推荐)
}

注意:匿名goroutine如果捕获外部变量,要注意闭包特性(可能共享变量)。

3. 等待goroutine完成(sync.WaitGroup)

time.Sleep不适合生产环境。标准库sync.WaitGroup提供了可靠的等待机制。

WaitGroup示例
package main

import (
    "fmt"
    "sync"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()  // 完成时计数器减1
    fmt.Printf("Worker %d 开始工作\n", id)
    // 模拟工作
    fmt.Printf("Worker %d 完成\n", id)
}

func main() {
    var wg sync.WaitGroup
    for i := 1; i <= 3; i++ {
        wg.Add(1)         // 计数器加1
        go worker(i, &wg) // 传递WaitGroup地址
    }
    wg.Wait() // 阻塞直到计数器归零
    fmt.Println("所有worker完成")
}
Worker 3 开始工作
Worker 3 完成
Worker 1 开始工作
Worker 1 完成
Worker 2 开始工作
Worker 2 完成
所有worker完成
注意:wg.Add必须在goroutine启动前调用,且wg.Done最好使用defer确保即使出错也能减计数器。

4. goroutine vs OS线程

  • 创建成本:goroutine栈初始2KB,可动态增长;OS线程栈通常1~8MB。
  • 切换开销:goroutine由Go调度器在用户态管理,切换成本远低于线程。
  • 数量:一个Go程序可以轻松运行数十万甚至百万个goroutine,而线程数通常受限。
// 示例:启动大量goroutine(不会立即耗尽内存)
var wg sync.WaitGroup
for i := 0; i < 100000; i++ {
    wg.Add(1)
    go func(n int) {
        defer wg.Done()
        time.Sleep(time.Millisecond)
    }(i)
}
wg.Wait()

5. Go调度器:GMP模型

Go调度器采用GMP模型:G(goroutine)、M(操作系统线程)、P(处理器/上下文)。P的数量由GOMAXPROCS决定,默认为CPU核心数。调度器将G分配到M上执行,实现多路复用。

可以通过runtime.GOMAXPROCS(n)设置并行度。

6. 数据竞态提醒

多个goroutine并发访问同一变量可能导致不可预知的结果。下面的示例存在数据竞争。

var counter int
var wg sync.WaitGroup

for i := 0; i < 1000; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        counter++ // 非原子操作,存在竞态
    }()
}
wg.Wait()
fmt.Println(counter) // 通常小于1000
解决方案:使用sync.Mutex、atomic包或channel。具体将在下一章详细介绍。

7. 控制goroutine退出

通常使用channel传递退出信号,或用context.Context。

func worker(stop <-chan struct{}) {
    for {
        select {
        case <-stop:
            fmt.Println("收到退出信号")
            return
        default:
            // 正常工作
        }
    }
}

综合示例:模拟并发任务

使用WaitGroup实现并发处理
package main

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

type Task struct {
    ID   int
    Name string
}

func process(task Task, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Printf("开始处理任务: %d - %s\n", task.ID, task.Name)
    time.Sleep(time.Millisecond * 500) // 模拟耗时
    fmt.Printf("完成任务: %d\n", task.ID)
}

func main() {
    tasks := []Task{
        {1, "数据清洗"},
        {2, "数据分析"},
        {3, "生成报告"},
    }

    var wg sync.WaitGroup
    for _, task := range tasks {
        wg.Add(1)
        go process(task, &wg)
    }
    wg.Wait()
    fmt.Println("所有任务处理完成")
}
开始处理任务: 2 - 数据分析
开始处理任务: 1 - 数据清洗
开始处理任务: 3 - 生成报告
完成任务: 2
完成任务: 1
完成任务: 3
所有任务处理完成
小结:goroutine是Go并发的基石,轻量且高效。使用go关键字即可启动,配合sync.WaitGroup可以协调多个goroutine的完成。但要注意数据竞态和生命周期管理。下一章我们将学习goroutine之间的通信方式——channel。