简单C++线程池

Java 中有一个很方便的 ThreadPoolExecutor,可以用做线程池。想找一下 C++ 的类似设施,尤其是能方便理解底层原理可上手的。网上找到的 demo,基本都是介绍的 projschj 的C++11线程池。这份源码最后的commit日期是2014年,现在是2021年了,本文将在阅读源码的基础上,对这份代码进行一些改造。关于线程池,目前网上讲解最好的一篇文章是这篇 Java线程池实现原理及其在美团业务中的实践,值得一读。

改造后的源码在 https://gitee.com/zhcpku/ThreadPool 进行提供。

projschj 的代码

1. 数据结构

主要包含两个部分,一组执行线程、一个任务队列。执行线程空闲时,总是从任务队列中取出任务执行。具体执行逻辑后面会进行解释。

class ThreadPool {
// ...
private:
using task_type = std::function<void()>;
// need to keep track of threads so we can join them
std::vector<std::thread> workers;
// the task queue
std::queue<task_type> tasks;
};

2. 同步机制

这里包括一把锁、一个条件变量,还有一个bool变量:

  • 锁用于保护任务队列、条件变量、bool变量的访问;
  • 条件变量用于唤醒线程,通知任务到来、或者线程池停用;
  • bool变量用于停用线程池;
class ThreadPool {
// ...
private:
// synchronization
std::mutex queue_mutex;
std::condition_variable condition;
bool stop;
};

3. 线程池启动

启动线程池,首先要做的是构造指定数量的线程出来,然后让每个线程开始运行。

对于每个线程,运行逻辑是一样的:尝试从任务队列中获取任务并执行,如果拿不到任务、并且线程池没有被停用,则睡眠等待。

这里线程等待任务使用的是条件变量,而不是信号量或者自旋锁等其他设施,是为了让线程睡眠,避免CPU空转浪费。

// the constructor just launches some amount of workers
inline ThreadPool::ThreadPool(size_t thread_num)
: stop(false)
{
for (size_t i = 0; i < thread_num; ++i) {
workers.emplace_back([this] {
for (;;) {
task_type task;
{
std::unique_lock<std::mutex> lock(this->queue_mutex);
this->condition.wait(
lock, [this] { return this->stop || !this->tasks.empty(); });
if (this->stop && this->tasks.empty()) {
return;
}
task = std::move(this->tasks.front());
this->tasks.pop();
}
task();
}
});
}
}

4.停用线程池

线程的停用,需要让每一个线程停下来,并且等到每个线程都停止再退出主线程才是比较安全的操作。

停止分三步:设置停止标识、通知到每一个线程(睡眠的线程需要唤醒)、等到每一个线程停止。

// the destructor joins all threads
inline ThreadPool::~ThreadPool()
{
{
std::unique_lock<std::mutex> lock(queue_mutex);
stop = true;
}
condition.notify_all();
for (std::thread& worker : workers) {
worker.join();
}
}

5. 提交新任务

这是整个线程池的核心,也是写的最复杂,用C++新特性最多的地方,包括但不限于:

自动类型推导、变长模板函数、右值引用、完美转发、原地构造、智能指针、future、bind ……

顺带提一句,要是早有变长模板参数,std::min / std::max 也不至于只能比较两个数大小,再多就得用大括号包起来作为 initialize_list 传进去了。

这里提交任务时,由于我们的任务类型定义为一个无参无返回值的函数对象,所以需要先通过 std::bind 把函数及其参数打包成一个 对应类型的可调用对象,返回值将通过 future 异步获取。然后是要把这个任务插入任务队列末尾,因为任务队列被多线程并发访问,所以需要加锁。

另外需要处理的两个情况,一个是线程睡眠时,新入队任务需要主要唤醒线程;另一个是线程池要停用时,入队操作是非法的。

// add new work item to the pool
template <class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type>
{
using return_type = typename std::result_of<F(Args...)>::type; auto task = std::make_shared<std::packaged_task<return_type()>>(
std::bind(std::forward<F>(f), std::forward<Args>(args)...)); std::future<return_type> res = task->get_future();
{
std::unique_lock<std::mutex> lock(queue_mutex); // don't allow enqueueing after stopping the pool
if (stop) {
throw std::runtime_error("enqueue on stopped ThreadPool");
}
tasks.emplace([task]() { (*task)(); });
}
condition.notify_one();
return res;
}

改造

以上代码已经足以阐释线程池基本原理了,以下改进主要从可靠性、易用性、使用场景等方面进行改进。

1. non-copyable

线程池本身应该是不可复制的,这里我们通过删除拷贝构造函数和赋值操作符,以及其对用的右值引用版本来实现:

class ThreadPool {
// ...
private:
// non-copyable
ThreadPool(const ThreadPool&) = delete;
ThreadPool(ThreadPool&&) = delete;
ThreadPool& operator=(const ThreadPool&) = delete;
ThreadPool& operator=(ThreadPool&&) = delete;
};

2. default-thread-number

除了手动指定线程个数,更合适的做法是主动探测CPU支持的物理线程数,并以此作为执行线程个数:

class ThreadPool {
public:
explicit ThreadPool(size_t thread_num = std::thread::hardware_concurrency());
size_t ThreadCount() { return workers.size(); }
// ...
};

3. 延迟创建线程

线程不必一次就创建出来,可以等到任务到来的时候再创建,降低资源占用。

// TBD

4. 临时线程数量扩充

线程池的应用场景主要针对的是CPU密集型应用,但是遇到IO密集型场景,也要保证可用性。如果我们的线程个数固定的话,会出现一些问题,比如:

  • 几个IO任务占据了线程,并且进入了睡眠,这个时候CPU空闲,但是后面的任务却得不到处理,任务队列越来越长;
  • 几个线程在睡眠等待某个信号或者资源,但是这个信号或资源的提供者是任务队列中的某个任务,没有空闲线程,提供者永远提供此信号或资源。

    因此我们需要一种机制,临时扩充线程数量,从线程池中的睡眠线程手中“抢回”CPU。

    其实,更好的解决办法是改造线程池,使用固定个数的线程,然后把任务打包到协程中执行,当遇到IO的时候协程主动让出CPU,这样其他任务就能上CPU运行了。毕竟,多线程擅长处理的是CPU密集型任务,多协程才是处理IO密集型任务的。…… 这不就是协程库了嘛!比如 libco、libgo 就是这种解决方案。

    // TBD

5. 线程池停用启动

上面的线程池,其启动停止时机分别是构造和析构的时候,还是太粗糙了。我们为其提供手动启动、停止的函数,并支持停止之后重新启动:

// TBD


总结

不干了,2021年了,研究协程库去了!

参考文献

  1. projschj 的C++11 线程池
  2. Java线程池实现原理及其在美团业务中的实践

简单C++线程池的更多相关文章

  1. 【C/C++开发】C++实现简单的线程池

    C++实现简单的线程池 线程池编程简介: 在我们的服务端的程序中运用了大量关于池的概念,线程池.连接池.内存池.对象池等等.使用池的概念后可以高效利用服务器端的资源,比如没有大量的线程在系统中进行上下 ...

  2. java基础:简单实现线程池

    前段时间自己研究了下线程池的实现原理,通过一些源码对比,发现其实核心的东西不难,于是抽丝剥茧,决定自己实现一个简单线程池,当自已实现了出一个线程池后.发现原来那么高大上的东西也可以这么简单. 先上原理 ...

  3. Linux C 一个简单的线程池程序设计

    最近在学习linux下的编程,刚开始接触感觉有点复杂,今天把线程里比较重要的线程池程序重新理解梳理一下. 实现功能:创建一个线程池,该线程池包含若干个线程,以及一个任务队列,当有新的任务出现时,如果任 ...

  4. Linux C 实现一个简单的线程池

    线程池的定义 线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务.线程池线程都是后台线程.每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中.如 ...

  5. linux网络编程之简单的线程池实现

    转眼间离15年的春节越来越近了,还有两周的工作时间貌似心已经不在异乡了,期待与家人团聚的日子,当然最后两周也得坚持站好最后一班岗,另外期待的日子往往是心里不能平静的,越是想着过年,反而日子过得越慢,于 ...

  6. linux网络编程-一个简单的线程池(41)

    有时我们会需要大量线程来处理一些相互独立的任务,为了避免频繁的申请释放线程所带来的开销,我们可以使用线程池 1.线程池拥有若干个线程,是线程的集合,线程池中的线程数目有严格的要求,用于执行大量的相对短 ...

  7. java写的简单通用线程池demo

    首先声明,代码部分来自网络. 1.入口DabianTest: package com.lbh.myThreadPool; import java.util.ArrayList; import java ...

  8. Java一个简单的线程池实现

    线程池代码 import java.util.List; import java.util.Vector; public class ThreadPool  {     private static  ...

  9. Python简单的线程池

    class ThreadPool(object): def __init__(self, max_num=20): # 创建一个队列,队列里最多只能有10个数据 self.queue = queue. ...

随机推荐

  1. 单片机学习(一)项目的建立和vscode代码编辑环境的设置

    目录 Keil项目的建立 使用vscode进行开发 工欲善其事必先利其器,因此我们先搭建一个比较舒服的开发环境. Keil项目的建立 打开Keil软件点击Project/New uVision Pro ...

  2. Spring Cloud Gateway自定义异常处理Exception Handler

    版本: Spring Cloud 2020.0.3 常见的方法有 实现自己的 DefaultErrorWebExceptionHandler 或 仅实现ErrorAttributes. 方法1: Er ...

  3. 利用system generator 生成vivado ip—以低通滤波器举例

    前段时间自学了matlab和vivado联合推出的system generator工具,用来做数字信号处理,十分好用且使开发更便捷,下面举个例子来供大家一起学习下. 首先打开matlab命令行,输入s ...

  4. elsa-core——1.Hello World:Console

    github上有一个开源的工作流项目elsa,elsa-core是core语言的版本,其文档因为是英文,因此会让很多人不想看,或者是看不下去,从这篇文章开始我将开始对elsa-core的文档翻译成英文 ...

  5. JVM学习笔记-第七章-虚拟机类加载机制

    JVM学习笔记-第七章-虚拟机类加载机制 7.1 概述 Java虚拟机描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被 ...

  6. Windows10公钥远程连接Linux服务器

    目录 前言 一.环境准备 二.使用步骤 1.服务器安装并配置OpenSSH 2. 本地生成密钥 3. 服务器ssh添加密钥 三 总结 前言 使用公钥远程登陆Linux十分方便,无需输入密码,同时采用V ...

  7. Dart空安全的底层原理与适配

    一.在空安全推出之前,静态类型系统允许所有类型的表达式中的每一处都可以有 null. 从类型理论的角度来说,Null 类型被看作是所有类型的子类: 类型会定义一些操作对象,包括 getters.set ...

  8. .Net Core with 微服务 - 分布式事务 - TCC

    上一次我们讲解了分布式事务的 2PC.3PC .那么这次我们来理一下 TCC 事务.本次还是讲解 TCC 的原理跟 .NET 其实没有关系. TCC Try 准备阶段,尝试执行业务 Confirm 完 ...

  9. ReentrantLock中的Condition(等待和唤醒)

    Condition 类的 awiat 方法和 Object 类的 wait 方法等效 Condition 类的 signal 方法和 Object 类的 notify 方法等效 Condition 类 ...

  10. mfc 常用的知识点

    在MFC中引入了文档-视结构的概念,文档相当于数据容器,视相当于查看数据的窗口或是和数据发生交互的窗口.因此一个完整的应用一般由四个类组成:CWinApp应用类,CFrameWnd窗口框架类,CDoc ...