C++ 线程池

28 Aug 2024 | 5 分钟阅读

线程池线程的集合,每个线程都有一个特定的任务。因此,不同的线程会执行不同类型的作业。结果是,每个线程都专门处理不同的任务。一个线程负责执行一组特定的相似函数,而另一个线程执行一组不同的相同函数,这种模式会延续到其他线程。每个线程都应该专注于可比较的函数,因为不同的线程有不同的参数。

这个线程池必须在 C++ 中维护。C++ 需要一个库来生成和管理线程池。这很可能是因为有多种构建线程池的方法。因此,C++ 程序员必须根据需求设计线程池。

什么是线程?

线程是通过创建线程类而创建的组件。在常见的实例化中,线程构造函数的第一个参数通常是顶层方法的名称。线程构造函数的其余输入是函数参数。一旦创建线程,函数就开始执行。C++ 的 main() 函数就是一个顶层函数。该全局作用域中的其他职责是顶层操作main() 函数是一个不需要显式声明的线程,这与其他线程不同。

线程池应该具备哪些属性?

线程池只是一组可以使用的线程。在 C++ 中,它可以表示为 std::thread 数组或 vector<std::vectorstd::thread> 适合任何扩展。

在某个时刻,线程池中的每个线程都可能被分配一个作业。当工作线程被创建时,具体的作业是未知的。在 C++ 中,这意味着线程池中的一个线程

  • 它应该能够执行任意函数,允许任何参数集和任何返回类型。
  • 它应该被允许将任务执行结果传回给任务的发布者。
  • 它应该能够在需要时被唤醒以执行作业,同时在不需要时不占用过多的 CPU 资源
  • 在必要时,控制器线程必须能够控制它以暂停任务、停止接受任务、拒绝未完成的作业等。

对于第一点,当前的 C++ 方法是利用头文件 <functional> (std::bind, std::function, 等) 提供的框架,并结合模板参数包。对于第二点,传统技术是在发布任务的同时实现回调函数;较新的 C++ 方法则涉及使用 std::packaged_taskstd::future。对于第三点,对于不频繁的作业可以考虑 std::condition_variable,对于更频繁的任务可以考虑 std::this_thread::yield。对于第四点,可以通过将内部变量作为令牌的表示,并让每个工作线程定期检查该令牌来实现。

C++ 中的线程池需求与架构

上一段解释的线程池设计很简单。它意味着线程池没有任何特殊需求。因此,我们可以假设

  • 我们可以传递任何接受任何形式输入参数的函数。为方便起见,在这种情况下,我们可以为我们的任务使用 std:: 的变体。
  • 队列必须是线程安全的,因为线程池需要一个队列来包含作业及其参数。
  • 线程池只有在队列中的所有作业都完成后才会停止运行。

下面列表总结了我们执行所需的关键思想。

  • 任务 - 表示我们的线程池可以执行的任务的结构。在其最基本的形式中,此对象包含一个必须执行的操作的 lambda(或函数指针)及其参数。
  • 任务队列 - 这是一个用于存放将由线程池取出的作业的容器。
  • 线程池 - 这是一个包含线程池功能的结构体。它包含任务队列、一个 std::vector of std::jthreads,以及从任务队列中推入和弹出的逻辑。

在 C++ 中实现线程池

接下来看看实现以及代码上的一些关键点。

  • 线程池是任务队列之上的一个管理层。
  • 推送到线程池等同于向队列中添加一个作业。
  • 不需要弹出机制,因为我们的基本架构要求线程池仅在完成所有任务后才终止。
  • 析构函数必须确保池中的所有线程都已停止。因此,它应该向池中的每个线程发送一个停止任务。

线程如何执行任务?

在上面的线程池代码中,我们可以看到 _threads vector 中有 n_threadsstd::jthread 项。但是,每个线程执行什么代码?简单来说,就是从队列中弹出并按需停止的逻辑。

使用线程池

一个使用线程池显示递增整数的简单应用程序。它还提供线程 id,以确认每次计数确实是在不同的线程上处理的。


下一个主题C++ 中的 KMP 算法