文末有源码下载链接
Go语言里面最常用的并发模型有10种,今天我们举例5种,明天再举例剩余的5种。它们比基础的goroutine+channel更高级,更适合工程化,更适合使用在业务里面。
1、工作池模式(Worker Pool)
工作池模式主要用于限制并发数量、防止goroutine爆炸,适合批处理任务、爬虫、消费队列等。我们已麻将游戏的洗牌、发牌为例
//workerpool.go
package workerpool
type Task struct {
// 桌面id
TableID int
// 操作类型
Op string
}
// worker 池处理任务
func worker(id int, tasks <-chan Task, wg *sync.WaitGroup) {
defer wg.Done()
for t := range tasks {
fmt.Printf("工人 %d 正在给 %d 号桌 %s\n", id, t.TableID, t.Op)
// 模拟处理任务
time.Sleep(time.Millisecond * 500)
}
}
//workerpool_test.go
package workerpool
import (
"fmt"
"sync"
"testing"
)
func TestWorker(t *testing.T) {
// 创建任务通道和等待组
taskCh := make(chan Task, 10)
var wg sync.WaitGroup
// worker 数量
numWorkers := 5
// 启动 worker 池
for i := 1; i <= numWorkers; i++ {
wg.Add(1)
go worker(i, taskCh, &wg)
}
// 发送任务到任务通道
for i := 1; i <= 5; i++ {
taskCh <- Task{
TableID: i,
Op: "洗牌",
}
taskCh <- Task{
TableID: i,
Op: "发牌",
}
}
// 关闭任务通道并等待所有 worker 完成
close(taskCh)
wg.Wait()
fmt.Println("所有任务结束")
}
测试结果:
2、流水线模式(Pipeline)
流水线模式主要用来处理阶段任务,每个阶段独立并发,提高吞吐量。我们已麻将的校验出牌合法性检验为例。
//pipeline.go
package pipeline
type Play struct {
// 玩家
Player string
// 牌
Tile string
}
// 生成模拟出牌数据
func genPlays() <-chan Play {
out := make(chan Play)
go func() {
out <- Play{Player: "张三", Tile: "1万"}
out <- Play{Player: "李四", Tile: "2万"}
out <- Play{Player: "王五", Tile: "3万"}
close(out)
}()
return out
}
// 验证阶段
func stageValidate(in <-chan Play) <-chan Play {
out := make(chan Play)
go func() {
for p := range in {
// 简单验证逻辑,加入1万不合法
if p.Tile == "1万" {
fmt.Printf("%s的牌%s非法,已被丢弃\n", p.Player, p.Tile)
continue
}
out <- p
}
close(out)
}()
return out
}
// 合法化阶段
func stageLegalize(in <-chan Play) <-chan Play {
out := make(chan Play)
go func() {
for p := range in {
// 假设条不合法
if p.Tile == `条` {
continue
}
// 简单合法化逻辑
fmt.Printf("%s的牌%s已被合法化检查\n", p.Player, p.Tile)
out <- p
}
close(out)
}()
return out
}
// 广播阶段
func stageBroadcast(in <-chan Play) {
for p := range in {
fmt.Printf("广播阶段:通知所有玩家 %s 出了 %s\n", p.Player, p.Tile)
}
}
//pipeline_test.go
package pipeline
import "testing"
func TestPipeline(t *testing.T) {
//生成打牌动作
plays := genPlays()
validatedPlays := stageValidate(plays)
legalizedPlays := stageLegalize(validatedPlays)
stageBroadcast(legalizedPlays)
}
测试结果:
3、扇出/扇入模式(Fan-out/Fan-in)
扇出/扇入模式适合分发任务后,聚合任务。我们以麻将胡牌倍数的计算为例。
//fanoutfanin.go
package fanoutfanin
type Tile struct {
// 牌名
Name string
// 牌值
No int
}
// 倍数结构体
type Fan struct {
// 倍数名
Name string
// 倍数值
Score int
}
// 胡牌结构体
type Hand struct {
// 牌
Tiles []Tile
// 是否自摸
IsSelfDraw bool
}
// --- Fan-out Worker 阶段---
// 清一色检查
func CheckQingYiSe(hand Hand) <-chan Fan {
out := make(chan Fan)
go func() {
defer close(out)
if len(hand.Tiles) > 0 {
count := 0
for _, tile := range hand.Tiles {
if tile.Name == "万" {
count++
}
}
if count == len(hand.Tiles) {
//假定清一色是6倍
out <- Fan{Name: "清一色", Score: 6}
}
}
}()
return out
}
// 7对检查
func Check7Dui(hand Hand) <-chan Fan {
out := make(chan Fan)
go func() {
defer close(out)
if true {
out <- Fan{Name: "7对", Score: 5}
}
}()
return out
}
// --- Fan-in 聚合阶段---
func mergeFans(in ...<-chan Fan) <-chan Fan {
var wg sync.WaitGroup
out := make(chan Fan)
output := func(c <-chan Fan) {
defer wg.Done()
for f := range c {
out <- f
}
}
wg.Add(len(in))
for _, c := range in {
go output(c)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
package fanoutfanin
import (
"fmt"
"testing"
)
func TestFanoutFanin(t *testing.T) {
hand := Hand{
Tiles: []Tile{
{Name: "万", No: 1},
{Name: "万", No: 2},
{Name: "万", No: 3},
{Name: "万", No: 4},
{Name: "万", No: 5},
{Name: "万", No: 6},
{Name: "万", No: 7},
{Name: "万", No: 7},
{Name: "万", No: 8},
{Name: "万", No: 8},
{Name: "万", No: 8},
{Name: "万", No: 9},
{Name: "万", No: 9},
{Name: "万", No: 9},
},
IsSelfDraw: false,
}
fmt.Printf("1.并行检查所有倍数\n")
c1 := CheckQingYiSe(hand)
c2 := Check7Dui(hand)
fmt.Printf("2.聚合结果\n")
finalCh := mergeFans(c1, c2)
totalScore := 1
foundFans := 0
for fan := range finalCh {
fmt.Printf("发现种类:%s(倍数:%d)\n", fan.Name, fan.Score)
totalScore += fan.Score
foundFans++
}
fmt.Printf("总共发现%d种倍数\n", foundFans)
fmt.Printf("最终的倍数是:%d\n", totalScore)
}
测试结果:
4、演员模型(Actor Model)
每个演员(实体)只通过消息(channel)通信,内部状态私有。我们以麻将房间为例。
//actormodel.go
package actormodel
type RoomMsg any
type Enter struct {
Player string
}
type Exit struct {
Player string
}
type Play struct {
// 玩家
Player string
// 牌
Tile string
}
func roomActor(roomId int, in <-chan RoomMsg) {
fmt.Printf("房间%d开始游戏\n", roomId)
viewers := 0
for msg := range in {
switch m := msg.(type) {
case Enter:
viewers++
fmt.Printf("%s进入了房间\n", m.Player)
case Exit:
viewers--
fmt.Printf("%s离开了房间\n", m.Player)
case Play:
fmt.Printf("%s打了一张牌:%s\n", m.Player, m.Tile)
}
}
fmt.Println("房间", roomId, "结束游戏")
}
//actormodel_test.go
package actormodel
import (
"testing"
"time"
)
func TestActor(t *testing.T) {
in := make(chan RoomMsg, 10)
go roomActor(1, in)
in <- Enter{"张三"}
in <- Enter{"李四"}
in <- Enter{"王五"}
in <- Play{"张三", "1万"}
in <- Exit{"王五"}
time.Sleep(100 * time.Millisecond)
close(in)
time.Sleep(time.Second)
}
测试结果:
5、发布/订阅模式(Pub/Sub)
发布/订阅模式适合事件驱动、消息广播等场景。我们以麻将消息为例。
//pubsub.go
package pubsub
import "sync"
type Broker struct {
subs []chan string
mu sync.Mutex
}
// 订阅消息
func (b *Broker) Sub() chan string {
b.mu.Lock()
defer b.mu.Unlock()
ch := make(chan string, 10)
b.subs = append(b.subs, ch)
return ch
}
// 发布消息
func (b *Broker) Pub(msg string) {
b.mu.Lock()
defer b.mu.Unlock()
for _, s := range b.subs {
select {
case s <- msg:
default:
}
}
}
//pubsub_test.go
package pubsub
import (
"fmt"
"testing"
)
func TestPubsub(t *testing.T) {
b := &Broker{}
s1 := b.Sub()
s2 := b.Sub()
s3 := b.Sub()
go func() {
for m := range s1 {
fmt.Println("第一个订阅者获得消息", m)
}
}()
go func() {
for m := range s2 {
fmt.Println("第2个订阅者获得消息", m)
}
}()
go func() {
for m := range s3 {
fmt.Println("第3个订阅者获得消息", m)
}
}()
b.Pub("张三打了一张牌:1万")
b.Pub("李四碰了张三的一万")
b.Pub("王五托管了游戏")
close(s1)
close(s2)
close(s3)
}
测试结果:
6、源码地址
如果您喜欢这篇文章,请点赞、推荐+分享给更多朋友,万分感谢!