📚 原创系列: “Go语言学习系列”
🔄 转载说明: 本文最初发布于"Gopher部落"微信公众号,经原作者授权转载。
🔗 关注原创: 欢迎扫描文末二维码,关注"Gopher部落"微信公众号获取第一手Go技术文章。
📑 Go语言学习系列导航
🚀 第三阶段:进阶篇本文是【Go语言学习系列】的第32篇,当前位于第三阶段(进阶篇)
- 并发编程(一):goroutine基础
- 并发编程(二):channel基础
- 并发编程(三):select语句
- 并发编程(四):sync包
- 并发编程(五):并发模式 👈 当前位置
- 并发编程(六):原子操作与内存模型
- 数据库编程(一):SQL接口
- 数据库编程(二):ORM技术
- Web开发(一):路由与中间件
- Web开发(二):模板与静态资源
- Web开发(三):API开发
- Web开发(四):认证与授权
- Web开发(五):WebSocket
- 微服务(一):基础概念
- 微服务(二):gRPC入门
- 日志与监控
- 第三阶段项目实战:微服务聊天应用
📖 文章导读
在前面四篇文章中,我们深入学习了Go语言的并发原语:goroutine、channel、select语句以及sync包。今天,我们将探讨如何将这些组件组合起来,形成强大且可复用的并发模式。这些模式可以帮助我们更有效地解决各种并发编程问题。
一、什么是并发模式
并发模式是在并发环境中解决特定问题的常见结构和方法。就像设计模式帮助我们组织代码一样,并发模式帮助我们组织并发逻辑,使其更易于理解、测试和维护。
好的并发模式应该具备以下特点:
- 清晰的责任边界
- 良好的错误处理
- 可控的资源使用
- 优雅的终止机制
掌握并发模式有以下好处:
- 提高代码质量和可维护性
- 减少并发错误(如死锁、竞态条件)
- 提高性能和资源利用率
- 简化复杂并发逻辑的实现
- 使代码更加模块化和可重用
二、生产者-消费者模式
生产者-消费者是最基本也是最常用的并发模式之一。它将"生产数据"和"消费数据"的过程解耦,通过channel在两者之间传递数据。
2.1 基本实现
package main
import (
"fmt"
"sync"
"time"
)
func producer(jobs chan<- int) {
defer close(jobs)
for i := 1; i <= 5; i++ {
fmt.Printf("生产任务: %d\n", i)
jobs <- i
time.Sleep(time.Millisecond * 500)
}
}
func consumer(id int, jobs <-chan int, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
fmt.Printf("消费者 %d 处理任务: %d\n", id, job)
time.Sleep(time.Second) // 模拟处理时间
}
}
func main() {
jobs := make(chan int, 3)
var wg sync.WaitGroup
// 启动一个生产者
go producer(jobs)
// 启动两个消费者
for i := 1; i <= 2; i++ {
wg.Add(1)
go consumer(i, jobs, &wg)
}
wg.Wait()
fmt.Println("所有工作完成")
}
这个例子展示了生产者-消费者模式的基本用法:
- 生产者生成任务并发送到channel
- 多个消费者从channel接收任务并处理
- 生产者完成后关闭channel
- 主goroutine等待所有消费者完成
2.2 带错误处理的生产者-消费者
现实应用中,我们通常需要处理错误:
package main
import (
"errors"
"fmt"
"sync"
"time"
)
type Job struct {
ID int
Err error
}
type Result struct {
JobID int
Value string
Err error
}
func producer(jobs chan<- Job) {
defer close(jobs)
for i := 1; i <= 5; i++ {
var err error
if i == 3 {
err = errors.New("模拟生产错误")
}
jobs <- Job{ID: i, Err: err}
time.Sleep(time.Millisecond * 500)
}
}
func consumer(jobs <-chan Job, results chan<- Result, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
if job.Err != nil {
results <- Result{JobID: job.ID, Err: job.Err}
continue
}
// 模拟处理
time.Sleep(time.Second)
var err error
if job.ID == 4 {
err = errors.New("模拟处理错误")
}
result := Result{
JobID: job.ID,
Value: fmt.Sprintf("处理结果 %d", job.ID),
Err: err,
}
results <- result
}
}
func main() {
jobs := make(chan Job, 5)
results := make(chan Result, 5)
var wg sync.WaitGroup
// 启动生产者
go producer(jobs)
// 启动消费者
for i := 1; i <= 3; i++ {
wg.Add(1)
go consumer(jobs, results, &wg)
}
// 收集结果
go func() {
wg.Wait()
close(results)
}()
// 处理结果
for result := range results {
if result.Err != nil {
fmt.Printf("任务 %d 出错: %v\n", result.JobID, result.Err)
continue
}
fmt.Printf("任务 %d 完成: %s\n", result.JobID, result.Value)
}
}
这个增强版实现了以下改进:
- 使用结构体传递任务和结果,包含错误信息
- 在消费者中处理生产者可能产生的错误
- 消费者也可能产生错误,并传递给结果收集者
- 主goroutine处理并整理所有结果
2.3 实际应用场景
生产者-消费者模式在很多场景下非常有用:
- Web服务器处理请求:传入的HTTP请求作为生产者,工作池作为消费者
- 数据处理管道:从数据源读取数据(生产者),经过多阶段处理(消费者)
- 任务队列:应用程序生成后台任务(生产者),工作者处理任务(消费者)
- 日志处理:应用程序生成日志事件(生产者),日志处理器写入存储(消费者)
三、工作池模式 (Worker Pool)
工作池模式是生产者-消费者模式的扩展,它维护一组工作者(goroutine)来处理一系列任务。与简单的生产者-消费者模式相比,工作池模式更强调工作者的管理和任务的分发。
3.1 基本工作池
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
fmt.Printf("工作者 %d 开始处理任务 %d\n", id, job)
time.Sleep(time.Second) // 模拟工作耗时
fmt.Printf("工作者 %d 完成任务 %d\n", id, job)
results <- job * 2
}
}
func main() {
const numJobs = 10
const numWorkers = 3
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
var wg sync.WaitGroup
// 启动工作者
for w := 1; w <= numWorkers; w++ {
wg.Add(1)
go worker(w, jobs, results, &wg)
}
// 发送任务
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs)
// 等待所有工作者完成
go func() {
wg.Wait()
close(results)
}()
// 收集结果
for result := range results {
fmt.Printf("结果: %d\n", result)
}
}
基本工作池的特点:
- 预先创建固定数量的工作者
- 所有工作者共享同一个任务队列
- 任务完成后结果发送到结果队列
- 主程序等待所有工作者完成并收集结果
3.2 可限流的工作池
在实际应用中,我们可能需要限制并发数量或请求速率:
package main
import (
"fmt"
"sync"
"time"
)
type RateLimiter struct {
interval time.Duration
ticker *time.Ticker
stopCh chan struct{}
}
func NewRateLimiter(rps int) *RateLimiter {
interval := time.Second / time.Duration(rps)
return &RateLimiter{
interval: interval,
ticker: time.NewTicker(interval),
stopCh: make(chan struct{}),
}
}
func (rl *RateLimiter) Allow() bool {
select {
case <-rl.ticker.C:
return true
case <-rl.stopCh:
return false
default:
return false
}
}
func (rl *RateLimiter) Stop() {
rl.ticker.Stop()
close(rl.stopCh)
}
func processRequest(id int, limiter *RateLimiter, wg *sync.WaitGroup) {
defer wg.Done()
start := time.Now()
// 等待限流器允许
for !limiter.Allow() {
time.Sleep(time.Millisecond * 10)
}
// 模拟处理请求
time.Sleep(time.Millisecond * 50)
fmt.Printf("请求 %d 处理完成,等待时间: %v\n", id, time.Since(start))
}
func main() {
// 每秒5个请求
limiter := NewRateLimiter(5)
defer limiter.Stop()
var wg sync.WaitGroup
// 模拟20个并发请求
for i := 1; i <= 20; i++ {
wg.Add(1)
go processRequest(i, limiter, &wg)
}
wg.Wait()
fmt.Println("所有请求处理完成")
}
限流工作池的关键功能:
- 通过令牌桶或其他限流算法控制请求速率
- 过载时请求可以等待或被拒绝
- 防止系统资源过度使用
- 保护下游服务免受突发流量影响
3.3 工作池的实际应用
工作池模式在以下场景特别有用:
- API服务器:控制对数据库或第三方服务的请求速率
- 批量数据处理:处理大量数据时限制内存使用
- 爬虫系统:控制爬取速率,避免对目标站点造成压力
- 任务调度系统:管理和分配计算密集型任务
- 微服务通信:限制服务间的调用流量
四、管道模式 (Pipeline)
管道模式将数据处理分成多个阶段,每个阶段通过channel连接起来。这种模式特别适合处理数据流,每个阶段都可以并发执行。
4.1 基本管道
package main
import (
"fmt"
)
// 生成器:生成整数
func generator(nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
out <- n
}
}()
return out
}
// 平方:计算输入值的平方
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
out <- n * n
}
}()
return out
}
// 过滤:只保留奇数
func onlyOdd(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
if n%2 != 0 {
out <- n
}
}
}()
return out
}
func main() {
// 构建管道
nums := generator(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
squares := square(nums)
odds := onlyOdd(squares)
// 消费结果
for odd := range odds {
fmt.Println(odd)
}
}
基本管道模式的特点:
- 每个阶段是一个函数,接收上一阶段的输出作为输入
- 每个阶段在单独的goroutine中运行
- 阶段间通过channel传递数据
- 当输入channel关闭且处理完所有数据时,该阶段关闭其输出channel
4.2 带错误处理的管道
实际应用中,我们需要处理每个管道阶段可能出现的错误:
package main
import (
"errors"
"fmt"
)
type Result struct {
Value int
Err error
}
func generator(nums ...int) <-chan Result {
out := make(chan Result)
go func() {
defer close(out)
for _, n := range nums {
if n < 0 {
out <- Result{Err: errors.New("负数不被允许")}
return
}
out <- Result{Value: n}
}
}()
return out
}
func square(in <-chan Result) <-chan Result {
out := make(chan Result)
go func() {
defer close(out)
for res := range in {
if res.Err != nil {
out <- res // 传递错误
continue
}
out <- Result{Value: res.Value * res.Value}
}
}()
return out
}
func main() {
// 构建管道
values := generator(1, 2, 3, -4, 5)
squares := square(values)
// 消费结果
for res := range squares {
if res.Err != nil {
fmt.Printf("错误: %v\n", res.Err)
continue
}
fmt.Println(res.Value)
}
}
带错误处理的管道特点:
- 每个结果包含值和可能的错误
- 一旦发生错误,错误会沿管道传播
- 管道后期阶段可以选择如何处理错误(传递、处理或停止)
- 主函数可以收集处理所有错误
4.3 管道模式的优势与实际应用
管道模式的主要优势:
- 模块化:每个处理阶段都是独立的,易于测试和维护
- 并发处理:各阶段可以并行执行,提高吞吐量
- 解耦:数据生产和消费解耦,提高代码可读性
- 灵活组合:可以根据需要动态组合不同处理阶段
实际应用场景:
- ETL处理:数据提取、转换和加载过程
- 图像处理:图像加载、缩放、滤镜应用等多步骤处理
- 数据分析:数据读取、清洗、聚合、分析的流水线
- 实时数据流处理:日志分析、事件处理等
五、扇入扇出模式 (Fan-in/Fan-out)
扇出是将任务分配给多个worker并行处理,扇入是将多个结果汇总到一个channel。这种模式适合处理可以并行的任务,然后需要汇总结果的场景。
5.1 基本实现
package main
import (
"fmt"
"sync"
)
// 扇出:将一个输入拆分为多个并行处理
func fanOut(in <-chan int, numWorkers int) []<-chan int {
outputs := make([]<-chan int, numWorkers)
for i := 0; i < numWorkers; i++ {
outputs[i] = processWorker(in, i)
}
return outputs
}
// 扇入:将多个输入合并到一个输出
func fanIn(inputs []<-chan int) <-chan int {
var wg sync.WaitGroup
out := make(chan int)
// 为每个输入启动一个goroutine
for _, input := range inputs {
wg.Add(1)
go func(ch <-chan int) {
defer wg.Done()
for val := range ch {
out <- val
}
}(input)
}
// 当所有输入处理完毕后关闭输出
go func() {
wg.Wait()
close(out)
}()
return out
}
// 工作者:处理输入并产生输出
func processWorker(in <-chan int, workerID int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for val := range in {
// 模拟处理:计算平方
result := val * val
fmt.Printf("工作者 %d 处理 %d -> %d\n", workerID, val, result)
out <- result
}
}()
return out
}
func main() {
// 创建输入
input := make(chan int)
go func() {
defer close(input)
for i := 1; i <= 10; i++ {
input <- i
}
}()
// 扇出:分配给3个工作者
workers := fanOut(input, 3)
// 扇入:汇总所有结果
results := fanIn(workers)
// 处理最终结果
var sum int
for res := range results {
sum += res
}
fmt.Printf("所有结果的总和: %d\n", sum)
}
扇入扇出模式的关键组件:
- 扇出:将一个任务分配给多个worker并行处理
- worker:每个worker独立处理分配给它的任务
- 扇入:将多个worker的结果合并到一个channel
5.2 应用场景
扇入扇出模式在以下场景特别有用:
- 并行计算:将大型计算任务拆分为多个子任务并行处理
- 数据聚合:从多个源收集数据并合并结果
- 并行API请求:同时发起多个API请求,然后合并响应
- 分布式系统:分散工作负载,然后聚合结果
六、超时与取消模式
在并发程序中,我们常常需要处理超时和取消操作。Go的context包和select语句提供了优雅的方式来实现这些功能。
6.1 超时与上下文取消
package main
import (
"context"
"fmt"
"sync"
"time"
)
// 一个可能耗时的操作
func slowOperation(ctx context.Context) (string, error) {
select {
case <-time.After(2 * time.Second):
return "操作完成", nil
case <-ctx.Done():
return "", ctx.Err()
}
}
// 使用超时控制
func withTimeout() {
// 创建一个带1秒超时的上下文
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel() // 确保取消函数被调用
result, err := slowOperation(ctx)
if err != nil {
fmt.Printf("超时示例出错: %v\n", err)
return
}
fmt.Printf("超时示例结果: %s\n", result)
}
// 使用手动取消
func withCancellation() {
ctx, cancel := context.WithCancel(context.Background())
// 启动一个goroutine在1秒后取消
time.AfterFunc(1*time.Second, cancel)
result, err := slowOperation(ctx)
if err != nil {
fmt.Printf("取消示例出错: %v\n", err)
return
}
fmt.Printf("取消示例结果: %s\n", result)
}
// 使用上下文控制多个goroutine
func workerWithContext(ctx context.Context, id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("工作者 %d 启动\n", id)
select {
case <-time.After(3 * time.Second):
fmt.Printf("工作者 %d 完成工作\n", id)
case <-ctx.Done():
fmt.Printf("工作者 %d 接收到取消信号: %v\n", id, ctx.Err())
}
}
func multipleWorkers() {
// 创建一个2秒后自动取消的上下文
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
var wg sync.WaitGroup
// 启动多个工作者
for i := 1; i <= 3; i++ {
wg.Add(1)
go workerWithContext(ctx, i, &wg)
}
// 等待工作者完成或超时
wg.Wait()
fmt.Println("所有工作者已退出")
}
func main() {
fmt.Println("-- 超时示例 --")
withTimeout()
fmt.Println("\n-- 取消示例 --")
withCancellation()
fmt.Println("\n-- 多工作者示例 --")
multipleWorkers()
}
Context的核心功能:
- 超时控制:
context.WithTimeout
和context.WithDeadline
- 手动取消:
context.WithCancel
- 传递值:
context.WithValue
- 优雅取消:通过
Done()
channel通知取消事件
6.2 超时重试模式
在网络请求等不稳定操作中,重试是常见需求:
package main
import (
"context"
"errors"
"fmt"
"math/rand"
"time"
)
// 模拟可能失败的操作
func doOperation(ctx context.Context) (string, error) {
// 模拟随机失败
if rand.Float32() < 0.7 {
return "", errors.New("操作失败")
}
select {
case <-time.After(500 * time.Millisecond):
return "操作成功", nil
case <-ctx.Done():
return "", ctx.Err()
}
}
// 带重试的操作
func doOperationWithRetry(maxRetries int, timeout time.Duration) (string, error) {
var lastErr error
for retry := 0; retry < maxRetries; retry++ {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
result, err := doOperation(ctx)
cancel() // 及时释放资源
if err == nil {
return result, nil
}
lastErr = err
fmt.Printf("尝试 %d 失败: %v,重试中...\n", retry+1, err)
time.Sleep(time.Millisecond * 200 * time.Duration(retry+1)) // 退避策略
}
return "", fmt.Errorf("在 %d 次尝试后失败: %w", maxRetries, lastErr)
}
func main() {
rand.Seed(time.Now().UnixNano())
result, err := doOperationWithRetry(5, 800*time.Millisecond)
if err != nil {
fmt.Printf("最终错误: %v\n", err)
return
}
fmt.Printf("最终结果: %s\n", result)
}
重试模式的关键点:
- 指数退避:连续失败后增加重试间隔
- 最大重试次数:防止无限重试
- 超时控制:单次操作不应无限等待
- 错误处理:保留最后的错误信息供调试
6.3 超时与取消的实际应用
这些模式在以下场景尤为重要:
- HTTP客户端:防止请求无限等待
- 数据库操作:控制查询执行时间
- 分布式系统:优雅处理服务不可用
- 长时间运行的操作:提供用户取消能力
- 资源清理:确保临时资源被正确释放
七、并发控制模式
有时我们需要精确控制并发数量或并发操作。这在资源受限的环境中尤为重要。
7.1 信号量模式
使用带缓冲的channel实现信号量:
package main
import (
"fmt"
"sync"
"time"
)
// 信号量实现
type Semaphore struct {
tokens chan struct{}
}
func NewSemaphore(limit int) *Semaphore {
return &Semaphore{
tokens: make(chan struct{}, limit),
}
}
func (s *Semaphore) Acquire() {
s.tokens <- struct{}{}
}
func (s *Semaphore) Release() {
<-s.tokens
}
// 并发执行但限制最大并发数
func processWithLimit(items []int, concurrency int) {
sem := NewSemaphore(concurrency)
var wg sync.WaitGroup
for i, item := range items {
wg.Add(1)
// 获取令牌
sem.Acquire()
go func(id, val int) {
defer wg.Done()
defer sem.Release() // 释放令牌
// 模拟处理
fmt.Printf("处理项目 %d: %d 开始\n", id, val)
time.Sleep(time.Second)
fmt.Printf("处理项目 %d: %d 完成\n", id, val)
}(i, item)
}
wg.Wait()
}
func main() {
items := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
fmt.Println("限制最大3个并发:")
processWithLimit(items, 3)
}
信号量模式的特点:
- 使用带缓冲的channel控制并发数量
- 每次操作前获取令牌,操作后释放令牌
- 可应用于任何需要限制并发的场景
- 简单高效,符合Go的设计理念
7.2 并发与串行结合
有些应用需要在某些阶段并发,而在另一些阶段串行执行:
package main
import (
"fmt"
"sync"
"time"
)
// 串行阶段
func prepare(data []int) []int {
fmt.Println("准备阶段(串行)开始")
result := make([]int, len(data))
for i, val := range data {
result[i] = val * 2
time.Sleep(100 * time.Millisecond) // 模拟处理
}
fmt.Println("准备阶段(串行)完成")
return result
}
// 并行阶段
func process(data []int) []int {
fmt.Println("处理阶段(并行)开始")
result := make([]int, len(data))
var wg sync.WaitGroup
for i, val := range data {
wg.Add(1)
go func(idx, value int) {
defer wg.Done()
// 模拟复杂处理
time.Sleep(500 * time.Millisecond)
result[idx] = value * value
fmt.Printf("处理项目 %d 完成\n", idx)
}(i, val)
}
wg.Wait()
fmt.Println("处理阶段(并行)完成")
return result
}
// 串行阶段
func finalize(data []int) int {
fmt.Println("最终阶段(串行)开始")
sum := 0
for _, val := range data {
sum += val
time.Sleep(100 * time.Millisecond) // 模拟处理
}
fmt.Println("最终阶段(串行)完成")
return sum
}
func main() {
input := []int{1, 2, 3, 4, 5}
// 串行-并行-串行处理流程
prepared := prepare(input) // 串行
processed := process(prepared) // 并行
result := finalize(processed) // 串行
fmt.Printf("最终结果: %d\n", result)
}
这种模式适用于:
- 数据处理前需要预处理的场景
- 并行处理后需要聚合结果的场景
- 某些阶段有依赖关系,无法并行的场景
八、实战示例:异步任务系统
下面是一个更复杂的示例,结合了多种并发模式,实现一个完整的异步任务系统:
package main
import (
"context"
"fmt"
"math/rand"
"sync"
"time"
)
// 任务状态
type TaskStatus string
const (
StatusPending TaskStatus = "pending"
StatusRunning TaskStatus = "running"
StatusCompleted TaskStatus = "completed"
StatusFailed TaskStatus = "failed"
StatusCancelled TaskStatus = "cancelled"
)
// 任务定义
type Task struct {
ID string
Description string
Status TaskStatus
Result string
Error error
CreatedAt time.Time
CompletedAt time.Time
}
// 任务系统
type TaskSystem struct {
tasks map[string]*Task
queue chan string
semaphore *Semaphore
maxRetries int
mu sync.Mutex
wg sync.WaitGroup
ctx context.Context
cancel context.CancelFunc
}
// 创建新的任务系统
func NewTaskSystem(concurrency, queueSize, maxRetries int) *TaskSystem {
ctx, cancel := context.WithCancel(context.Background())
return &TaskSystem{
tasks: make(map[string]*Task),
queue: make(chan string, queueSize),
semaphore: NewSemaphore(concurrency),
maxRetries: maxRetries,
ctx: ctx,
cancel: cancel,
}
}
// 添加任务
func (ts *TaskSystem) AddTask(description string) string {
ts.mu.Lock()
defer ts.mu.Unlock()
id := fmt.Sprintf("task-%d", len(ts.tasks)+1)
task := &Task{
ID: id,
Description: description,
Status: StatusPending,
CreatedAt: time.Now(),
}
ts.tasks[id] = task
// 将任务ID放入队列
select {
case ts.queue <- id:
fmt.Printf("任务 %s 已加入队列\n", id)
default:
task.Status = StatusFailed
task.Error = fmt.Errorf("队列已满,无法添加任务")
fmt.Printf("任务 %s 无法加入队列: %v\n", id, task.Error)
}
return id
}
// 获取任务状态
func (ts *TaskSystem) GetTask(id string) (Task, bool) {
ts.mu.Lock()
defer ts.mu.Unlock()
task, exists := ts.tasks[id]
if !exists {
return Task{}, false
}
return *task, true
}
// 执行单个任务
func (ts *TaskSystem) executeTask(taskID string) {
ts.mu.Lock()
task, exists := ts.tasks[taskID]
if !exists {
ts.mu.Unlock()
return
}
task.Status = StatusRunning
ts.mu.Unlock()
fmt.Printf("开始执行任务 %s: %s\n", task.ID, task.Description)
// 模拟工作
success := false
var err error
for retry := 0; retry < ts.maxRetries; retry++ {
// 检查是否被取消
select {
case <-ts.ctx.Done():
ts.mu.Lock()
task.Status = StatusCancelled
task.Error = fmt.Errorf("任务系统关闭")
task.CompletedAt = time.Now()
ts.mu.Unlock()
return
default:
// 继续执行
}
// 模拟随机执行时间和成功/失败
time.Sleep(time.Millisecond * time.Duration(500+rand.Intn(1000)))
if rand.Float32() < 0.6 { // 60%成功率
success = true
break
}
err = fmt.Errorf("随机失败 (尝试 %d/%d)", retry+1, ts.maxRetries)
fmt.Printf("任务 %s 尝试 %d 失败: %v\n", task.ID, retry+1, err)
// 退避策略
time.Sleep(time.Millisecond * 200 * time.Duration(retry+1))
}
ts.mu.Lock()
defer ts.mu.Unlock()
task.CompletedAt = time.Now()
if success {
task.Status = StatusCompleted
task.Result = fmt.Sprintf("任务 %s 完成,耗时: %v", task.ID, task.CompletedAt.Sub(task.CreatedAt))
fmt.Printf("任务 %s 成功: %s\n", task.ID, task.Result)
} else {
task.Status = StatusFailed
task.Error = err
fmt.Printf("任务 %s 最终失败: %v\n", task.ID, err)
}
}
// 启动工作者
func (ts *TaskSystem) startWorker(id int) {
defer ts.wg.Done()
fmt.Printf("工作者 %d 已启动\n", id)
for {
select {
case <-ts.ctx.Done():
fmt.Printf("工作者 %d 关闭\n", id)
return
case taskID, ok := <-ts.queue:
if !ok {
// 队列已关闭
fmt.Printf("工作者 %d 发现队列已关闭\n", id)
return
}
// 获取信号量
ts.semaphore.Acquire()
// 执行任务
fmt.Printf("工作者 %d 接收任务 %s\n", id, taskID)
ts.executeTask(taskID)
// 释放信号量
ts.semaphore.Release()
}
}
}
// 启动任务系统
func (ts *TaskSystem) Start(numWorkers int) {
for i := 1; i <= numWorkers; i++ {
ts.wg.Add(1)
go ts.startWorker(i)
}
fmt.Printf("任务系统已启动,使用 %d 个工作者\n", numWorkers)
}
// 停止任务系统
func (ts *TaskSystem) Stop() {
fmt.Println("正在停止任务系统...")
ts.cancel()
close(ts.queue)
ts.wg.Wait()
fmt.Println("任务系统已停止")
}
// 信号量实现
type Semaphore struct {
tokens chan struct{}
}
func NewSemaphore(limit int) *Semaphore {
return &Semaphore{
tokens: make(chan struct{}, limit),
}
}
func (s *Semaphore) Acquire() {
s.tokens <- struct{}{}
}
func (s *Semaphore) Release() {
<-s.tokens
}
func main() {
rand.Seed(time.Now().UnixNano())
// 创建任务系统:最大3个并发,队列容量10,最大重试3次
system := NewTaskSystem(3, 10, 3)
// 启动5个工作者
system.Start(5)
// 添加一些任务
for i := 1; i <= 15; i++ {
id := system.AddTask(fmt.Sprintf("任务 %d", i))
fmt.Printf("添加了任务: %s\n", id)
time.Sleep(time.Millisecond * 200)
}
// 等待一段时间处理任务
time.Sleep(10 * time.Second)
// 停止任务系统
system.Stop()
// 打印最终状态
fmt.Println("\n最终任务状态:")
for i := 1; i <= 15; i++ {
id := fmt.Sprintf("task-%d", i)
task, exists := system.GetTask(id)
if exists {
fmt.Printf("- %s: %s, 状态: %s\n", task.ID, task.Description, task.Status)
if task.Error != nil {
fmt.Printf(" 错误: %v\n", task.Error)
}
if task.Result != "" {
fmt.Printf(" 结果: %s\n", task.Result)
}
}
}
}
这个异步任务系统结合了以下并发模式:
- 生产者-消费者模式:添加任务和处理任务
- 工作池模式:多个工作者处理任务队列
- 信号量模式:限制并发执行任务数量
- 超时和重试模式:任务执行失败后重试
- 上下文取消模式:优雅关闭整个系统
九、总结
本文介绍了Go语言中常用的并发模式:
- 生产者-消费者模式:用于任务分发和处理
- 工作池模式:管理和控制一组工作者
- 管道模式:将数据处理分成多个阶段
- 扇入扇出模式:并行处理数据并汇总结果
- 超时与取消模式:处理超时和取消操作
- 并发控制模式:限制并发数量
- 异步任务系统:结合多种模式的综合应用
掌握这些模式,再结合goroutine、channel、select语句和sync包,可以帮助我们设计和实现高效、健壮的并发系统。
最佳实践
最后,总结一些使用并发模式的最佳实践:
- 明确责任:每个goroutine应有明确的责任和生命周期
- 错误处理:设计错误传播机制,不要丢失错误信息
- 资源控制:限制并发数量,防止资源耗尽
- 优雅关闭:提供取消机制,确保资源正确释放
- 避免泄漏:确保所有goroutine最终会退出
- 简单胜于复杂:使用最简单的能满足需求的并发模式
- 测试:编写并发测试,使用竞态检测器(race detector)
在下一篇文章中,我们将开始探索Go语言的原子操作与内存模型,进一步深入理解Go的并发机制。
👨💻 关于作者与Gopher部落
"Gopher部落"专注于Go语言技术分享,提供从入门到精通的完整学习路线。
🌟 为什么关注我们?
- 系统化学习路径:本系列44篇文章循序渐进,带你完整掌握Go开发
- 实战驱动教学:理论结合实践,每篇文章都有可操作的代码示例
- 持续更新内容:定期分享最新Go生态技术动态与大厂实践经验
- 专业技术社区:加入我们的技术交流群,与众多Go开发者共同成长
📱 关注方式
- 微信公众号:搜索 “Gopher部落” 或 “GopherTribe”
- 优快云专栏:点击页面右上角"关注"按钮
💡 读者福利
关注公众号回复 “Go并发” 即可获取:
- 完整Go并发编程示例代码
- Go并发模式实战指南PDF
- 高性能Go应用优化技巧
期待与您在Go语言的学习旅程中共同成长!