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

文末有源码下载链接!

Go语言里面最常用的并发模型有10种,今天我们继续讲剩下的5种常用并发模式。

1、预先占位结果模式(Future/Promise)

常用于在未来获取某一个结果的场景。例如麻将胡牌后,要计算倍数,这个计算比较耗时,所以就让这个计算放在后面异步去计算,同时要通知客户端播放胡牌的音乐和胡牌的特效。直到倍数计算完成后,再通知客户端显示详细的结果。

bash 复制代码
//futurepromise.go
package futurepromise
import (
    "fmt"
    "time"
)
// 异步计算的最终结果
type FanResult struct {
    // 总倍数
    TotalScore int
    // 详情
    Details string
    Err     error
}
// 结果占位符
type Future chan FanResult
// Get方法:阻塞等待并获取最终结果
func (f Future) Get() (FanResult, error) {
    result := <-f
    return result, result.Err
}
type Tile struct {
    // 牌名
    Name string
    // 牌值
    No int
}
// 胡牌结构体
type Hand struct {
    // 牌
    Tiles []Tile
    // 是否自摸
    IsSelfDraw bool
}
// CalcFanScore 是Promise函数,它接受胡牌信息,启动一个Goroutine异步任务,并立即返回Future占位符
func CalcFanScore(hand Hand) Future {
    resultCh := make(chan FanResult, 1)
    go func() {
        fmt.Println("[Promise] 计算倍数开始...")
        time.Sleep(2 * time.Second)
        totalScore := 11
        details := "清一色(6)倍+7对(5)倍=11倍"
        resultCh <- FanResult{
            TotalScore: totalScore,
            Details:    details,
            Err:        nil,
        }
        fmt.Println("[Promise] 计算倍数结束,并发送结果")
        close(resultCh)
    }()
    // 立即返回Future占位符
    return resultCh
}
bash 复制代码
//futurepromise_test.go
package futurepromise
import (
    "fmt"
    "testing"
    "time"
)
func TestFuturePromise(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.Println("---end---")
    fmt.Println("玩家胡牌了!")
    fanFuture := CalcFanScore(hand)
    fmt.Println("启动异步计算倍数")
    time.Sleep(time.Millisecond * 500)
    fmt.Println("播放玩家胡牌音乐,通知其他玩家XXX胡牌了")
    time.Sleep(time.Millisecond * 500)
    //最后,调用fanFuture.Get来获取最终结果,知道结果计算出来为止
    finalRes, err := fanFuture.Get()
    if err != nil {
        fmt.Printf("倍数计算出错:%v\n", err)
        return
    }
    fmt.Println("倍数计算结果出来了")
    fmt.Println("总倍数是", finalRes.TotalScore)
    fmt.Println("详情:", finalRes.Details)
    fmt.Println("---end---")
}

测试结果:

2、Context可控并发模式

这个模式比较熟悉了,我们用麻将游戏里,超时自动打一张牌来举例。

bash 复制代码
//context.go
package contextdemo
import (
    "context"
    "fmt"
    "time"
)

// 等待玩家打牌
func waitPlayerAction(ctx context.Context, player string) (string, error) {
    select {
    case <-time.After(5 * time.Second): //模拟玩家5秒都没有出牌
        return "", fmt.Errorf("玩家%s5秒内没有任何动作", player)
    case <-ctx.Done():
        return "", ctx.Err()
    }
}
bash 复制代码
//context_test.go
package contextdemo
import (
    "context"
    "fmt"
    "testing"
    "time"
)
func TestContext(t *testing.T) {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    actionCh := make(chan string, 1)
    go func() {
        if act, err := waitPlayerAction(context.Background(), "张三"); err == nil {
            actionCh <- act
        } else {
            //没有动作
        }
    }()
    select {
    case act := <-actionCh:
        fmt.Println("玩家动作", act)
    case <-ctx.Done():
        fmt.Println("超时,自动打一张牌")
    }
}

测试结果:

3、信号量控制并发模式

信号量适用于限制资源使用(比如一台服务器只能并发开多少个麻将房间)。

bash 复制代码
// semaphore.go
package semaphore
const MAX_ROOMS = 10
type RoomManager struct {
    roomSlots   chan struct{}
    activeRooms map[int]bool
    mu          sync.Mutex
    nextRoomID  int
}
func NewRoomManager() *RoomManager {
    return &RoomManager{
        roomSlots:   make(chan struct{}, MAX_ROOMS),
        activeRooms: make(map[int]bool),
        nextRoomID:  100,
    }
}
func (rm *RoomManager) OpenRoom() int {
    fmt.Println("[请求]房间创建请求到达,等待空闲槽位...")
    // 如果roomSlots已满,就在这儿等待
    rm.roomSlots <- struct{}{}
    rm.mu.Lock()
    roomid := rm.nextRoomID
    rm.nextRoomID++
    rm.activeRooms[roomid] = true
    currentLoad := len(rm.roomSlots)
    rm.mu.Unlock()
    fmt.Printf("[创建] 房间%d成功创建。当前房间数是:%d/%d\n", roomid, currentLoad, MAX_ROOMS)
    time.Sleep(time.Duration(roomid%3) * time.Second)
    return roomid
}
func (rm *RoomManager) CloseRoom(roomId int) {
    rm.mu.Lock()
    if !rm.activeRooms[roomId] {
        rm.mu.Unlock()
        return
    }
    delete(rm.activeRooms, roomId)
    rm.mu.Unlock()
    // 释放信号量
    <-rm.roomSlots
    currentLoad := len(rm.roomSlots)
    fmt.Printf("[释放] 房间%d成功释放。当前房间数是:%d/%d\n", roomId, currentLoad, MAX_ROOMS)
}
bash 复制代码
// semaphore_test.go
package semaphore
import (
    "fmt"
    "sync"
    "testing"
    "time"
)
func TestSemaphore(t *testing.T) {
    manager := NewRoomManager()
    var wg sync.WaitGroup
    const totalRequests = 12
    fmt.Printf("服务器最大容量:%d个房间。\n", MAX_ROOMS)
    for i := 0; i < totalRequests; i++ {
        wg.Add(1)
        go func(requestNum int) {
            defer wg.Done()
            roomId := manager.OpenRoom()
            //模拟房间正在打牌耗时1秒
            time.Sleep(time.Second)
            manager.CloseRoom(roomId)
        }(i + 1)
    }
    wg.Wait()
    fmt.Println("所有房间任务已经处理完毕。")
}

测试结果:

4、工作池自动扩容模式

bash 复制代码
//workerpool.go
package dynamicworkerpool
func worker(id int, jobs <-chan int, quit <-chan struct{}, wg *sync.WaitGroup) {
    defer wg.Done()
    for {
        select {
        case j, ok := <-jobs:
            if !ok {
                return
            }
            fmt.Println("工人", id, "处理任务", j)
            time.Sleep(time.Duration(rand.Intn(200)+50) * time.Millisecond)
        case <-quit:
            fmt.Println("工人", id, "收到结束信号")
            return
        }
    }
}
bash 复制代码
//workerpool_test.go
package dynamicworkerpool
func TestDynamicWorkerPool(t *testing.T) {
    jobs := make(chan int, 1000)
    var workerQuits []chan struct{}
    stop := make(chan struct{})
    maxWorkers := 10
    var wg sync.WaitGroup
    // 调度器:根据 jobs 队列长度调整 worker 数量(非常简单的策略)
    go func() {
        workers := 0
        for {
            select {
            case <-stop:
                // 停止调度器时,关闭剩余的 worker quit 通道以确保 worker 退出
                for _, q := range workerQuits {
                    close(q)
                }
                return
            default:
            }
            log.Println("检测jobs长度")
            l := len(jobs)
            target := 1
            switch {
            case l > 20:
                target = 10
            case l > 10:
                target = 3
            case l > 5:
                target = 2
            }
            target = min(target, maxWorkers)
            if target > workers {
                // 启动更多 worker(为每个 worker 创建独立 quit 通道)
                add := target - workers
                for i := 0; i < add; i++ {
                    wg.Add(1)
                    q := make(chan struct{})
                    workerQuits = append(workerQuits, q)
                    go worker(workers+i+1, jobs, q, &wg)
                }
                workers = target
                fmt.Println("扩容 ->", workers, "个工人")
            } else if target < workers {
                // 精确缩容:关闭最近创建的 worker 的 quit 通道,并从列表移除
                for i := 0; i < workers-target; i++ {
                    last := len(workerQuits) - 1
                    if last >= 0 {
                        close(workerQuits[last])
                        workerQuits = workerQuits[:last]
                    }
                }
                workers = target
                fmt.Println("缩容 ->", workers, "个工人")
            }
            time.Sleep(200 * time.Millisecond)
        }
    }()
    // 产生任务(模拟高峰 / 低谷)
    go func() {
        for i := 1; i <= 600; i++ {
            jobs <- i
            time.Sleep(time.Duration(rand.Intn(80)) * time.Millisecond)
        }
        close(jobs)
    }()
    // 等待所有 workers 结束(注意:简化版,这里睡一会儿以等待)
    time.Sleep(6 * time.Second)
    // 停止调度器(它会关闭剩余的 worker quit 通道)
    close(stop)
    wg.Wait()
    fmt.Println("结束")
}

测试结果:

5、并发任务分组模式(ErrGroup)

来自golang.org/x/sync/errgroup,多任务并发执行,自动管理错误和退出。以麻将计算倍数为例,清一色、七对、大对子等。

bash 复制代码
//errgroup.go
package errgroupdemo
import (
    "context"
    "fmt"
    "math/rand"
    "time"
)
func compute(ctx context.Context, name string, delay time.Duration) error {
    select {
    case <-time.After(delay):
        if rand.Intn(10) < 2 {
            fmt.Println(name, "失败")
            return fmt.Errorf("计算%s失败", name)
        }
        fmt.Println("计算", name, "成功")
        return nil
    case <-ctx.Done():
        fmt.Println("计算", name, "取消")
        return ctx.Err()
    }
}
bash 复制代码
//errgroup_test.go
package errgroupdemo
import (
    "context"
    "fmt"
    "testing"
    "time"
    "golang.org/x/sync/errgroup"
)
func TestErrGroup(t *testing.T) {
    ctx := context.Background()
    g, ctx := errgroup.WithContext(ctx)
    methods := []struct {
        name  string
        delay time.Duration
    }{
        {"平胡", 100 * time.Millisecond},
        {"七对", 200 * time.Millisecond},
        {"清一色", 150 * time.Millisecond},
    }
    for _, m := range methods {
        m := m //核心:创建循环变量 m 的局部副本
        g.Go(func() error {
            return compute(ctx, m.name, m.delay)
        })
    }
    if err := g.Wait(); err != nil {
        fmt.Println("有计算失败了:", err)
        return
    }
    fmt.Println("所有计算都成功了")
}

测试结果:

6、源码地址

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


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

相关推荐
怕浪猫9 小时前
第19章:Go语言工具链与工程实践
后端·go·编程语言
tyung1 天前
Go 为什么没成为游戏服务器主流语言
go
F1FJJ1 天前
基于网络隐身的内网穿透
网络协议·网络安全·go
凉凉的知识库1 天前
Go中的零值与空值,你搞懂了么?
分布式·面试·go
Nyarlathotep01132 天前
Go语言http请求过程分析
go
Coding君2 天前
每日一Go-25、Go语言进阶:深入并发模式1
go
X_PENG2 天前
【Golang】Retry重试实践
go
怕浪猫2 天前
第17章:反射与泛型编程——运行时能力与代码复用
后端·go·编程语言
石牌桥网管2 天前
正则表达式:匹配不包含指定字符串的文本
java·javascript·python·正则表达式·go·php