现代的软件一般都使用了多线程技术,在有些软件里面,一个线程被创建出来执行了仅仅一个任务,然后就被销毁了。线程的创建与销毁是需要消耗资源,这样为了执行单一任务而被创建出来的线程越多,性能也就越差。如果能意识到线程仅仅是负责指令流的执行,并重复利用同一个线程去执行多个函数,将线程的创建和销毁的次数控制在有限次内,频繁创建与摧毁线程这种不必要的开销就能够有效避免。

  线程池就是这样一种将线程的创建与摧毁控制在一定次数内,并利用同一线程反复执行不同人任务的技术,当然,其中的线程数不止一条。线程池中线程的个数一般和硬件线程数量一致时(暂时不考虑当前线程和线程池以外的线程数),因为当进程中的线程数与硬件线程数一致时能达到最佳并发,C++语言中,硬件线程数可以由函数std::thread::hardware_concurrency()的返回值得到,函数返回的非0即为硬件线程数。

  线程池内的线程一般在初始化时一起创建,在指定退出时一起退出。其中要执行任务的线程与外部线程通过一个线程安全的容器来实现交互,外部线程作为生产者往容器中添加任务,池中的线程作为消费者拿出线程中的任务去执行。当容器中没有任务时,池中的线程会阻塞直到得到通知并且容器中存放了任务(阻塞的线程不会得到系统调度器的调度)。

  说了这么多,来看看线程池怎么实现的吧。先来看看头文件ThreadPool.h:

#ifndef _THREADPOOL_H_
#define _THREADPOOL_H_ #include <functional>
#include <mutex>
#include <atomic>
#include <list> constexpr bool ThreadExit = true; class InterfaceTaskWithArgCommand //继承自此接口便可使用命令模式版本线程池
{
public:
virtual void Execute() = 0;
virtual void DestoryMyself() = 0;
virtual ~InterfaceTaskWithArgCommand() = 0;
}; struct TaskArgType
{
void *pArg;
int iArgLen;
TaskArgType() : pArg(nullptr), iArgLen(0)
{ }
TaskArgType(void *pData, int iLen) : pArg(pData), iArgLen(iLen)
{ }
}; struct ThreadTask final
{
enum ThreadTaskType
{
ExecWithNothing,
ExecWithArg,
ExecByCommandPattern,
Exit
}; ThreadTaskType TaskType; std::function<void()> CB; //只执行不保证被操作对象生命周期 std::function<void(TaskArgType)> ArgCB; //被操作对象的生命周期由线程池托管,需在执行函数中释放,否则造成内存泄漏
TaskArgType Arg; InterfaceTaskWithArgCommand *TaskCommand; //命令模式版本 ThreadTask()
{
}
ThreadTask(ThreadTaskType type) : TaskType(type)
{
}
ThreadTask(std::function<void()> f) : CB(f), TaskType(ThreadTaskType::ExecWithNothing)
{
}
ThreadTask(std::function<void(TaskArgType)> f, void *p, int size) : ArgCB(f), Arg(p, size), TaskType(ThreadTaskType::ExecWithNothing)
{
}
ThreadTask(InterfaceTaskWithArgCommand *Command) : TaskCommand(Command), TaskType(ThreadTaskType::ExecByCommandPattern)
{
} bool Execute()
{
bool bExit = false;
switch(TaskType)
{
case ThreadTask::ThreadTaskType::ExecWithNothing:
CB();
break;
case ThreadTask::ThreadTaskType::ExecWithArg:
ArgCB(Arg);
break;
case ThreadTask::ThreadTaskType::ExecByCommandPattern:
TaskCommand->Execute();
break;
case ThreadTask::ThreadTaskType::Exit:
default:
bExit = ThreadExit;
break;
}
return bExit;
}
}; //线程的各种个数最好由配置文件决定,配置文件根据具体场景配置
class ThreadPool
{
public:
ThreadPool(); //考虑到此类可能存在多个实例的可能(如线程池隔离的情况,不同的线程池做不同类型的事情),构造函数设计为公有
ThreadPool(int iInitThreadCount, int iHoldThreadCount, int iMaxThreadCount); ~ThreadPool() = default; void Start();
void Stop();
void AddTask(std::function<void()> Task);
void AddTask(InterfaceTaskWithArgCommand *Command);
void AddTask(std::function<void(TaskArgType)> Task, void *pData, int iLen); int GetCurTaskCount();
int GetCurThreadCount(); void SetHoldThreadCount(int iCount);
void SetMaxThreadCount(int iCount); private:
void AddThread();
void AddThreadSuitably();
void ReduceThread();
void Run();
void MontorIdle();
void SetStop(); ThreadPool(const ThreadPool& rhs) = delete;
ThreadPool(ThreadPool&& rhs) = delete;
ThreadPool& operator=(const ThreadPool& rhs) = delete;
ThreadPool& operator=(ThreadPool&& rhs) = delete; private:
int m_iInitThreadCount; //初始个数
std::atomic<int> m_aiHoldThreadCount; //保持个数
std::atomic<int> m_aiMaxThreadCount; //最大允许个数
std::atomic<int> m_aiCurThreadCount; //当前个数 std::atomic<bool> m_abStopFlag; std::mutex m_mLock;
std::list<ThreadTask> m_listTask; std::condition_variable m_ConditionVariable;
}; #endif // !_THREADPOOL_H_

  在头文件里,我定义了三种可执行的类型:命令模式类型,可携带固定参数的无返回值function类型,无参数无返回值function类型,这三个类型都是任务ThreadTask的执行方式。ThreadTask中还有多种构造方式,使用不同的构造方式,指示其可执行的类型的枚举变量也将不同,这会使得ThreadTask的Execute根据不同的类型执行不同的分支。

  从ThreadPool开始,就是线程池本身了。

  在类中,我没有将线程池设为单例模式,是考虑到在某些情况下需要线程池隔离,比如将负责网络IO与CPU密集型计算的线程池分开,避免相互影响。在线程池内,将不需要的拷贝/移动构造、拷贝/移动赋值声明为删除。m_iInitThreadCount是初始个数代表着线程池刚刚被创建出来时候池中线程的个数,这个数目不需要在运行中改变,所以可以是基本的int类型,m_aiHoldThreadCount与m_aiMaxThreadCount和m_aiCurThreadCount分别是保持线程个数,最大线程个数和当前线程个数,这三个在运行中可改变且可能同时被多个线程访问所以使用了原子的方式。这个线程池的线程数量在一定范围(m_aiMaxThreadCount)内可以动态增加以应对大量的突发任务,但是过多的线程会带来过多的调度的可能,从而影响性能,所以使用m_aiHoldThreadCount变量来指示在绝大多数情况下线程池内应该保有的线程个数。m_listTask和m_mLock是存放任务的容器和保护容易的锁,m_ConditionVariable用于实现如前所述的"当容器中没有任务时,池中的线程会阻塞直到得到通知并且容器中存放了任务"。

  接下来就是源文件了ThreadPool.cpp,一起来看一下:

#include <thread>

#include "ThreadPool.h"

ThreadPool::ThreadPool() :
m_iInitThreadCount(4), m_aiHoldThreadCount(4), m_aiMaxThreadCount(4), m_aiCurThreadCount(0), m_abStopFlag(true)
{
} ThreadPool::ThreadPool(int iInitThreadCount, int iHoldThreadCount, int iMaxThreadCount) :
m_iInitThreadCount(iInitThreadCount), m_aiHoldThreadCount(iHoldThreadCount), m_aiMaxThreadCount(iMaxThreadCount), m_aiCurThreadCount(0), m_abStopFlag(true)
{
} void ThreadPool::AddThread()
{
std::thread th(&ThreadPool::Run, this);
th.detach();
m_aiCurThreadCount++;
} void ThreadPool::AddThreadSuitably()
{
if(m_aiCurThreadCount < m_aiMaxThreadCount && m_aiCurThreadCount <= GetCurTaskCount())
{
AddThread();
}
} void ThreadPool::ReduceThread()
{
{
std::lock_guard<std::mutex> lk(m_mLock);
m_listTask.emplace_back(ThreadTask::ThreadTaskType::Exit);
} m_ConditionVariable.notify_one();
} void ThreadPool::Run()
{
bool bExit = false;
while(!m_abStopFlag && !bExit)
{
std::unique_lock<std::mutex> lk(m_mLock);
while(m_listTask.empty()) //防止虚假唤醒
{
m_ConditionVariable.wait(lk);
if(m_abStopFlag)
{
m_aiCurThreadCount--;
return;
}
} ThreadTask Task = m_listTask.front();
m_listTask.pop_front();
lk.unlock(); bExit = Task.Execute(); }
m_aiCurThreadCount--;
} void ThreadPool::Start()
{
m_abStopFlag = false;
for(int i = 0; i < m_iInitThreadCount; ++i)
{
AddThread();
}
}
//阻塞直到全部线程停止,不提供异步停止线程的方式
//停止线程的场景一般在程序结束的时候,此时如果是异步停止的话,难以保证线程池中的线程操作的目标生命周期还未结束
void ThreadPool::Stop()
{
do
{
SetStop();
} while(0 != GetCurThreadCount()); //线程池里可能跑的长函数,通知的时候可能没在等待条件变量
} void ThreadPool::AddTask(std::function<void()> Task)
{
if(!Task)
return; bool bNeedCall = false;
AddThreadSuitably(); { //此范围解锁是考虑到,wait条件变量的线程在被唤醒之后会尝试去加锁,如果还未被解锁的话,被唤醒的线程将再阻塞直到通知线程解锁,这引发了系统额外一次的调度
std::lock_guard<std::mutex> lk(m_mLock); //添加任务的线程同时可能不止一条线程
if(m_listTask.empty())
bNeedCall = true; m_listTask.emplace_back(Task);
} if(bNeedCall)
m_ConditionVariable.notify_one(); //条件标量会导致系统调用,导致用户态与内核态之前的切换,避免掉不必要的开销
} void ThreadPool::AddTask(InterfaceTaskWithArgCommand *Command)
{
if(nullptr == Command)
return; bool bNeedCall = false;
AddThreadSuitably(); {
std::lock_guard<std::mutex> lk(m_mLock); //添加任务的线程同时可能不止一条线程
if(m_listTask.empty())
bNeedCall = true; m_listTask.emplace_back(Command);
} if(bNeedCall)
m_ConditionVariable.notify_one();
} void ThreadPool::AddTask(std::function<void(TaskArgType)> Task, void *pData, int iLen)
{
if(!Task || nullptr == pData || iLen < 1)
return; bool bNeedCall = false;
AddThreadSuitably(); {
std::lock_guard<std::mutex> lk(m_mLock); //添加任务的线程同时可能不止一条线程
if(m_listTask.empty())
bNeedCall = true; m_listTask.emplace_back(Task, pData, iLen);
}
if(bNeedCall)
m_ConditionVariable.notify_one();
} int ThreadPool::GetCurTaskCount()
{
std::lock_guard<std::mutex> lk(m_mLock);
return m_listTask.size();
} int ThreadPool::GetCurThreadCount()
{
return m_aiCurThreadCount;
} void ThreadPool::MontorIdle()
{
if(m_aiHoldThreadCount < m_aiCurThreadCount)
{
//利用定时器控制线程池里空闲线程的个数
ReduceThread();
}
} void ThreadPool::SetStop()
{
if(!m_abStopFlag)
{
m_abStopFlag = true;
m_ConditionVariable.notify_all();
}
} void ThreadPool::SetHoldThreadCount(int iCount)
{
m_aiHoldThreadCount = iCount;
} void ThreadPool::SetMaxThreadCount(int iCount)
{
m_aiMaxThreadCount = iCount;
}

  先说说线程的添加部分吧。线程池中线程的创建是靠函数AddThread()来实现的,此函数做的东西很简单,就是启动一条执行Run()的线程并分离它然后对当前线程数计数。Run()则只要没有收到停止命令就一直尝试取出容器中存放的任务来执行,当容器没有任务时,条件变量就会等待wait(),直到其他存放任务的线程通知notify_one()它。可能有些朋友不太明白,条件变量为什么需要锁,我在这里解释一下,因为是多线程环境,所以取任务的容器需要有锁m_mLock保护,先说结论,条件变量在等待的时候则会释放当前的锁来给其他的线程机会去往容器存任务,我们假设下如果不释放锁的会怎样,如果不释放锁其他线程永远也没有机会往容器里存放任务了,同样等待条件变量的线程也将没有机会得其他线程的通知,而等待的线程将会因为收不到唤醒通知而永远等待,这是个死锁的局面...囧...。所以记住条件变量在等待时会释放锁来给其他线程机会,并等待其他线程通知。接着,在条件变量被唤醒后它又会尝试获取当前的锁m_mLock来得到访问容器的机会, 考虑到锁的粒度对并发性的影响,主动释放来控制粒度以获取更大的并发性lk.unlock();。这样,池里的多条线程就可以同时执行任务。函数中有对容器是否为空的循环判断并在符合条件时等待while(m_listTask.empty()),一方面这能避免虚假唤醒,另一方面,当容器中有任务时也可以避免等待,直接去取出任务来执行,直到容器中没有任务,这也能避免其他线程的不必要的通知notify_one(),这个一会说。

  接下来说说怎么向容器中添加任务的。我重载了函数AddTask()来实现往容器中添加不同类型的可执行任务,在函数的一开始,先判断下任务的合法性,空任务会引出崩溃问题,所以一定要提前处理。前面提过这个线程池可以动态增加线程数来应对突发任务的情况,这正是函数AddThreadSuitably()所做的。由于取任务的时候的做法是只要容器中存在任务,就不停取除非容器任务被全部取出,所以在存放任务时,我添加了一个标志bNeedCall来确定是否需要通知去唤醒池中的线程,很明显,当容器中还存在任务时,可以不用通知。这个函数中利用了块来控制锁的粒度,正如我的注释中所描述,是为了避免一次额外的调度。

  还是应该说说关于池中线程数量的控制,前文说过,进程中线程的个数最好和硬件线程数一致来获取最佳并发。然而在处理完突发情况之后,线程池中线程个数会根据我们设定的范围增加,所以我们应该控制线程池中线程数量的控制。利用定时器并设定合理的值,延迟销毁池中一定的空闲线程的方式比起直接销毁空闲线程来有更大的概率在销毁线程之前在遇到一次突发情况。对于线程的控制这部分,由于依赖于定时器,暂时还未能实现,后面出定时器篇的时候再补上。

  最后,来说说线程的停止Stop()部分吧,这里也有东西可以说说,由于线程池使用的是条件变量而不是信号量,就导致在通知notify_all()的时候需要池中的线程在wait()才算是有效的通知,换言之,只有在等待中的线程才会响应通知并被唤醒,所以这里我们需要循环通知while(0 != GetCurThreadCount());,正好,同步的循环通知也能保证池中还在执行的线程访问的对象生命周期有效,当然,保证这一点的条件是,线程池在对象生命周期停止前同步调用Stop()函数。

  接下来就是测试代码啦,头文件test.h很简单,就是一些函数声明

#ifndef TEST_H_
#define TEST_H_ void doTest(); void doTestThreadPool(); #endif // !TEST_H_

  源文件test.cpp如下  

#include <memory>
#include <vector>
#include <iostream>
#include <thread> #include "ThreadPool.h"
#include "test.h" static std::vector<std::function<void()>> PrepareForThreadPoolTask()
{
int a = 0;
std::vector<std::function<void()>> vecTask(10000);
for(auto& elem : vecTask)
{
elem = std::bind([](int a){
std::cout << "************************" << a << "************************" << std::endl;
},a++);
} return vecTask; //RVO
} void doTestThreadPool()
{
std::unique_ptr<ThreadPool> pThreadPool(new ThreadPool(1,1,1));
pThreadPool->Start();
pThreadPool->SetMaxThreadCount(std::thread::hardware_concurrency()); std::vector<std::function<void()>> vecTask = PrepareForThreadPoolTask();
for(auto & elem : vecTask)
{
pThreadPool->AddTask(elem);
} do
{
std::this_thread::sleep_for(std::chrono::seconds(1));
} while(pThreadPool->GetCurTaskCount()); pThreadPool->Stop();
std::system("pause");
} void doTest()
{
doTestThreadPool();
}

  运行结果如下:

  好了,今天的线程池就到这里吧,后面会持续对线程池更新,比如增加线程池中还未执行任务的取消功能,增加优先级功能,对线程池不足之处的改进,完善未完成部分。。。。。如果你喜欢的话,请持续关注我的博客园。

线程池的C++实现(一)的更多相关文章

  1. 多线程爬坑之路-学习多线程需要来了解哪些东西?(concurrent并发包的数据结构和线程池,Locks锁,Atomic原子类)

    前言:刚学习了一段机器学习,最近需要重构一个java项目,又赶过来看java.大多是线程代码,没办法,那时候总觉得多线程是个很难的部分很少用到,所以一直没下决定去啃,那些年留下的坑,总是得自己跳进去填 ...

  2. C#多线程之线程池篇3

    在上一篇C#多线程之线程池篇2中,我们主要学习了线程池和并行度以及如何实现取消选项的相关知识.在这一篇中,我们主要学习如何使用等待句柄和超时.使用计时器和使用BackgroundWorker组件的相关 ...

  3. C#多线程之线程池篇2

    在上一篇C#多线程之线程池篇1中,我们主要学习了如何在线程池中调用委托以及如何在线程池中执行异步操作,在这篇中,我们将学习线程池和并行度.实现取消选项的相关知识. 三.线程池和并行度 在这一小节中,我 ...

  4. C#多线程之线程池篇1

    在C#多线程之线程池篇中,我们将学习多线程访问共享资源的一些通用的技术,我们将学习到以下知识点: 在线程池中调用委托 在线程池中执行异步操作 线程池和并行度 实现取消选项 使用等待句柄和超时 使用计时 ...

  5. NGINX引入线程池 性能提升9倍

    1. 引言 正如我们所知,NGINX采用了异步.事件驱动的方法来处理连接.这种处理方式无需(像使用传统架构的服务器一样)为每个请求创建额外的专用进程或者线程,而是在一个工作进程中处理多个连接和请求.为 ...

  6. Java线程池解析

    Java的一大优势是能完成多线程任务,对线程的封装和调度非常好,那么它又是如何实现的呢? jdk的包下和线程相关类的类图. 从上面可以看出Java的线程池主的实现类主要有两个类ThreadPoolEx ...

  7. Android线程管理之ExecutorService线程池

    前言: 上篇学习了线程Thread的使用,今天来学习一下线程池ExecutorService. 线程管理相关文章地址: Android线程管理之Thread使用总结 Android线程管理之Execu ...

  8. Android线程管理之ThreadPoolExecutor自定义线程池

    前言: 上篇主要介绍了使用线程池的好处以及ExecutorService接口,然后学习了通过Executors工厂类生成满足不同需求的简单线程池,但是有时候我们需要相对复杂的线程池的时候就需要我们自己 ...

  9. -Android -线程池 批量上传图片 -附php接收代码

    (出处:http://www.cnblogs.com/linguanh/) 目录: 1,前序 2,类特点 3,用法 4,java代码 5,php代码 1,前序 还是源于重构,看着之前为赶时间写着的碎片 ...

  10. C#多线程--线程池(ThreadPool)

    先引入一下线程池的概念: 百度百科:线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务.线程池线程都是后台线程.每个线程都使用默认的堆栈大小,以默认的优先级运行, ...

随机推荐

  1. CodeForces - 1214D D. Treasure Island

    题目链接:https://vjudge.net/problem/2728294/origin 思路:可以抽象成管道,先试试能不能找到一个通道能通到终点, 如果可以则封锁这个通道,一个石头即可, 再试试 ...

  2. 百万年薪python之路 -- 函数名的第一类对象及使用

    函数名是一个变量, 但它是一个特殊的变量, 与括号配合可以执行函数的变量 1.1.函数名的内存地址 def func(): print("呵呵") print(func) 结果: ...

  3. rabbitmq学习-如何安装rabbitmq

    学习当然还是需要看官网地址的哈 官网地址 你可能会说老铁,看不懂英文咋办?我只能说各大翻译软件以及广大网友总有一款是你喜欢的 广大网友翻译的 中文文档 什么是rabbitmq? rabbitmq (R ...

  4. recovery模式差分(增量)升级小结

    最近在做recovery模式下的升级,简单的总结一下. 先说说recovery模式,他是个升级小系统,有单独的kernel,通过特定的系统命令就可以进入到此系统中,选择进入正常系统的kernel还是r ...

  5. 设计模式(八)Abstract Factory模式

    抽象工厂的工作是将“抽象零件”组装为“抽象产品”.在抽象工厂模式中将会出现抽象工厂,它会将抽象零件组装为抽象产品.也就是说,我们并不关心零件的具体实现,而是只关心接口.我们仅适用该接口将零件组装起来成 ...

  6. K8s 还是 k3s?This is a question

    本文来自:Rancher Labs 自k3s问世以来,社区里有许多小伙伴都问过这样的问题"除了中间的数字之外,k3s和K8s的区别在哪里?","在两者之间应该如何选择?& ...

  7. 在移动硬盘中安装win10和macos双系统

    本文通过在SSD移动硬盘中安装win10和macos双系统,实现操作系统随身携带 小慢哥的原创文章,欢迎转载 目录 ▪ 目标 ▪ 准备工作 ▪ Step1. 清空分区,转换为GPT ▪ Step2. ...

  8. Python安装pyinstaller方法,以及将项目生成可执行程序的步骤

    pyinstaller安装方法 前提:确保计算机安装了Python语言环境,并且正确配置了环境变量. 方法一:联网在线自动安装 选择一 Windows OS下进入cmd(命令行窗口) 输入:pip i ...

  9. Node.js 使用 express-jwt 解析 JWT

    Node.js 上 Token 鉴权常用的是 passport,它可以自定义校验策略,但如果你是用 express 框架,又只是解析 JWT 这种简单需求,可以尝试下 express-jwt 这个中间 ...

  10. 哪种方式更适合在React中获取数据?

    作者:Dmitri Pavlutin 译者:小维FE 原文:dmitripavlutin.com 国外文章,笔者采用意译的方式,以保证文章的可读性. 当执行像数据获取这样的I/O操作时,你必须发起获取 ...