每日一Go-25、Go语言进阶:深入并发模式1

文末有源码下载链接

Go语言里面最常用的并发模型有10种,今天我们举例5种,明天再举例剩余的5种。它们比基础的goroutine+channel更高级,更适合工程化,更适合使用在业务里面。

1、工作池模式(Worker Pool)

工作池模式主要用于限制并发数量、防止goroutine爆炸,适合批处理任务、爬虫、消费队列等。我们已麻将游戏的洗牌、发牌为例

bash 复制代码
//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)
    }
}
bash 复制代码
//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)

流水线模式主要用来处理阶段任务,每个阶段独立并发,提高吞吐量。我们已麻将的校验出牌合法性检验为例。

bash 复制代码
//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)
    }
}
bash 复制代码
//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)

扇出/扇入模式适合分发任务后,聚合任务。我们以麻将胡牌倍数的计算为例。

bash 复制代码
//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
}
bash 复制代码
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)通信,内部状态私有。我们以麻将房间为例。

bash 复制代码
//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, "结束游戏")
}
bash 复制代码
//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)

发布/订阅模式适合事件驱动、消息广播等场景。我们以麻将消息为例。

bash 复制代码
//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:
        }
    }
}
bash 复制代码
//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、源码地址

pan.baidu.com/s/1B6pgLWfS...


如果您喜欢这篇文章,请点赞、推荐+分享给更多朋友,万分感谢!

相关推荐
X_PENG10 小时前
【Golang】Retry重试实践
go
怕浪猫12 小时前
第17章:反射与泛型编程——运行时能力与代码复用
后端·go·编程语言
石牌桥网管12 小时前
正则表达式:匹配不包含指定字符串的文本
java·javascript·python·正则表达式·go·php
2301_816997881 天前
Go语言基础语法
go
Nyarlathotep01131 天前
Go结构体字段定义
go
2301_816997881 天前
Go语言开发环境搭建
go
2301_816997881 天前
Go语言简介
golang·go
KeithChu2 天前
Go 语言中的 slice 类型
go