2.1.1 并发概念与 Goroutine

2.1.1 并发概念与 Goroutine #

并发与并行的区别 #

在深入学习 Go 语言的并发编程之前,我们需要明确理解并发(Concurrency)和并行(Parallelism)这两个概念的区别。

并发(Concurrency) #

并发是指程序的结构,即程序被设计成可以同时处理多个任务的能力。并发关注的是任务的组织和协调,而不是任务的实际执行方式。在单核处理器上,并发通过时间片轮转来实现,看起来像是同时执行多个任务,但实际上是快速切换执行。

并行(Parallelism) #

并行是指程序的执行,即多个任务真正同时执行。并行需要多核处理器的支持,每个核心可以独立执行不同的任务。

Rob Pike(Go 语言的创始人之一)有一句经典的话:“并发是同时处理很多事情,并行是同时做很多事情。”

// 并发示例:一个服务器同时处理多个客户端请求
func handleClient(conn net.Conn) {
    // 处理客户端请求
    defer conn.Close()
    // ... 处理逻辑
}

func server() {
    listener, _ := net.Listen("tcp", ":8080")
    for {
        conn, _ := listener.Accept()
        go handleClient(conn) // 并发处理每个客户端
    }
}

Goroutine 简介 #

Goroutine 是 Go 语言并发编程的核心概念,它是一种轻量级的线程,由 Go 运行时管理。与传统的操作系统线程相比,Goroutine 具有以下特点:

轻量级 #

  • 内存占用小:每个 Goroutine 的初始栈大小只有 2KB,而操作系统线程通常需要 2MB
  • 创建成本低:创建一个 Goroutine 的成本远低于创建一个操作系统线程
  • 数量限制少:理论上可以创建数百万个 Goroutine

由 Go 运行时管理 #

  • 用户态调度:Goroutine 的调度完全在用户态进行,避免了内核态和用户态之间的切换开销
  • 协作式调度:Goroutine 在特定的调度点主动让出 CPU,而不是被强制抢占

创建和使用 Goroutine #

基本语法 #

创建 Goroutine 的语法非常简单,只需要在函数调用前加上 go 关键字:

package main

import (
    "fmt"
    "time"
)

func sayHello(name string) {
    for i := 0; i < 3; i++ {
        fmt.Printf("Hello, %s! (%d)\n", name, i+1)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    // 启动一个 Goroutine
    go sayHello("Alice")

    // 启动另一个 Goroutine
    go sayHello("Bob")

    // 主 Goroutine 等待一段时间
    time.Sleep(500 * time.Millisecond)
    fmt.Println("Main function finished")
}

匿名函数 Goroutine #

我们也可以使用匿名函数创建 Goroutine:

package main

import (
    "fmt"
    "time"
)

func main() {
    // 使用匿名函数创建 Goroutine
    go func() {
        for i := 0; i < 3; i++ {
            fmt.Printf("Anonymous goroutine: %d\n", i)
            time.Sleep(100 * time.Millisecond)
        }
    }()

    // 带参数的匿名函数 Goroutine
    message := "Hello from closure"
    go func(msg string) {
        fmt.Println(msg)
    }(message)

    time.Sleep(500 * time.Millisecond)
}

闭包中的变量捕获 #

在使用闭包创建 Goroutine 时,需要特别注意变量的捕获方式:

package main

import (
    "fmt"
    "time"
)

func main() {
    // 错误的方式:所有 Goroutine 都会打印相同的值
    fmt.Println("错误的方式:")
    for i := 0; i < 3; i++ {
        go func() {
            fmt.Printf("Value: %d\n", i) // 所有 Goroutine 都会打印 3
        }()
    }
    time.Sleep(100 * time.Millisecond)

    fmt.Println("\n正确的方式1:")
    // 正确的方式1:通过参数传递
    for i := 0; i < 3; i++ {
        go func(val int) {
            fmt.Printf("Value: %d\n", val)
        }(i)
    }
    time.Sleep(100 * time.Millisecond)

    fmt.Println("\n正确的方式2:")
    // 正确的方式2:在循环内部创建局部变量
    for i := 0; i < 3; i++ {
        i := i // 创建局部变量
        go func() {
            fmt.Printf("Value: %d\n", i)
        }()
    }
    time.Sleep(100 * time.Millisecond)
}

Goroutine 的生命周期 #

创建阶段 #

当使用 go 关键字启动一个函数时,Go 运行时会:

  1. 为新的 Goroutine 分配栈空间(初始 2KB)
  2. 将 Goroutine 添加到调度器的运行队列中
  3. 在适当的时机开始执行该 Goroutine

执行阶段 #

Goroutine 在执行过程中可能处于以下状态:

  • 运行中(Running):正在 CPU 上执行
  • 就绪(Runnable):等待被调度执行
  • 阻塞(Blocked):等待某个条件满足(如 I/O 操作、Channel 操作等)

结束阶段 #

Goroutine 在以下情况下会结束:

  1. 正常结束:函数执行完毕并返回
  2. panic 未恢复:发生 panic 且没有被 recover
  3. 程序退出:主程序结束时,所有 Goroutine 都会被终止
package main

import (
    "fmt"
    "time"
)

func worker(id int, done chan bool) {
    fmt.Printf("Worker %d starting\n", id)

    // 模拟工作
    time.Sleep(time.Second)

    fmt.Printf("Worker %d done\n", id)
    done <- true
}

func main() {
    done := make(chan bool, 3)

    // 启动多个 worker Goroutine
    for i := 1; i <= 3; i++ {
        go worker(i, done)
    }

    // 等待所有 worker 完成
    for i := 1; i <= 3; i++ {
        <-done
    }

    fmt.Println("All workers completed")
}

主 Goroutine 与程序退出 #

需要特别注意的是,当主 Goroutine(main 函数)结束时,整个程序就会退出,不管其他 Goroutine 是否还在运行:

package main

import (
    "fmt"
    "time"
)

func longRunningTask() {
    for i := 0; i < 10; i++ {
        fmt.Printf("Task running: %d\n", i)
        time.Sleep(500 * time.Millisecond)
    }
}

func main() {
    go longRunningTask()

    fmt.Println("Main function starting")
    time.Sleep(2 * time.Second) // 只等待 2 秒
    fmt.Println("Main function ending")
    // 程序退出,longRunningTask 可能还没完成
}

为了确保所有 Goroutine 都能正常完成,我们需要使用同步机制,这将在后续章节中详细介绍。

实践示例:并发下载器 #

让我们通过一个实际的例子来演示 Goroutine 的使用:

package main

import (
    "fmt"
    "io"
    "net/http"
    "sync"
    "time"
)

// 下载任务结构
type DownloadTask struct {
    URL      string
    Filename string
}

// 下载函数
func download(task DownloadTask, wg *sync.WaitGroup) {
    defer wg.Done()

    start := time.Now()
    fmt.Printf("开始下载: %s\n", task.URL)

    // 模拟 HTTP 请求
    resp, err := http.Get(task.URL)
    if err != nil {
        fmt.Printf("下载失败 %s: %v\n", task.URL, err)
        return
    }
    defer resp.Body.Close()

    // 读取响应内容(这里只是丢弃,实际应用中会保存到文件)
    _, err = io.Copy(io.Discard, resp.Body)
    if err != nil {
        fmt.Printf("读取失败 %s: %v\n", task.URL, err)
        return
    }

    duration := time.Since(start)
    fmt.Printf("下载完成: %s (耗时: %v)\n", task.URL, duration)
}

func main() {
    // 下载任务列表
    tasks := []DownloadTask{
        {"https://httpbin.org/delay/1", "file1.txt"},
        {"https://httpbin.org/delay/2", "file2.txt"},
        {"https://httpbin.org/delay/1", "file3.txt"},
        {"https://httpbin.org/delay/3", "file4.txt"},
    }

    var wg sync.WaitGroup
    start := time.Now()

    // 并发下载
    for _, task := range tasks {
        wg.Add(1)
        go download(task, &wg)
    }

    // 等待所有下载完成
    wg.Wait()

    totalDuration := time.Since(start)
    fmt.Printf("所有下载完成,总耗时: %v\n", totalDuration)
}

小结 #

在本节中,我们学习了:

  1. 并发与并行的区别:并发是程序结构,并行是执行方式
  2. Goroutine 的特点:轻量级、由 Go 运行时管理
  3. 创建 Goroutine:使用 go 关键字
  4. 变量捕获:在闭包中正确捕获变量的方法
  5. 生命周期管理:理解 Goroutine 的创建、执行和结束

Goroutine 是 Go 语言并发编程的基础,掌握其基本概念和使用方法对于编写高效的并发程序至关重要。在下一节中,我们将深入了解 Goroutine 的调度机制。

练习题 #

  1. 编写一个程序,创建 10 个 Goroutine,每个 Goroutine 打印自己的编号和当前时间
  2. 修改并发下载器示例,添加超时控制和错误重试机制
  3. 实现一个简单的并发计数器,多个 Goroutine 同时对一个共享变量进行递增操作(注意:这里会有竞态条件,我们将在后续章节解决)