返回专辑
·Johan·4 分钟阅读

线程池直觉:任务粒度与伪共享

线程池与 work-stealing:感知任务并行化时的队列与粒度。

线程池直觉:任务粒度与伪共享

1. 加线程 throughput 反降——粒度错了

1000 个 task 每个只算 10µs,队列锁与调度开销淹没收益。Jetson 上 hardware_concurrency() 开满 worker,与相机 IRQ、GPU 抢核,体素滤波帧时间反而涨。线程池适合 粗粒度 work:一帧点云按 tile、一张图按行块——毫秒级 CPU,不是 per-point。调度开销与 task 耗时同一数量级时,并行必亏——先量单 task 耗时再定切分。

把 1000 个小 task 丢线程池,若每 task 只算 10µs,队列锁占主导。work-stealing(TBB/OpenMP 内部)能平衡负载,但救不了「task 太碎」。自研 pool 前先测固定 worker 数 + 粗 tile 是否已够。集成测试用真实分辨率和 tile 数,别只用 unit test 的 tiny array。

2. 任务粒度与切分

cpp
for (size_t off = 0; off < n; off += 65536)
  pool.submit([off, &cloud] { filterRange(cloud, off, off + 65536); });

粒度 << 调度开销 → 越并越慢。benchmark 1/4/16 线程 vs 粒度曲线找 knee。queue 深度设上限;满时 try_enqueue 失败 policy 写清:drop tile 还是 block producer——无限 queue 会 OOM。每 tile 一个 std::async 开销大;counting_latch 或 atomic tile 计数更稳。坏做法:每点一个 future;好做法:按 64k 点分块。

3. 伪共享与计数

不同 worker 写同一 cache line 的 atomic 任务计数会 ping-pong。各自本地累加再 merge;hot counter 与 queue 头尾 alignas(64) 分离。atomic 任务计数与 worker local queue 头尾同 cache line 会 ping-pong,padding 分离 hot counter。

cpp
BS::thread_pool pool(4);
std::latch done(static_cast<std::ptrdiff_t>(tiles.size()));
for (auto& t : tiles)
  pool.detach_task([&] { processTile(t); done.count_down(); });
done.wait();

4. 与实时线程、ROS executor 隔离

decode SCHED_FIFO 线程不进 general pool;pool 跑 offline map optimize。混用 cyclictest 可见 priority inversion。计算 offload 到 pool 后,结果回 executor 用 lock-free queue 或 dispatch 回单线程 publish——别在 worker 里直接 publish 竞态。TBB task_arena::isolate 可把 offline optimize 与 online decode 分区。

线程数默认 hardware_concurrency()-1,留一核给 decode/executor;pool 线程别设 SCHED_FIFO,与 RT 线程抢 scheduler。在线感知别在 SCHED_FIFO 解码线程里 pool.submit——priority inversion 尖峰会在 cyclictest 里看见。

5. 确定性与验证

work-stealing 可能打乱 task 顺序——需要 deterministic replay 的 pipeline 要固定调度或单线程 fallback。改 pool 大小后对照 perf stat -e context-switches 与 bag replay 延迟。std::packaged_task + future 在实时环析构可能 block;fire-and-forget 配 latch 更稳。

6. 案例:Jetson 开满核反而变慢

体素滤波默认开满 hardware_concurrency(),帧时间从 19ms 涨到 28ms——与相机 IRQ、GPU 抢核。改成固定 4 worker 并在提交侧用 task_arena(4) 限制嵌套并行后恢复。Intel oneTBB task_arena::isolate 适合把 offline optimize 和 online decode 分区。queue 满时 try_enqueue 失败就丢最旧 tile 并 Throttled WARN,比无限 queue OOM 强。onboard 留核是常态而非调优——感知栈与 ISP/GPU 共享 SoC,线程满开是反模式。

7. work-stealing 直觉

TBB/OpenMP 内部 work-stealing 平衡负载;自研前先测 std::async 是否够。stealing 救不了 task 太碎——先找粒度 knee。非确定性 replay 时 task 顺序随机,文档标注限制或提供单线程 fallback。

8. 线程池大小与负载形态

CPU-bound tile 工作线程数接近物理核;IO-bound 或等 GPU 时可更少 worker,避免空转抢锁。嵌套并行(tile 内再 OpenMP)须 task_arenaOMP_NUM_THREADS 封顶,否则 thread explosion。改 pool 大小后看 context-switch 与 L3 miss,不只看 wall time——steal 过多说明粒度或分区仍不对。

9. 与 bag replay 的确定性

并行 filter 若影响输出顺序(如 merge 无 sort),bag replay 对比会 flaky——要么固定 merge 序,要么 acceptance 用集合等价而非字节序。文档写清哪些 stage 允许 non-deterministic speedup。安全相关路径(collision check)优先 deterministic 单线程,再谈 parallel。

10. 验收

  • tile 粒度 ms 级,非 µs 级。
  • pool 线程非 SCHED_FIFO;与 RT 线程资源隔离可测。
  • queue 满有明确 policy,无 silent OOM。
  • 并行路径有 non-determinism 文档,replay 测试通过或标注限制。

12. 与 GPU pipeline 的协调

CPU tile pool 与 CUDA stream 并行时,CPU worker 数还要扣除 GPU 占用的核与内存带宽——Jetson 上尤甚。submit 侧批大小对齐 GPU kernel 的 batch 更划算,别 CPU 切太碎再 H2D 多次。profile 看 end-to-end frame time,不只看 CPU filter 段。嵌套 OpenMP 与 pool 并存时,OMP_NUM_THREADS 与 pool worker 乘积可能爆炸——用 task_arena 或 explicit 禁嵌套。集成测试记录 parallel 与 serial 输出差异上限;安全相关 stage 默认 serial,性能 stage 才 parallel——文档写清哪段允许 non-deterministic order。worker 线程名便于 perf 与 crash stack 辨认,别都叫 thread_pool。改 pool 大小后务必重跑 cyclictest 与 bag replay,别只信 desktop benchmark。

queue 深度 metric 暴露背压:持续满说明 worker 不足或 tile 仍太细。cyclictest 与 pool 并行跑,验证 RT 线程未被 pool 拖累。offline 任务与 online 任务分 pool 实例,别共 queue——否则 optimize 堵 decode。bag replay 对比 parallel vs serial 时,接受集合等价或固定 merge 序,别要求字节级一致除非 pipeline 承诺 deterministic。

← 全部文章

johan's blog