Go语言并发编程的深度探索:goroutines与Channels的奇妙世界
在编程的浩瀚宇宙中,Go语言以其简洁的语法、强大的并发模型以及高效的性能,在众多编程语言中脱颖而出。Go语言的并发编程模型,尤其是goroutines和Channels的设计,为开发者提供了一种既直观又高效的并发编程方式。今天,我们就来深入探索Go语言的并发编程精髓,揭开goroutines与Channels的神秘面纱。
一、Go语言并发编程基础
1.1 并发与并行的区别
在讨论Go的并发编程之前,我们需要先澄清两个概念:并发(Concurrency)与并行(Parallelism)。并发指的是程序中的多个任务可以在同一时间段内交错执行,而并行则是指这些任务在同一时刻真正的同时执行。Go语言的goroutines和Channels主要解决的是并发问题,但得益于Go的运行时(runtime)和调度器(scheduler),它们也能在多核处理器上实现高效的并行执行。
1.2 Goroutines:轻量级的线程
Goroutines是Go语言的核心特性之一,它们是由Go的运行时管理的轻量级线程。与传统的线程或进程相比,goroutines的创建和切换成本极低,这使得在Go中编写高并发的程序变得异常简单。你只需使用go关键字在函数调用前,就可以轻松地启动一个新的goroutine。
go func() {
// 这里是goroutine执行的代码
}()
二、Channels:goroutines之间的通信桥梁
2.1 Channels简介
Channels是Go语言中goroutines之间通信的主要机制。你可以将Channels想象成goroutines之间的管道,通过它们可以安全地在不同的goroutines之间传递数据。Channels的使用避免了使用共享内存进行通信时可能出现的竞态条件(race conditions)和数据竞争(data races)问题。
2.2 Channels的基本操作
- 发送数据:使用
<-操作符和channel变量,可以将数据发送到channel中。如果channel已满(对于无缓冲的channel),发送操作将阻塞,直到有goroutine从channel中接收数据。
ch <- value
- 接收数据:同样使用
<-操作符,但位于channel变量的左侧,用于从channel中接收数据。如果channel为空(对于无缓冲的channel),接收操作将阻塞,直到有goroutine向channel中发送数据。
value := <-ch
2.3 缓冲Channels与无缓冲Channels
-
无缓冲Channels:在无缓冲的Channels中,发送操作会阻塞,直到另一个goroutine准备好接收数据。接收操作也会阻塞,直到有数据到来。
-
缓冲Channels:你可以为Channels指定一个缓冲区大小,在缓冲区未满时,发送操作不会阻塞。同样,在缓冲区非空时,接收操作也不会阻塞。
ch := make(chan int, 10) // 创建一个缓冲大小为10的int类型channel
三、进阶应用:使用goroutines与Channels解决并发问题
3.1 返回单向接收通道做为函数返回结果
package main
import (
"fmt"
"math/rand"
"time"
)
func demo() <-chan int {
r := make(chan int)
go func() {
time.Sleep(time.Second * 3) // 模拟实际业务
r <- rand.Intn(100)
}()
return r
}
func sum(a, b int) int {
return a*a + b*b
}
func main() {
rand.Seed(time.Now().UnixNano()) // Go 1.20之前需要
a, b := demo(), demo()
fmt.Println(sum(<-a, <-b))
}
3.2 将单向发送通道类型用做函数实参
package main
import (
"fmt"
"math/rand"
"time"
)
func demo(r chan<- int) {
time.Sleep(time.Second * 3) // 模拟业务
r <- rand.Intn(100)
}
func sum(a, b int) int {
return a*a + b*b
}
func main() {
rand.Seed(time.Now().UnixNano()) // Go 1.20之前需要
ra, rb := make(chan int, 1), make(chan int, 1)
go demo(ra)
go demo(rb)
fmt.Println(sum(<-ra, <-rb))
}
3.3 使用通道实现通知
3.3.1 单对单通知
package main
import (
"crypto/rand"
"fmt"
"os"
"sort"
)
func main() {
values := make([]byte, 32 * 1024 * 1024)
if _, err := rand.Read(values); err != nil {
fmt.Println(err)
os.Exit(1)
}
done := make(chan struct{}) // 也可以是缓冲的
// 排序协程
go func() {
sort.Slice(values, func(i, j int) bool {
return values[i] < values[j]
})
done <- struct{}{} // 通知排序已完成
}()
// 并发地做一些其它事情...
<- done // 等待通知
fmt.Println(values[0], values[len(values)-1])
}
3.3.2 单对多通知
package main
import "log"
import "time"
type T = struct{}
func worker(id int, ready <-chan T, done chan<- T) {
<-ready // 阻塞在此,等待通知
log.Print("Worker#", id, "开始工作")
// 模拟一个工作负载。
time.Sleep(time.Second * time.Duration(id+1))
log.Print("Worker#", id, "工作完成")
done <- T{} // 通知主协程(N-to-1)
}
func main() {
log.SetFlags(0)
ready, done := make(chan T), make(chan T)
go worker(0, ready, done)
go worker(1, ready, done)
go worker(2, ready, done)
// 模拟一个初始化过程
time.Sleep(time.Second * 3 / 2)
// 单对多通知
close(ready)
// 等待被多对单通知
<-done
<-done
<-done
}
3.4 互斥锁
package main
import "fmt"
func main() {
mutex := make(chan struct{}, 1) // 容量必须为1
counter := 0
increase := func() {
mutex <- struct{}{} // 加锁
counter++
<-mutex // 解锁
}
increase1000 := func(done chan<- struct{}) {
for i := 0; i < 1000; i++ {
increase()
}
done <- struct{}{}
}
done := make(chan struct{})
go increase1000(done)
go increase1000(done)
<-done; <-done
fmt.Println(counter) // 2000
}
3.5 信号量
package main
import (
"log"
"time"
"math/rand"
)
type Seat int
type Bar chan Seat
func (bar Bar) ServeCustomer(c int) {
log.Print("顾客#", c, "进入酒吧")
seat := <- bar // 需要一个位子来喝酒
log.Print("++ customer#", c, " drinks at seat#", seat)
log.Print("++ 顾客#", c, "在第", seat, "个座位开始饮酒")
time.Sleep(time.Second * time.Duration(2 + rand.Intn(6)))
log.Print("-- 顾客#", c, "离开了第", seat, "个座位")
bar <- seat // 释放座位,离开酒吧
}
func main() {
rand.Seed(time.Now().UnixNano()) // Go 1.20之前需要
bar24x7 := make(Bar, 10) // 此酒吧有10个座位
// 摆放10个座位。
for seatId := 0; seatId < cap(bar24x7); seatId++ {
bar24x7 <- Seat(seatId) // 均不会阻塞
}
for customerId := 0; ; customerId++ {
time.Sleep(time.Second)
go bar24x7.ServeCustomer(customerId)
}
for {time.Sleep(time.Second)} // 睡眠不属于阻塞状态
}
四、总结
以上就是Go语言的并发编程模型中goroutines跟channel的使用教程。有其他用途可以在评论区留言。