概念
Go 的并发模型以 goroutine 和 channel 为核心,提供了一种简洁高效的并发编程方式。
其调度器采用 GMP 模型(G — Goroutine,M — 系统线程,P — 逻辑处理器),负责将 goroutine 分配到系统线程上执行,并通过 M 与 P 的协同配合,实现高效的并发管理。
Goroutine(协程) 是 Go 中的并发执行单元,相当于轻量级的线程。它由 Go 运行时统一调度,用户无需手动管理线程分配,只需使用 go 关键字即可启动。Goroutine 具有非阻塞特性,能够轻松支撑成千上万个并发任务。
Channel(通道) 则是 goroutine 之间通信的主要机制,支持同步和数据传递,有助于避免显式使用锁。通道通过 chan 关键字创建,并使用 <- 操作符进行数据的发送与接收
Go 的并发口号是:不要通过共享内存来通信,而要通过通信来共享内存
协程(goroutine )
协程使用案例:
import (
"fmt"
"time"
)
func cook(dish string) {
for i := 1; i <= 3; i++ {
fmt.Println("后厨正在做:", dish)
// 模拟做菜的时间
time.Sleep(500 * time.Millisecond)
}
}
func main() {
// 开启一个协程
go cook("红烧肉")
// 主线程
cook("西红柿炒蛋")
}Go 语言的程序生命周期完全由主 goroutine(即运行
main函数的 goroutine)决定。所以,当main函数执行完毕(主 goroutine 退出)时,Go 程序会终止整个进程,所有尚未执行完的 goroutine 都会被强制中断,不会等待它们完成
生命周期(context)
当 main goroutine 下达一道命令让两个 goroutine 去负责执行。但是如果中间有需要让负责执行的两个 goroutine 立马停止会很麻烦。而 context.Context 就是一个带“取消按钮”的令牌,main goroutine 把令牌交给每一个负责执行的 goroutine,只要 main goroutine 按下按钮(调用 cancel),所有拿到这个令牌的 goroutine 都会同时收到“定制”信号
Context 不传递数据,它只传递控制信号和截止时间(以及少量请求范围的元数据)
详情
Context 是一个接口,包含四个方法:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}停止信号(Done() <-chan struct{}):返回一个只读的 channel,当这个 channel 被关闭时,意味着“可以停止了”
停止原因(Err() error):如果 Done 的 channel 已经被关闭,Err() 会返回一个非 nil 的错误,说明取消原因:
context.Canceled:主动调用cancelcontext.DeadlineExceeded:超时
最后时间(Deadline() (time.Time, bool)):返回 Context 的绝对截止时间,以及是否设置了截止时间
携带数据(Value(key) interface{}):可以携带请求相关的数据(如用户ID、追踪ID)。注意:仅用于传递跨 API 边界的请求范围数据,对于业务参数不要传递
根与树
根节点有两个:
ctx := context.Background() // 大本营,永不取消,无超时,无值
ctx := context.TODO() // 暂时不确定用啥,先占个位
TODO只是语义上与Background一样,用来提示开发者将来要替换成具体的 context
衍生出子 Context
- WithCancel:生成一个可以主动取消的令牌
- WithDeadline / WithTimeout:生成一个带绝对/相对超时的令牌,时间到自动取消
- WithValue:在令牌上写入一些参数,传给下游
这些函数都会返回一个新的
Context和一个CancelFunc(WithValue没有CancelFunc,因为它不产生取消能力)。只要父 Context 被取消,它所有的子 Context 也会被自动取消
ctx := context.Background()
ctxA, cancelA := context.WithCancel(ctx)
ctxB, cancelB := context.WithTimeout(ctxA, 5*time.Second) // 5秒后自动取消
ctxC := context.WithValue(ctxB, "user", "小张") // 贴个标签上述内容一层套一层形成一棵树
- 如果调用
cancelA(),ctxA、ctxB、ctxC 全被取消 - 如果 5 秒内没有调用
cancelA,ctxB 的超时也会触发,同样导致 ctxB、ctxC 取消
案例:
func cookWithCtx(ctx context.Context, dish string) {
for {
select {
case <-ctx.Done():
fmt.Println(dish, "被取消,停止烹饪")
return
default:
fmt.Println("正在做", dish)
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go cookWithCtx(ctx, "红烧肉")
go cookWithCtx(ctx, "清蒸鱼")
time.Sleep(2 * time.Second)
cancel() // 总厨下令取消
time.Sleep(time.Second) // 让它们打印完毕
}通道(Channel)
创建一个通道(make(chan [类型], [传送带格数])):
// 完全体
var ch chan string = make(chan string)
// 简写
ch := make(chan string)通道有两种(区别为是否赋值传送带格数):
- 无缓冲通道:传送带上一格都没有,必须同时有人放、有人取。如果传送带上还有数据没有取否,则放的人会阻塞;如果取数据的人没有取到数据,那么取的人也会阻塞。用于精确同步。
- 有缓冲通道:传送带上有可以暂存的格子,可以放满或取空而不阻塞
无缓冲:
func main() {
ch := make(chan string) // 无缓冲传送带,传递string
go func() {
ch <- "红烧肉已完成" // 后厨放上结果
fmt.Println("后厨已经做完了菜")
}()
msg := <-ch // 主厨师取走结果
fmt.Println("取到:", msg)
}有缓冲(三格):
ch := make(chan int, 3) // 传送带可以暂存3个整数
ch <- 1
ch <- 2
ch <- 3
// 再放就会阻塞,直到有人取走放取数据:把数据送入通道和从通道中取出数据都是使用
<-符号,这个符号同时也是通道存满数据时阻塞的位置
遍历与关闭通道
Go 通过 range 关键字来实现遍历读取到的数据,类似于与数组或切片。格式如:v, ok := <-ch。如果通道接收不到数据后 ok 就为 false,这时通道就可以使用 close() 函数来关闭通道
func cook(dish []string, ch chan string) {
for _, v := range dish {
fmt.Println("后厨正在做:", v)
ch <- v + " 已经做好了"
}
// 关闭通道
close(ch)
}
func main() {
var ch chan string = make(chan string)
dish := []string{"红烧肉", "西红柿炒蛋", "青椒肉丝"}
// 开启一个协程
go cook(dish, ch)
// 遍历
for v := range ch {
fmt.Println("获取到菜品:" + v)
}
}上述案例中对于通道关闭有些错误,正常做法应该为:谁创建,谁关闭。避免在接收者端关闭 channel 造成 panic
Select 语句
类似于一个 goroutine 同时盯着几条传送带。哪个上面有数据来了,就去取走;如果好几条同时有数据,随机选一个处理。如果都没有,goroutine 可以等待(default 则不等待),也可以设置超时
同时监听多个 channel:
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "红烧肉"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "清蒸鱼"
}()
for i := 0; i < 2; i++ {
select {
// 等待ch1有数据
case dish := <-ch1:
fmt.Println("取到", dish)
// 等待ch2有数据
case dish := <-ch2:
fmt.Println("取到", dish)
// 等待三秒,如果没有数据,就打印"等太久了,不等了"
case <-time.After(3 * time.Second):
fmt.Println("等太久了,不等了")
return
}
}同步工具
如果存在多个协程要操作同一个资源,则是需要加锁的
互斥锁(sync.Mutex)
import "sync"
var (
counter int
mu sync.Mutex
)
func increment() {
mu.Lock() // 上锁
counter++ // 这段代码一次只有一个人能进入
mu.Unlock() // 解锁
}等待所有(sync.WaitGroup)
var wg sync.WaitGroup
func work(id int) {
defer wg.Done() // 导报一层等待
fmt.Printf("厨师 %d 开始工作\n", id)
time.Sleep(time.Second)
fmt.Printf("厨师 %d 完成工作\n", id)
}
func main() {
for i := 1; i <= 5; i++ {
wg.Add(1) // 增加一重等待
go work(i)
}
wg.Wait() // 等所有报告完成
fmt.Println("所有菜都好了,可以上桌")
}