文末有源码下载链接!
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、源码地址
如果您喜欢这篇文章,请点赞、推荐+分享给更多朋友,万分感谢!