线程管理#
另请参阅
线程池#
许多 Arrow C++ 操作会将工作分配到多个线程中,以利用底层硬件的并行性。例如,在 读取 Parquet 文件 时,我们可以并行解码每一列。为了实现这一点,我们将任务提交给某种执行器(executor)。
在 Arrow C++ 中,我们使用线程池进行并行调度,并在用户请求串行执行时使用事件循环。用户也可以提供自定义的实现,但这属于高级概念,此处不作介绍。
CPU 与 I/O#
为了最大限度地减少上下文切换的开销,我们用于 CPU 密集型任务的默认线程池具有固定大小,默认值为进程的 CPU 亲和性(在 Linux 上)或 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 等分析工具时可能会显示为内存泄漏。这是无害的,可以安全地抑制,或者在编译 Arrow C++ 时不启用 jemalloc。
异步工具#
Future#
Arrow C++ 使用 arrow::Future 在线程之间传递结果。通常,当一个操作需要执行某种会阻塞一段时间的长耗时任务时,会创建一个 arrow::Future。arrow::Future 对象主要用于内部使用,任何返回 arrow::Future 的方法通常也会提供一个同步变体。