TBB Parallel Pipeline 详细用法指南

tbb::parallel_pipeline 是 TBB 库中一个强大的流水线并行处理工具,专为序列化数据处理而设计。它可以将一系列处理步骤组织成流水线,每个阶段可以并行或串行执行。

基本概念

  1. 流水线(Pipeline):多个处理阶段的组合
  2. 过滤器(Filter):单个处理阶段
  3. 令牌(Token):控制并发度的单位
  4. 过滤模式:
    • filter::serial_in_order:串行执行且保持输入顺序
    • filter::serial_out_of_order:串行执行但不保证顺序
    • filter::parallel:并行执行多个项目

基本语法

#include <tbb/pipeline.h>

tbb::parallel_pipeline(
    size_t max_tokens,  // 并发度(流水线中同时处理的项目数)
    filter1 & filter2 & filter3 & ... & filterN  // 流水线过滤器链接
);

每个过滤器可以使用 tbb::make_filter 创建:

tbb::make_filter<InputType, OutputType>(
    tbb::filter::mode,  // 过滤器模式(串行或并行)
    [](InputType input) -> OutputType {  // 过滤器函数
        // 处理逻辑
        return output;
    }
);

详细示例

以下是一个完整的示例,展示了使用 parallel_pipeline 处理人体检索的流程:

#include <tbb/pipeline.h>
#include <tbb/task_group.h>
#include <vector>
#include <string>
#include <iostream>

struct SearchRequest {
    std::string query;
    int searchId;
};

struct DatabaseRecord {
    std::vector<float> features;
    std::string recordId;
};

struct ScoredResult {
    std::string recordId;
    float score;
};

struct SearchResult {
    std::vector<ScoredResult> results;
    int searchId;
};

// 人体检索流水线示例
void personSearchPipeline(const std::vector<SearchRequest>& requests) {
    // 结果存储
    std::vector<SearchResult> finalResults;
    std::mutex resultsMutex;
    
    // 1. 设置并行度(令牌数)- 可根据CPU核心数调整
    size_t maxTokens = 16;
    
    tbb::parallel_pipeline(
        // 最大并发处理的项目数
        maxTokens,
        
        // 第一阶段:输入阶段 - 通常是串行的,提供输入数据
        tbb::make_filter<void, SearchRequest*>(
            tbb::filter::serial_in_order, 
            [&requests, counter = size_t{0}](tbb::flow_control& fc) mutable -> SearchRequest* {
                if (counter >= requests.size()) {
                    // 结束流水线
                    fc.stop();
                    return nullptr;
                }
                // 返回下一个请求指针
                return const_cast<SearchRequest*>(&requests[counter++]);
            }
        ) &
        
        // 第二阶段:特征提取 - 可并行处理多个请求
        tbb::make_filter<SearchRequest*, std::pair<SearchRequest*, std::vector<float>>>(
            tbb::filter::parallel,
            [](SearchRequest* request) -> std::pair<SearchRequest*, std::vector<float>> {
                // 模拟从请求中提取特征
                std::vector<float> features(128, 0.5f); // 假设的特征向量
                std::cout << "提取特征: 请求ID " << request->searchId << std::endl;
                return {request, features};
            }
        ) &
        
        // 第三阶段:数据库查询 - I/O操作,可并行执行以重叠等待时间
        tbb::make_filter<std::pair<SearchRequest*, std::vector<float>>, 
                         std::pair<SearchRequest*, std::vector<DatabaseRecord>>>(
            tbb::filter::parallel,
            [](std::pair<SearchRequest*, std::vector<float>> input) -> 
               std::pair<SearchRequest*, std::vector<DatabaseRecord>> {
                // 模拟数据库查询,获取候选记录
                std::cout << "数据库查询: 请求ID " << input.first->searchId << std::endl;
                
                // 模拟从数据库检索记录
                std::vector<DatabaseRecord> records;
                for (int i = 0; i < 10; i++) {
                    records.push_back({
                        std::vector<float>(128, 0.3f), // 假设的特征
                        "record_" + std::to_string(i)
                    });
                }
                
                return {input.first, records};
            }
        ) &
        
        // 第四阶段:特征比对 - 计算密集型,适合并行
        tbb::make_filter<std::pair<SearchRequest*, std::vector<DatabaseRecord>>, 
                         std::pair<SearchRequest*, std::vector<ScoredResult>>>(
            tbb::filter::parallel,
            [](std::pair<SearchRequest*, std::vector<DatabaseRecord>> input) -> 
               std::pair<SearchRequest*, std::vector<ScoredResult>> {
                auto request = input.first;
                auto& records = input.second;
                
                std::cout << "特征比对: 请求ID " << request->searchId << std::endl;
                
                // 模拟特征比对和评分
                std::vector<ScoredResult> scoredResults;
                for (const auto& record : records) {
                    // 简单模拟评分计算
                    float score = 0.8f + (static_cast<float>(rand()) / RAND_MAX) * 0.2f;
                    scoredResults.push_back({record.recordId, score});
                }
                
                return {request, scoredResults};
            }
        ) &
        
        // 第五阶段:结果排序 - 按照顺序处理每个结果集更好
        tbb::make_filter<std::pair<SearchRequest*, std::vector<ScoredResult>>, SearchResult*>(
            tbb::filter::serial_in_order,
            [](std::pair<SearchRequest*, std::vector<ScoredResult>> input) -> SearchResult* {
                auto request = input.first;
                auto& scoredResults = input.second;
                
                std::cout << "结果排序: 请求ID " << request->searchId << std::endl;
                
                // 根据分数排序
                std::sort(scoredResults.begin(), scoredResults.end(), 
                    [](const ScoredResult& a, const ScoredResult& b) {
                        return a.score > b.score;
                    });
                
                // 创建结果对象
                auto* result = new SearchResult{scoredResults, request->searchId};
                return result;
            }
        ) &
        
        // 第六阶段:结果收集 - 通常是串行的,将结果保存
        tbb::make_filter<SearchResult*, void>(
            tbb::filter::serial_in_order,
            [&finalResults, &resultsMutex](SearchResult* result) {
                std::cout << "收集结果: 请求ID " << result->searchId 
                          << ", 找到 " << result->results.size() << " 条匹配" << std::endl;
                
                // 保存结果
                {
                    std::lock_guard<std::mutex> lock(resultsMutex);
                    finalResults.push_back(*result);
                }
                
                // 释放内存
                delete result;
            }
        )
    );
    
    // 流水线执行完成后,finalResults 包含所有处理结果
    std::cout << "处理完成,共 " << finalResults.size() << " 条结果" << std::endl;
}

在人体检索系统中的应用

针对人体检索系统,parallel_pipeline 可以用于以下场景:

flowchart LR A["输入请求\n(串行)"] --> B["特征提取\n(并行)"] B --> C["数据库查询\n(并行)"] C --> D["特征比对\n(并行)"] D --> E["结果聚合\n(串行)"] E --> F["后处理和输出\n(串行)"]

实际应用示例

// 人体检索系统中的具体应用
void personTrackPipeline(const std::vector<TrackRequest>& requests) {
    tbb::parallel_pipeline(
        // 设置合理的并发度
        std::min(static_cast<size_t>(16), requests.size()),
        
        // 输入阶段
        tbb::make_filter<void, TrackRequest*>(
            tbb::filter::serial_in_order,
            [&requests, i = 0](tbb::flow_control& fc) mutable -> TrackRequest* {
                if (i >= requests.size()) {
                    fc.stop();
                    return nullptr;
                }
                return const_cast<TrackRequest*>(&requests[i++]);
            }
        ) &
        
        // 并行检索实名有抓拍档案
        tbb::make_filter<TrackRequest*, std::pair<TrackRequest*, ArchiveResults>>(
            tbb::filter::parallel,
            [](TrackRequest* req) -> std::pair<TrackRequest*, ArchiveResults> {
                ArchiveResults results = searchRealNameWithCapture(req->parameters);
                return {req, results};
            }
        ) &
        
        // 并行检索实名无抓拍档案和非实名档案
        tbb::make_filter<std::pair<TrackRequest*, ArchiveResults>, 
                         std::tuple<TrackRequest*, ArchiveResults, ArchiveResults, ArchiveResults>>(
            tbb::filter::parallel,
            [](std::pair<TrackRequest*, ArchiveResults> in) -> 
               std::tuple<TrackRequest*, ArchiveResults, ArchiveResults, ArchiveResults> {
                auto req = in.first;
                auto realWithCapture = in.second;
                
                // 并行执行这两个检索(使用内部TBB任务组)
                ArchiveResults realNoCapture;
                ArchiveResults unrealArchives;
                
                tbb::task_group g;
                g.run([&]{ realNoCapture = searchRealNameNoCapture(req->parameters); });
                g.run([&]{ unrealArchives = searchNoName(req->parameters); });
                g.wait();
                
                return {req, realWithCapture, realNoCapture, unrealArchives};
            }
        ) &
        
        // 并行检索实时记录
        tbb::make_filter<std::tuple<TrackRequest*, ArchiveResults, ArchiveResults, ArchiveResults>,
                         std::tuple<TrackRequest*, ArchiveResults, ArchiveResults, ArchiveResults, RecordResults>>(
            tbb::filter::parallel,
            [](std::tuple<TrackRequest*, ArchiveResults, ArchiveResults, ArchiveResults> in) -> 
               std::tuple<TrackRequest*, ArchiveResults, ArchiveResults, ArchiveResults, RecordResults> {
                auto req = std::get<0>(in);
                RecordResults rtRecords = searchRealtime(req->parameters);
                return {req, std::get<1>(in), std::get<2>(in), std::get<3>(in), rtRecords};
            }
        ) &
        
        // 结果合并与排序
        tbb::make_filter<std::tuple<TrackRequest*, ArchiveResults, ArchiveResults, ArchiveResults, RecordResults>,
                         SearchResponse*>(
            tbb::filter::serial_in_order,
            [](std::tuple<TrackRequest*, ArchiveResults, ArchiveResults, ArchiveResults, RecordResults> in) -> 
               SearchResponse* {
                auto req = std::get<0>(in);
                
                // 合并所有结果
                SearchResponse* response = new SearchResponse();
                response->requestId = req->requestId;
                
                // 合并不同来源的结果
                mergeResults(response->results, std::get<1>(in));  // 实名有抓拍
                mergeResults(response->results, std::get<2>(in));  // 实名无抓拍  
                mergeResults(response->results, std::get<3>(in));  // 非实名
                mergeResults(response->results, std::get<4>(in));  // 实时记录
                
                // 排序
                sortResults(response->results);
                
                return response;
            }
        ) &
        
        // 最终处理与存储
        tbb::make_filter<SearchResponse*, void>(
            tbb::filter::serial_in_order,
            [](SearchResponse* response) {
                // 存储结果到缓存
                storeResultToCache(response);
                
                // 输出或发送结果
                sendResultToClient(response);
                
                delete response;
            }
        )
    );
}

性能优化技巧

  1. 调整令牌数量:根据工作负载和CPU核数调整 max_tokens
  2. 选择适当的过滤模式:
    • 对I/O密集型操作使用 parallel 或 serial_out_of_order
    • 对必须保持顺序的操作使用 serial_in_order
  3. 合理分配工作量:避免某一阶段成为瓶颈
  4. 减少数据复制:尽可能使用指针或引用传递数据
  5. 合并短小阶段:流水线开销也需要考虑,过短的阶段可能得不偿失

通过合理设计并行流水线,可以充分利用多核CPU的处理能力,显著提升人体检索系统的性能。