发布于2026-07-31 阅读(0)
扫一扫,手机访问
说起Golang的并发模型,真正把它和其他语言拉开差距的,是Goroutine和Channel这一对核心概念。这里先给出一个核心判断:Go的并发设计哲学,根植于CSP理论,强调"通过通信来共享内存"——这个思路,从根本上抹平了传统并发编程中大量棘手的复杂性。

Goroutine到底是何方神圣?简单说,它就是Go语言里最轻量级的并发执行单元,由Go运行时在用户态全权管理。创建、销毁、切换的开销,跟操作系统线程完全不在一个量级——初始栈空间只要几KB,还能动态扩容。用go关键字一招呼,一个Goroutine就启动了:
go func() { fmt.Println("Hello from Goroutine") }()
它的调度走的是M:N模型(M个Goroutine映射到N个操作系统线程)。关键在"工作窃取"策略:空闲的逻辑处理器(P)会去别的P的本地队列里偷Goroutine来执行,把多核CPU的资源吃干榨净。
Channel则是Goroutine之间传递数据的同步原语,分两种:无缓冲的(同步阻塞)和有缓冲的(异步非阻塞)——
ch := make(chan int) // 无缓冲Channel
go func() { ch <- 42 }() // 发送阻塞,直到主Goroutine接收
fmt.Println(<-ch) // 接收阻塞,直到Goroutine发送
bufCh := make(chan int, 2) // 缓冲区大小为2
bufCh <- 1 // 不阻塞
bufCh <- 2 // 不阻塞
fmt.Println(<-bufCh) // 接收1
fmt.Println(<-bufCh) // 接收2
Channel遵守先进先出原则,关闭后不能再发数据,但可以继续收完剩余的数据。
Channel能解决大部分并发问题,但碰到共享内存的场景,还得靠sync包里的同步原语来兜底。
sync.WaitGroup靠计数器来同步。核心方法三个:Add(delta int)增加计数器(记得在启动Goroutine前调用);Done()减少计数器(通常放defer里);Wait()阻塞主Goroutine,直到计数器归零。看个例子:
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1) // 增加计数器
go func(i int) {
defer wg.Done() // 完成后减少计数器
fmt.Println(i)
}(i)
}
wg.Wait() // 等待所有Goroutine完成
Lock()和Unlock()实现排他访问,防止多个Goroutine同时改共享资源。比如:var (
counter int
mu sync.Mutex
)
func increment() {
mu.Lock() // 加锁
defer mu.Unlock() // 解锁
counter++
}
RLock()/RUnlock()),但写的时候必须排他(Lock()/Unlock())。sync.Once的Do(f func())方法,保证传入的函数只执行一次——最适合做配置加载这类初始化操作。比如:
var once sync.Once
func setup() {
once.Do(func() {
fmt.Println("Initialization done") // 仅执行一次
})
}
context包用来跨Goroutine传递取消信号、超时或截止时间,是防止Goroutine泄漏的必备武器。常用的方法:WithCancel创建可取消的Context;WithTimeout创建带超时的Context;Done()返回一个Channel,当Context被取消或超时的时候关闭。看一个例子:
ctx, cancel := context.WithCancel(context.Background())
go func(ctx context.Context) {
select {
case <-ctx.Done(): // 收到取消信号
fmt.Println("Goroutine stopped")
return
case <-time.After(2 * time.Second):
fmt.Println("Work done")
}
}(ctx)
time.Sleep(1 * time.Second)
cancel() // 手动取消
time.Sleep(1 * time.Second) // 等待Goroutine退出
select语句相当于Channel版的"IO多路复用",可以同时监听多个Channel的发送/接收事件。比如:
select {
case msg1 := <-ch1:
fmt.Println("Received:", msg1)
case msg2 := <-ch2:
fmt.Println("Received:", msg2)
case <-time.After(1 * time.Second): // 超时控制
fmt.Println("Timeout")
}
Go的并发调度器走的是M:N模型,核心组件三个:M(Machine)是操作系统线程,由内核调度;P(Processor)是逻辑处理器,绑定一个M,管理本地Goroutine队列(每个P的本地队列最多存256个Goroutine);G(Goroutine)是轻量级协程,存着执行栈和状态。
故事是这样的:工作窃取让空闲的P去别的P的队列里偷Goroutine,平衡负载;系统调用时,Goroutine执行文件IO之类的操作,M会解绑P,让P继续干别的Goroutine,不浪费线程;Go 1.14以后引入了基于信号的抢占式调度,防止长跑的Goroutine独占线程。
实际开发中,这些模式经常组合使用,效率拉满。
用缓冲Channel模拟线程池,限制同时跑的Goroutine数量,防止资源被吃光。比如:
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Printf("Worker %d processing job %d\n", id, j)
results <- j * 2 // 模拟耗时操作
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
// 启动3个Worker
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// 发送9个任务
for j := 1; j <= 9; j++ {
jobs <- j
}
close(jobs) // 关闭任务Channel
// 收集结果
for a := 1; a <= 9; a++ {
<-results
}
}
用多个Channel串联多个处理阶段,每个阶段由独立的Goroutine执行,实现流水线并发。比如:
func stage1(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * 2 // 第一阶段:乘以2
}
close(out)
}()
return out
}
func stage2(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n + 1 // 第二阶段:加1
}
close(out)
}()
return out
}
func main() {
in := make(chan int, 10)
go func() {
for i := 1; i <= 5; i++ {
in <- i
}
close(in)
}()
// 串联两个Stage
out1 := stage1(in)
out2 := stage2(out1)
// 输出最终结果
for n := range out2 {
fmt.Println(n) // 输出:3, 5, 7, 9, 11
}
}
-race标志检测(比如go run -race main.go),通过sync.Mutex或Channel避免共享内存的并发访问。context取消、done Channel通知),别让未释放的资源变成内存黑洞。
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8