Go语言并发编程详解:goroutine和channel实战
在这里插入代码片# Go语言并发编程详解:goroutine和channel实战
Go 语言天生支持并发,这得益于其轻量级的 goroutine 和强大的 channel 机制。本文将深入讲解 goroutine 和 channel 的使用方法,通过多个实战代码示例帮助你掌握 Go 并发编程的精髓。
一、为什么 Go 适合并发编程
Go 语言在设计之初就考虑了并发。它的并发模型基于 CSP(Communicating Sequential Processes),核心思想是:不要通过共享内存来通信,而要通过通信来共享内存。
Go 并发的两大基石:
- goroutine:轻量级线程,创建成本极低
- channel:goroutine 之间安全通信的管道
二、goroutine 详解
2.1 什么是 goroutine
goroutine 是 Go 中的轻量级执行单元。与操作系统线程相比:
- 创建一个 goroutine 只需约 2KB 栈空间
- 线程通常需要 1-8MB 栈空间
- goroutine 由 Go 运行时调度,而非操作系统
2.2 创建 goroutine
package main
import (
"fmt"
"time"
)
func sayHello(name string) {
for i := 0; i < 3; i++ {
fmt.Printf("%s: Hello %d\n", name, i)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
// 使用 go 关键字启动 goroutine
go sayHello("Goroutine-1")
go sayHello("Goroutine-2")
// 主 goroutine 也执行
sayHello("Main")
// 等待其他 goroutine 完成
time.Sleep(time.Second)
fmt.Println("All done!")
}
2.3 goroutine 的调度
Go 使用 GMP 调度模型:
- G:goroutine
- M:操作系统线程
- P:处理器(调度上下文)
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
// 设置使用多个 CPU 核心
runtime.GOMAXPROCS(runtime.NumCPU())
var wg sync.WaitGroup
wg.Add(3)
for i := 1; i <= 3; i++ {
go func(id int) {
defer wg.Done()
fmt.Printf("Worker %d running on CPU\n", id)
}(i)
}
wg.Wait()
fmt.Println("All workers completed")
}
三、channel 详解
3.1 什么是 channel
channel 是 goroutine 之间通信的管道。它是类型安全的,发送和接收操作都会阻塞,直到另一端准备好。
3.2 基本使用
package main
import "fmt"
func main() {
// 创建一个无缓冲 channel
ch := make(chan string)
// 启动 goroutine 发送数据
go func() {
ch <- "Hello from goroutine!"
}()
// 主 goroutine 接收数据
msg := <-ch
fmt.Println(msg)
}
3.3 缓冲 channel
package main
import "fmt"
func main() {
// 创建一个缓冲大小为 3 的 channel
ch := make(chan int, 3)
// 可以连续发送 3 个数据而不会阻塞
ch <- 1
ch <- 2
ch <- 3
// 接收数据
fmt.Println(<-ch) // 输出: 1
fmt.Println(<-ch) // 输出: 2
fmt.Println(<-ch) // 输出: 3
// 缓冲区已空,再接收会阻塞
// fmt.Println(<-ch) // deadlock!
}
3.4 channel 的方向
package main
import "fmt"
// 只能发送的 channel
func producer(out chan<- int) {
for i := 0; i < 5; i++ {
out <- i * 10
}
close(out)
}
// 只能接收的 channel
func consumer(in <-chan int) {
for num := range in {
fmt.Printf("Received: %d\n", num)
}
}
func main() {
ch := make(chan int, 5)
go producer(ch)
consumer(ch)
}
四、select 语句
select 让 goroutine 可以同时等待多个 channel 操作:
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "Message from ch1"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "Message from ch2"
}()
// 使用 select 等待多个 channel
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
}
}
}
4.1 超时处理
func waitForData(ch chan string) {
select {
case data := <-ch:
fmt.Printf("Received: %s\n", data)
case <-time.After(3 * time.Second):
fmt.Println("Timeout! No data received.")
}
}
五、实战:工作池(Worker Pool)
工作池是 Go 并发编程中最经典的模式之一:
package main
import (
"fmt"
"time"
)
// Worker 处理任务
func worker(id int, jobs <-chan int, results chan<- int) {
for job := range jobs {
fmt.Printf("Worker %d started job %d\n", id, job)
time.Sleep(time.Second) // 模拟耗时操作
results <- job * job // 返回平方值
}
}
func main() {
const numJobs = 10
const numWorkers = 3
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
// 启动 3 个 worker
for w := 1; w <= numWorkers; w++ {
go worker(w, jobs, results)
}
// 发送 10 个任务
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs)
// 收集结果
for r := 1; r <= numJobs; r++ {
fmt.Printf("Result: %d\n", <-results)
}
}
六、实战:扇出扇入(Fan-out Fan-in)
package main
import (
"fmt"
"sync"
)
// 生成数据
func generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
out <- n
}
}()
return out
}
// 处理数据(平方)
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
out <- n * n
}
}()
return out
}
// 合并多个 channel
func merge(cs ...<-chan int) <-chan int {
var wg sync.WaitGroup
out := make(chan int)
output := func(c <-chan int) {
defer wg.Done()
for n := range c {
out <- n
}
}
wg.Add(len(cs))
for _, c := range cs {
go output(c)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
func main() {
in := generate(1, 2, 3, 4, 5)
// Fan-out: 启动多个 square 处理器
c1 := square(in)
c2 := square(in)
// Fan-in: 合并结果
for result := range merge(c1, c2) {
fmt.Printf("Result: %d\n", result)
}
}
七、性能对比
我们对比串行和并发处理同样任务的性能:
package main
import (
"fmt"
"sync"
"time"
)
func slowTask(id int) int {
time.Sleep(100 * time.Millisecond)
return id * id
}
// 串行处理
func serialProcess(count int) int {
total := 0
for i := 0; i < count; i++ {
total += slowTask(i)
}
return total
}
// 并发处理
func concurrentProcess(count int) int {
var wg sync.WaitGroup
results := make(chan int, count)
for i := 0; i < count; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
results <- slowTask(id)
}(i)
}
go func() {
wg.Wait()
close(results)
}()
total := 0
for r := range results {
total += r
}
return total
}
func main() {
count := 20
start := time.Now()
serialResult := serialProcess(count)
serialTime := time.Since(start)
fmt.Printf("串行: 结果=%d, 耗时=%v\n", serialResult, serialTime)
start = time.Now()
concurrentResult := concurrentProcess(count)
concurrentTime := time.Since(start)
fmt.Printf("并发: 结果=%d, 耗时=%v\n", concurrentResult, concurrentTime)
fmt.Printf("加速比: %.2fx\n", float64(serialTime)/float64(concurrentTime))
}
运行结果示例:
串行: 结果=2470, 耗时=2.0031245s
并发: 结果=2470, 耗时=100.5234ms
加速比: 19.92x
可以看到,并发处理比串行处理快了约 20 倍!
八、常见陷阱与注意事项
8.1 goroutine 泄漏
// 错误示例:goroutine 泄漏
func leakExample() {
ch := make(chan int)
go func() {
val := <-ch // 如果没有发送者,这个 goroutine 会永远阻塞
fmt.Println(val)
}()
// 函数返回后,goroutine 仍在等待,造成泄漏
}
8.2 正确关闭 channel
// 只有发送者应该关闭 channel
func producer(ch chan<- int) {
defer close(ch) // 正确:发送者关闭
for i := 0; i < 10; i++ {
ch <- i
}
}
8.3 使用 sync.WaitGroup 等待
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
// do work
}(i)
}
wg.Wait() // 等待所有 goroutine 完成
九、总结
| 特性 | goroutine | 操作系统线程 |
|---|---|---|
| 创建成本 | ~2KB | ~1-8MB |
| 调度方式 | Go运行时 | 操作系统 |
| 切换成本 | 极低 | 较高 |
| 数量上限 | 十万级 | 千级 |
Go 并发编程的核心要点:
- 用 go 关键字启动 goroutine
- 用 channel 在 goroutine 间通信
- 用 select 处理多路 channel
- 用 sync.WaitGroup 等待 goroutine 完成
- 避免 goroutine 泄漏和死锁
掌握 goroutine 和 channel,你就掌握了 Go 语言最强大的特性。开始写你的并发程序吧!
如果这篇文章对你有帮助,欢迎点赞、收藏、关注! 后续会分享更多 Go 语言进阶教程,包括 context 包的使用、并发模式深入等内容。有问题欢迎评论区交流!
作者:tlyyxjz | 转载请注明出处
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)