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 运行时会:
- 为新的 Goroutine 分配栈空间(初始 2KB)
- 将 Goroutine 添加到调度器的运行队列中
- 在适当的时机开始执行该 Goroutine
执行阶段 #
Goroutine 在执行过程中可能处于以下状态:
- 运行中(Running):正在 CPU 上执行
- 就绪(Runnable):等待被调度执行
- 阻塞(Blocked):等待某个条件满足(如 I/O 操作、Channel 操作等)
结束阶段 #
Goroutine 在以下情况下会结束:
- 正常结束:函数执行完毕并返回
- panic 未恢复:发生 panic 且没有被 recover
- 程序退出:主程序结束时,所有 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)
}
小结 #
在本节中,我们学习了:
- 并发与并行的区别:并发是程序结构,并行是执行方式
- Goroutine 的特点:轻量级、由 Go 运行时管理
- 创建 Goroutine:使用
go
关键字 - 变量捕获:在闭包中正确捕获变量的方法
- 生命周期管理:理解 Goroutine 的创建、执行和结束
Goroutine 是 Go 语言并发编程的基础,掌握其基本概念和使用方法对于编写高效的并发程序至关重要。在下一节中,我们将深入了解 Goroutine 的调度机制。
练习题 #
- 编写一个程序,创建 10 个 Goroutine,每个 Goroutine 打印自己的编号和当前时间
- 修改并发下载器示例,添加超时控制和错误重试机制
- 实现一个简单的并发计数器,多个 Goroutine 同时对一个共享变量进行递增操作(注意:这里会有竞态条件,我们将在后续章节解决)