Go 协程01-基础用法
本文内容基于 AI 生成结果整理,可能包含不准确信息,仅供参考使用。
前置知识
进程和线程说明
- 进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位
- 线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位
- 一个进程可以创建销毁多个线程,同一个线程中的多个进程可以并发执行
- 一个程序至少有一个进程,一个进程至少有一个线程
并发和并行
- 多线程程序在单核上运行,就是并发
- 多线程程序在多核上运行,就是并行
并发:因为是在一个 cpu 上,比如有 10 个线程,每个线程执行 10 毫秒(进行轮询操作),从人的角度看,好像这 10 个线程都在运行,但是从微观上看,在某一个时间点看,其实只有一个线程在执行,这就是并发。
并行:因为是在多个 cpu 上(比如有 10 个 cpu),比如有 10 个线程,每个线程执行 10 毫秒(各自在不同的 cpu 上执行),从人的角度看,这 10 个线程都在运行,但是从微观上看,在某一个时间点看,也同时有 10 个线程在执行,这就是并行。
协程和主线程
-
主线程(有人理解成线程/也可以理解成进程):一个 go 线程上,可以起多个协程,可以理解成协程是轻量级的线程
-
协程的特点
- 有独立的栈空间
- 共享程序堆空间
- 调度由用户控制
- 协程是轻量级的线程
Goroutine(协程)
概念:
- Goroutine 是 Go 语言中的轻量级线程
- 由 Go 运行时管理,而非操作系统线程
- 创建成本极低(初始栈仅 2KB,可动态增长)
- 调度由 Go 运行时完成,非抢占式
特点:
- 语法简单:只需在函数调用前加
go
关键字 - 高效:一个 OS 线程可运行多个 goroutine
- 适合处理大量并发任务
go
func say(s string) {
time.Sleep(2 * time.Second)
fmt.Println(s)
}
func main() {
go say("hello") // 启动 goroutine
say("world") // 主 goroutine
}
当主 goroutine 执行完毕时,所有的 goroutine 都会被强制结束。
Channel(通道)
无缓冲 Channel
go
ch := make(chan int) // 无缓冲 channel
特点:
- 同步通信:发送和接收必须同时准备好
- 发送操作会阻塞,直到有接收者
- 接收操作会阻塞,直到有发送者
- 保证数据传递的同步性
工作原理:
txt
发送者 -> [无缓冲 channel] -> 接收者
(阻塞直到接收) (阻塞直到发送)
使用场景:
- 需要确保通信双方同步
- 需要严格的先后顺序
- 简单的信号通知
示例:
go
package main
import (
"fmt"
"time"
)
func worker(done chan<- bool) {
fmt.Println("working...")
time.Sleep(2 * time.Second)
done <- true
}
func main() {
done := make(chan bool)
go worker(done)
<-done
fmt.Println("done")
}
带缓冲 Channel
go
ch := make(chan int, 3) // 缓冲大小为3的 channel
特点:
- 异步通信:发送操作在缓冲区未满时不阻塞
- 只有当缓冲区满时,发送才会阻塞
- 只有当缓冲区空时,接收才会阻塞
- 允许发送和接收操作有一定的时间差
工作原理:
txt
发送者 -> [缓冲 channel(容量N)] -> 接收者
(仅当缓冲区满时阻塞) (仅当缓冲区空时阻塞)
使用场景:
- 需要处理突发流量
- 生产者和消费者速度不一致
- 需要一定程度的解耦
示例:
go
func main() {
ch := make(chan int, 2) // 缓冲大小为2
ch <- 1 // 不阻塞
ch <- 2 // 不阻塞
// ch <- 3 // 这里会阻塞,因为缓冲区已满
fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2
}
对比总结
特性 | 无缓冲 Channel | 带缓冲 Channel |
---|---|---|
创建方式 | make(chan T) |
make(chan T, N) |
同步性 | 强同步 | 弱同步 |
阻塞时机 | 发送和接收必须同时准备好 | 仅在缓冲区满/空时阻塞 |
数据安全 | 高(严格同步) | 中(可能丢失数据) |
性能 | 较低(频繁阻塞) | 较高(减少阻塞) |
典型用途 | 信号通知、严格同步 | 流量控制、解耦生产消费 |
Select 多路复用
概念:
- 用于同时监听多个 channel 操作
- 类似于 switch,但每个 case 必须是通信操作
- 会阻塞直到某个 case 可以执行
- 如果有多个 case 就绪,随机选择一个,未被选中的就绪 case 仍然保持就绪状态
特点:
- 处理多个 channel 的超时和默认情况
- 实现非阻塞的 channel 操作
- 常用于事件循环和超时控制
示例:
go
package main
import (
"fmt"
"time"
)
func main() {
c1 := make(chan string)
c2 := make(chan string)
go func() {
time.Sleep(time.Second * 1)
c1 <- "one"
}()
go func() {
time.Sleep(time.Second * 2)
c2 <- "two"
}()
for i := 0; i < 2; i++ {
select {
case msg1 := <-c1:
fmt.Println(msg1)
case msg2 := <-c2:
fmt.Println(msg2)
}
}
}
WaitGroup(等待组)
概念:
- 用于等待一组 goroutine 完成
- 内部维护一个计数器
Add()
增加计数器Done()
减少计数器Wait()
阻塞直到计数器归零
特点:
- 比 channel 更适合简单的等待场景
- 不需要传递数据,只需同步完成状态
- 通常与
defer
一起使用确保Done()
被调用
示例:
go
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("%d worker start \n", id)
time.Sleep(time.Second)
fmt.Printf("%d worker end \n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait()
fmt.Println("all workers completed")
}
Mutex(互斥锁)
概念:
- 用于保护共享资源免受并发访问
- 保证同一时间只有一个 goroutine 能访问临界区
- 有
Lock()
和Unlock()
方法 - 通常与
defer
一起使用确保解锁
特点:
- 比 channel 更适合保护共享状态
- 可能导致性能瓶颈(锁竞争)
- 需要小心避免死锁
示例:
go
package main
import (
"fmt"
"sync"
"time"
)
type safeCounter struct {
mu sync.Mutex
v map[string]int
}
func (sc *safeCounter) Inc(key string) {
sc.mu.Lock()
sc.v[key]++
sc.mu.Unlock()
}
func (sc *safeCounter) Val(key string) int {
sc.mu.Lock()
defer sc.mu.Unlock()
return sc.v[key]
}
func main() {
c := safeCounter{v: make(map[string]int)}
for i := 0; i < 1000; i++ {
go c.Inc("lei-cool")
}
time.Sleep(time.Second)
fmt.Println(c.Val("lei-cool"))
}
选择指南
- 何时用 channel:
- 传递数据所有权
- 协调 goroutine 工作流程
- 事件通知
- 何时用 mutex:
- 保护共享数据结构
- 缓存更新
- 需要原子操作的场景
- 何时用 WaitGroup:
- 等待一组 goroutine 完成
- 不需要传递数据
- 简单的完成同步