Go语言并发模式:Worker Pool与Fan-Out/Fan-In实战

Go语言并发模型构建在goroutine和channel之上,Worker Pool和Fan-Out/Fan-In是两种最常用的高阶并发模式。Worker Pool通过固定数量的goroutine处理任务队列,控制并发度防止资源耗尽;Fan-Out/Fan-In通过扇出分发任务、扇出汇聚结果,实现并行计算与结果收集的解耦。结合context取消、速率限制和错误传播,可以构建生产级高并发服务。

Go Worker Pool模式与context取消机制

Worker Pool的核心思想是预创建固定数量的worker goroutine,通过共享channel接收任务。任务生产者将任务写入任务channel,worker从channel读取并执行。这种模式有效控制并发goroutine数量,避免无限制创建goroutine导致的内存暴涨和调度开销。

context.Context贯穿整个生命周期管理,当外部信号触发取消时,所有worker应尽快退出。以下是支持优雅关闭的Worker Pool实现:

package main

import (
    "context"
    "fmt"
    "sync"
    "time"
)

type Task struct {
    ID    int
    Input string
}

type Result struct {
    TaskID int
    Value  string
    Err    error
}

func worker(ctx context.Context, id int, tasks <-chan Task, results chan<- Result, wg *sync.WaitGroup) {
    defer wg.Done()
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("worker %d shutting down\n", id)
            return
        case task, ok := <-tasks:
            if !ok {
                return
            }
            // 模拟任务处理
            time.Sleep(100 * time.Millisecond)
            select {
            case results <- Result{TaskID: task.ID, Value: "processed:" + task.Input}:
            case <-ctx.Done():
                return
            }
        }
    }
}

func NewWorkerPool(ctx context.Context, workerCount, queueSize int) (<-chan Result, chan<- Task) {
    tasks := make(chan Task, queueSize)
    results := make(chan Result, queueSize)
    var wg sync.WaitGroup

    for i := 0; i < workerCount; i++ {
        wg.Add(1)
        go worker(ctx, i+1, tasks, results, &wg)
    }

    // 监控goroutine:所有worker退出后关闭results channel
    go func() {
        wg.Wait()
        close(results)
    }()

    return results, tasks
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    results, tasks := NewWorkerPool(ctx, 4, 100)

    // 提交任务
    go func() {
        for i := 0; i < 20; i++ {
            tasks <- Task{ID: i, Input: fmt.Sprintf("job-%d", i)}
        }
        close(tasks)
    }()

    // 收集结果
    for r := range results {
        fmt.Printf("result: task=%d value=%s err=%v\n", r.TaskID, r.Value, r.Err)
    }
}

这段实现的关键设计在于:任务channel和结果channel都设置了缓冲区,避免生产者和消费者之间的阻塞;select同时监听ctx.Done()和任务channel,确保取消信号能及时响应;监控goroutine通过WaitGroup等待所有worker退出后再关闭results channel,消费者通过range检测channel关闭自然退出。

Fan-Out/Fan-In并行计算模式实现

Fan-Out/Fan-In模式将一个任务拆分为多个子任务并行执行(Fan-Out),然后将各子任务的结果汇聚为最终结果(Fan-In)。与Worker Pool的区别在于,Fan-Out通常是一次性分发固定数量的子任务,而非持续接收任务队列。

package main

import (
    "context"
    "fmt"
    "math/rand"
    "sync"
    "time"
)

// Fan-Out:将输入分散到多个goroutine并行处理
func fanOut[T any, R any](ctx context.Context, input []T, workerCount int, fn func(T) R) <-chan R {
    out := make(chan R)
    var wg sync.WaitGroup
    wg.Add(workerCount)

    // 任务分发channel
    taskCh := make(chan T)
    go func() {
        defer close(taskCh)
        for _, item := range input {
            select {
            case taskCh <- item:
            case <-ctx.Done():
                return
            }
        }
    }()

    // 启动多个worker并行消费任务
    for i := 0; i < workerCount; i++ {
        go func() {
            defer wg.Done()
            for item := range taskCh {
                select {
                case out <- fn(item):
                case <-ctx.Done():
                    return
                }
            }
        }()
    }

    // 所有worker完成后关闭输出channel
    go func() {
        wg.Wait()
        close(out)
    }()

    return out
}

// Fan-In:汇聚多个channel的数据到一个channel
func fanIn[T any](ctx context.Context, channels ...<-chan T) <-chan T {
    out := make(chan T)
    var wg sync.WaitGroup
    wg.Add(len(channels))

    for _, ch := range channels {
        go func(c <-chan T) {
            defer wg.Done()
            for val := range c {
                select {
                case out <- val:
                case <-ctx.Done():
                    return
                }
            }
        }(ch)
    }

    go func() {
        wg.Wait()
        close(out)
    }()

    return out
}

func main() {
    ctx := context.Background()
    input := make([]int, 100)
    for i := range input {
        input[i] = rand.Intn(100)
    }

    // Fan-Out:8个worker并行处理
    results := fanOut(ctx, input, 8, func(n int) int {
        time.Sleep(time.Duration(rand.Intn(50)) * time.Millisecond)
        return n * n // 计算平方
    })

    // Fan-In:汇聚结果
    total := 0
    for r := range results {
        total += r
    }
    fmt.Printf("sum of squares: %d\n", total)
}

这段代码使用Go 1.18+的泛型参数,fanOut和fanIn可以处理任意类型的输入和输出。fanIn函数接收多个channel,为每个channel启动一个转发goroutine,将数据汇聚到统一的输出channel。多个fanOut的输出可以通过fanIn合并,实现多级并行流水线。

速率限制并发请求与rate.Limiter令牌桶

在调用外部API时,无限制的并发请求会触发限流或导致服务过载。golang.org/x/time/rate包提供基于令牌桶算法的速率限制器,可以精确控制请求发送速率。令牌桶以固定速率生成令牌,每次请求消耗一个令牌,桶满时丢弃多余令牌。

package main

import (
    "context"
    "fmt"
    "net/http"
    "sync"
    "time"

    "golang.org/x/time/rate"
)

type APIClient struct {
    client  *http.Client
    limiter *rate.Limiter
}

func NewAPIClient(rps float64, burst int) *APIClient {
    return &APIClient{
        client: &http.Client{Timeout: 10 * time.Second},
        // rate: 每秒令牌数,burst: 桶容量(允许瞬时突发)
        limiter: rate.NewLimiter(rate.Limit(rps), burst),
    }
}

func (c *APIClient) Do(ctx context.Context, req *http.Request) (*http.Response, error) {
    // Wait阻塞直到获取令牌,或ctx取消
    if err := c.limiter.Wait(ctx); err != nil {
        return nil, fmt.Errorf("rate limiter: %w", err)
    }
    return c.client.Do(req.WithContext(ctx))
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    api := NewAPIClient(10, 5) // 每秒10个请求,突发5个

    urls := []string{
        "https://api.example.com/users",
        "https://api.example.com/posts",
        "https://api.example.com/comments",
        "https://api.example.com/albums",
        "https://api.example.com/photos",
    }

    var wg sync.WaitGroup
    results := make(chan string, len(urls))

    for _, url := range urls {
        wg.Add(1)
        go func(u string) {
            defer wg.Done()
            req, _ := http.NewRequest("GET", u, nil)
            resp, err := api.Do(ctx, req)
            if err != nil {
                results <- fmt.Sprintf("error %s: %v", u, err)
                return
            }
            defer resp.Body.Close()
            results <- fmt.Sprintf("ok %s: %d", u, resp.StatusCode)
        }(url)
    }

    go func() {
        wg.Wait()
        close(results)
    }()

    for r := range results {
        fmt.Println(r)
    }
}

rate.Limiter的Wait方法在令牌不足时阻塞等待,而非直接返回错误,适合需要保证请求最终发送的场景。如果需要非阻塞语义,使用Allow()方法检查令牌可用性后立即返回。burst参数控制瞬时突发容量,设为1时退化为严格匀速模式,适合对速率极其敏感的服务。

errgroup错误传播与goroutine生命周期管理

原生sync.WaitGroup不支持错误传播,任一goroutine出错时无法通知其他goroutine退出。golang.org/x/sync/errgroup包扩展了WaitGroup,在任一goroutine返回错误时自动取消context,触发其他goroutine退出。errgroup还支持通过SetLimit设置最大并发数,内置信号量控制。

package main

import (
    "context"
    "fmt"
    "net/http"
    "time"

    "golang.org/x/sync/errgroup"
)

func fetchURL(ctx context.Context, url string) error {
    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return fmt.Errorf("create request %s: %w", url, err)
    }

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return fmt.Errorf("fetch %s: %w", url, err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != 200 {
        return fmt.Errorf("unexpected status %s: %d", url, resp.StatusCode)
    }
    fmt.Printf("fetched %s OK\n", url)
    return nil
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
    defer cancel()

    // errgroup with context: 第一个错误会取消ctx
    g, ctx := errgroup.WithContext(ctx)

    // 限制最大并发数为3
    g.SetLimit(3)

    urls := []string{
        "https://httpbin.org/get",
        "https://httpbin.org/delay/2",
        "https://httpbin.org/status/404", // 这个会返回错误
        "https://httpbin.org/delay/3",
        "https://httpbin.org/get",
    }

    for _, url := range urls {
        // 注意:循环变量捕获问题,Go 1.22+已修复
        url := url
        g.Go(func() error {
            return fetchURL(ctx, url)
        })
    }

    if err := g.Wait(); err != nil {
        fmt.Printf("group failed: %v\n", err)
    } else {
        fmt.Println("all succeeded")
    }
}

errgroup.WithContext创建一个可取消的context,当任一g.Go注册的函数返回非nil错误时,该context被取消。其他正在执行的goroutine通过检查ctx.Done()感知取消信号并提前退出,避免无意义的计算开销。g.SetLimit(3)将并发数限制为3,超出限制的g.Go调用会阻塞等待,效果等同于信号量但代码更简洁。

循环变量捕获是Go并发编程的经典陷阱。在Go 1.22之前,for循环中的变量在每次迭代间共享同一地址,goroutine闭包捕获的是最终值。解决方案是在循环体内创建局部副本(url := url)。Go 1.22修复了这个问题,循环变量每次迭代都是新变量,但理解这个机制对维护旧代码仍有必要。

综合模式:限流Worker Pool完整实现

将Worker Pool、速率限制和errgroup三者结合,可以构建生产级并发处理管道。以下实现展示了一个完整的限流Worker Pool,支持优雅关闭、错误传播和速率控制:

package main

import (
    "context"
    "fmt"
    "sync"
    "time"

    "golang.org/x/sync/errgroup"
    "golang.org/x/time/rate"
)

type Job struct {
    ID   int
    Data string
}

type Processor struct {
    workers  int
    queue    int
    limiter  *rate.Limiter
}

func (p *Processor) Run(ctx context.Context, jobs []Job, process func(context.Context, Job) error) error {
    g, ctx := errgroup.WithContext(ctx)
    g.SetLimit(p.workers)

    jobCh := make(chan Job, p.queue)
    var once sync.Once

    // 生产者
    g.Go(func() error {
        defer once.Do(func() { close(jobCh) })
        for _, job := range jobs {
            if err := p.limiter.Wait(ctx); err != nil {
                return err
            }
            select {
            case jobCh <- job:
            case <-ctx.Done():
                return ctx.Err()
            }
        }
        return nil
    })

    // 消费者(多个worker通过g.SetLimit控制并发)
    for i := 0; i < p.workers; i++ {
        g.Go(func() error {
            for job := range jobCh {
                if err := process(ctx, job); err != nil {
                    return fmt.Errorf("job %d: %w", job.ID, err)
                }
            }
            return nil
        })
    }

    return g.Wait()
}

func main() {
    p := &Processor{
        workers: 5,
        queue:   20,
        limiter: rate.NewLimiter(rate.Limit(20), 5), // 20 QPS, burst 5
    }

    jobs := make([]Job, 50)
    for i := range jobs {
        jobs[i] = Job{ID: i, Data: fmt.Sprintf("data-%d", i)}
    }

    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    err := p.Run(ctx, jobs, func(ctx context.Context, job Job) error {
        time.Sleep(50 * time.Millisecond)
        fmt.Printf("processed job %d\n", job.ID)
        return nil
    })

    if err != nil {
        fmt.Printf("error: %v\n", err)
    }
}

这个综合实现中,errgroup同时管理生产者和消费者goroutine,任何一方的错误都会通过context取消传播到所有goroutine。生产者通过rate.Limiter控制任务投递速率,消费者通过g.SetLimit控制处理并发度。once.Do确保jobCh只关闭一次,避免重复关闭导致的panic。这种模式适用于需要限流的外部API批量调用、数据库批量写入等实际生产场景。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/go-yu-yan-bing-fa-mo-shi-workerpool-yu-fanoutfanin-shi-zhan/

(0)
小编小编
上一篇 3小时前
下一篇 3小时前

相关推荐