线程管理
另请参阅
线程管理 API 参考
线程池
许多 Arrow C++ 操作会将工作分配到多个线程以充分利用底层硬件并行性。例如,当读取 Parquet 文件时,我们可以并行解码每个列。为了实现这一目标,我们将任务提交给某种执行器。
在 Arrow C++ 中,我们使用线程池进行并行调度,以及在用户请求串行执行时使用事件循环。用户可以提供自己的自定义实现,尽管这是一个高级概念,这里不涵盖。
CPU vs. I/O
为了减小上下文切换的开销,我们的默认 CPU 密集型任务线程池具有固定大小,默认为 std::thread::hardware_concurrency。这意味着 CPU 任务不应该长时间阻塞,因为这会导致 CPU 的利用率不高。为了实现这一目标,我们还有一个单独的线程池,应该用于需要阻塞的任务。由于这些任务通常与 I/O 操作相关,我们将其称为 I/O 线程池。这种模型通常与异步计算相关。
I/O 线程池的大小当前默认为 8 个线程,并应根据 I/O 硬件的并行能力进行调整。例如,如果大多数读写发生在典型的 HDD 上,那么默认值 8 可能足够了。另一方面,如果大多数读写发生在远程文件系统(如 S3)上,通常可以受益于许多并发读取,并且可能可以通过增加 I/O 线程池的大小来提高 I/O 性能。默认 I/O 线程池的大小可以通过 ARROW_IO_THREADS 环境变量或 arrow::io::SetIOThreadPoolCapacity() 函数进行管理。
增加 CPU 线程池的大小不太可能产生任何好处。在某些情况下,减小 CPU 线程池的大小以减小 Arrow C++ 对与其他进程或用户线程共享的硬件的影响可能是有意义的。默认 CPU 线程池的大小可以通过 OMP_NUM_THREADS 环境变量或 arrow::SetCpuThreadPoolCapacity() 函数进行管理。
串行执行
在 Arrow C++ 中,可能使用线程的操作通常可以通过某种参数配置为串行运行。在这种情况下,我们通常会使用调用线程操作的事件循环替换 CPU 执行器。但是,许多操作将继续使用 I/O 线程池。这意味着即使请求了串行执行,仍然可能发生一些并行性。
Jemalloc 后台线程
使用 jemalloc 分配器时,jemalloc 将创建少量后台线程来管理内存池。这些线程应该对性能影响很小,但在运行像 Valgrind 这样的分析工具时,它们可能会显示为内存泄漏。这是无害的,可以安全地抑制,或者可以在不使用 jemalloc 的情况下编译 Arrow C++。
异步工具
Future Arrow C++ 使用 arrow::Future 在线程之间传递结果。通常,当操作需要执行某种需要一段时间阻塞的长时间运行任务时,将创建 arrow::Future。arrow::Future 对象主要用于内部使用,任何返回 arrow::Future 的方法通常也会有一个同步变体。