目录
[3.面试题: 线程池执行任务的耗时](#3.面试题: 线程池执行任务的耗时)
1.线程池的定义
从名称上看,是一批预先实例化线程,这些线程处于空闲状态,它们随时准备接收任务,这样可以避免在频繁创建线程时产生的开销,一次性创建一大批线程,这样能减少系统调用的次数,以空间换时间
详细解释: 线程过多会带来调度开销,进而影响缓存局部性和整体性能,而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务
这避免了在处理短时间任务时创建与销毁线程的代价,线程池不仅能够保证内核的充分利用,还能防止过分调度
可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量
可以用容器存储这些线程,比如队列、链表等
可以看看stackoverflow的回答: multithreading - What is a thread pool? - Software Engineering Stack Exchange
与生产者-消费者模型的关系
生产者向线程池提供任务,线程池选择线程执行任务,消费者向线程取得执行结果
2.线程池的应用场景
- 需要大量的线程来完成任务,且完成任务的时间比较短
比如WEB服务器完成网页请求这样的任务,因为单个任务小,而任务数量巨大,想象一个热门网站的点击次数,使用线程池是非常好的,但对于长时间的任务,线程池的优点就不明显了
- 对性能要求严苛的应用,比如要求服务器迅速响应客户请求,线程已经创建好了,减少延时
3.接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用,减少延时
3.代码
主线程分两步: 1.构建任务 2.交给线程池处理
准备工作
新建以下文件:
cpp
thread_pool/
├── makefile
└── thread_pool.cpp
thread_obj类
线程池存储的是一个个实例化后的thread_worker类
cpp
struct thread_worker
{
std::string _name;
pthread_t _tid;
};
thread_pool类
成员变量
由于线程池需要存储一个个实例化后的thread_worker类,这里使用vector容器存储:
cpp
std::vector<thread_worker> _pool;
根据之前OS78.【Linux】线程互斥(6) 基于阻塞队列的单生产者-单消费者模型(初步版本)和OS79.【Linux】线程互斥(7) 基于阻塞队列的多生产者-多消费者模型的文章,使用互斥锁+条件变量保护线程池:
cpp
pthread_mutex_t _lock;
pthread_cond_t _cond;
成员函数
构造函数
说明需要创建几个线程、初始化锁和条件变量
cpp
thread_pool(int num=5)
{
pthread_mutex_init(&_lock,nullptr);
pthread_cond_init(&_cond,nullptr);
_pool.resize(num);
create_thread();
}
create_thread是去创建线程
析构函数
销毁锁、条件变量、线程池
cpp
~thread_pool()
{
pthread_mutex_destroy(&_lock);
pthread_cond_destroy(&_cond);
_pool.clear();//这里主线程不等待新线程
}
create_thread
cpp
//private
void create_thread()
{
for (int i=0;i<_pool.size();i++)
{
std::string name="thread-"+std::to_string(i);
_pool[i]._name=name;
pthread_create(&(_pool[i]._tid),nullptr,handle_task,&_pool[i]);
}
}
线程函数是handle_task,线程创建好了就会自己去执行这个handle_task函数
handle_task
测试线程是否成功执行
这样写handle_task会报错:
cpp
void* handle_task(void* args)
{
pthread_detach(pthread_self());
for (;;)
{
printf("0x%X线程正在运行\n",thread_self());
sleep(1);
}
return nullptr;
}

原因: 非静态成员函数的第一个隐含的参数是this指针,是自动传入的(之前在CD11.【C++ Dev】类和对象(2)文章讲过this指针),但是POSIX标准规定,线程函数只能有一个参数,且参数必须是void*类型,所以gcc报错: 函数类型配不上
解决方法: 写成静态成员函数或者放类外面,这里用前者
cpp
static void* handle_task(void* args)
{
pthread_detach(pthread_self());
for (;;)
{
printf("0x%X线程正在运行\n",pthread_self());
sleep(1);
}
return nullptr;
}
运行结果: 此时尚未为线程分配任务

让线程池执行任务
主线程负责放任务
新线程执行handle_task,从任务队列中取任务,如果没有任务,就到条件变量下的等待队列中等待,否则取出队头的任务执行
设置任务队列:
cpp
std::queue<T> _tasks;
注意: STL库的所有容器不保证线程安全,需要程序员自己维护
cpp
for (;;)
{
while (_tasks.empty()) //防止伪唤醒
thread_sleep();
T t=_tasks.front();
_tasks.pop();
t();
}
加锁保证_tasks容器的线程安全,访问_tasks前需要加锁,if判断_tasks容器是否为空,访问了_tasks容器,因此加锁在if判断之前,解锁放在t()前,没有必要将解锁放在t()后,临界区越小,并发度越高
那么:
cpp
for (;;)
{
pthread_mutex_lock(&_lock);
while (_tasks.empty()) //防止伪唤醒
thread_sleep();
T t=_tasks.front();
_tasks.pop();
pthread_mutex_unlock(&_lock);
t();
}
thread_sleep即将线程放到条件变量下的等待队列中等待,不为空则主线程用threa_wakeup唤醒新线程
cpp
void thread_sleep()
{
pthread_cond_wait(&_cond,&_lock);
}
void thread_wakeup()
{
pthread_cond_signal(&_cond);
}
如果像下面这样写就有问题,由于handle_task是静态函数,没有this指针,那么就不好访问类内的成员
cpp
static void* handle_task(void* args)
{
pthread_detach(pthread_self());
for (;;)
{
pthread_mutex_lock(&_lock);
while (_tasks.empty()) //防止伪唤醒
{
printf("0x%lX线程: 暂无任务,去等待\n",pthread_self());
thread_sleep();
}
T t=_tasks.front();
_tasks.pop();
pthread_mutex_unlock(&_lock);
t();
}
return nullptr;
}
★解决方法: 通过pthread_create传入this
cpp
void create_thread()
{
for (int i=0;i<_pool.size();i++)
{
std::string name="thread-"+std::to_string(i);
_pool[i]._name=name;
pthread_create(&(_pool[i]._tid),nullptr,handle_task,this);
}
}
args就是this指针,指向实例化的thread_pool对象
cpp
static void* handle_task(void* args)
{
thread_pool<T>* ptr=static_cast<thread_pool<T>*>(args);
pthread_detach(pthread_self());
for (;;)
{
pthread_mutex_lock(&(ptr->_lock));
while (_tasks.empty()) //防止伪唤醒
{
printf("0x%lX线程: 暂无任务,去等待\n",pthread_self());
ptr->thread_sleep();
}
T t=(ptr->_tasks).front();
(ptr->_tasks).pop();
pthread_mutex_unlock(&(ptr->_lock));
t();
}
return nullptr;
}
push
作用: 将任务加入任务队列
任务加入任务队列设计访问临界资源,需要加锁; 将任务加入任务队列,那么任务队列一定不为空,那么主线程就能唤醒在条件变量下的等待队列中的线程了
cpp
void push(const T& t)
{
pthread_mutex_lock(&_lock);
_tasks.push(t);
thread_wakeup();
pthread_mutex_unlock(&_lock);
}
设计任务类
这里仅模拟,不分配真的任务
cpp
class Task
{
public:
void operator()(void)
{
int i=rand()%100;
printf("0x%lX线程: 执行第%d号任务\n",pthread_self(),i);
}
};
main函数
cpp
int main()
{
srand((unsigned int)time(0));
thread_pool<Task> tp;
for (;;)
{
Task t;
sleep(1);
tp.push(t);
sleep(1);
}
return 0;
}
运行结果:

3.面试题: 线程池执行任务的耗时
2025途虎一面 https://www.nowcoder.com/feed/main/detail/67330107da9c48bda098a2db3c5ae0b7
感谢胖乎乎的喜羊羊很想回老家牛友提供面试题!
第5题:
作答
分析官方文档
"核心线程数、最大线程数"是Java中的Class ThreadPoolExecutor里面的,C++开发人员如果要设计线程池,其实这些东西也是绕不开的
比如线程池启动的时候,初始线程数是多少,如果任务量大的话,可以让线程池创建新的线程,那最大的线程数就是对创建新线程的一个限制,什么时候就不能再创建新线程了......
官方文档: ThreadPoolExecutor_(Java_SE_25_&_JDK_25)
这里摘录需要用的内容:
Core and maximum pool sizes
A
ThreadPoolExecutorwill automatically adjust the pool size (see getPoolSize()) according to the bounds set by corePoolSize (see getCorePoolSize()) and maximumPoolSize (see getMaximumPoolSize()). When a new task is submitted in method execute(Runnable), if fewer than corePoolSize threads are running, a new thread is created to handle the request, even if other worker threads are idle. Else if fewer than maximumPoolSize threads are running, a new thread will be created to handle the request only if the queue is full. By setting corePoolSize and maximumPoolSize the same, you create a fixed-size thread pool. By setting maximumPoolSize to an essentially unbounded value such asInteger.MAX_VALUE, you allow the pool to accommodate an arbitrary number of concurrent tasks. Most typically, core and maximum pool sizes are set only upon construction, but they may also be changed dynamically using setCorePoolSize(int) and setMaximumPoolSize(int).On-demand construction
By default, even core threads are initially created (惰性分配) and started only when new tasks arrive, but this can be overridden dynamically using method prestartCoreThread() or prestartAllCoreThreads(). You probably want to prestart threads if you construct the pool with a non-empty queue.
Queuing
Any BlockingQueue may be used to transfer and hold submitted tasks. The use of this queue interacts with pool sizing:
- If fewer than corePoolSize threads are running, the Executor always prefers adding a new thread rather than queuing.
- If corePoolSize or more threads are running, the Executor always prefers queuing a request rather than adding a new thread.
- If a request cannot be queued, a new thread is created unless this would exceed maximumPoolSize, in which case, the task will be rejected.
ThreadPoolExecutor对象依据**corePoolSize(核心线程数)和maximumPoolSize(最大线程数)**调整线程池大小
默认情况下,核心线程是惰性分配的,只有当任务来了,核心线程才会创建并执行任务
线程池中如果运行着线程,这些线程可能处于工作状态,也可能处于空闲状态,当一个新任务被提交时,需要分类讨论:
1.当线程数量<核心线程数时,线程池会增加一个新的线程去执行这个任务,即使其他线程处于空闲状态,也会创建新线程来处理任务
2.核心线程数<=线程数量<最大线程数成立,那么仅当队列(后面会解释这个队列)已满时,才会创建一个新线程来处理任务 → 推论: 线程池中的线程数==核心线程数,优先将任务放入阻塞队列
上面提到的队列可以是任何存储任务的阻塞队列(BlockingQueue)
如果线程池中的线程数量 < 核心线程数,Executor 始终倾向于添加新线程而不是排队
如果线程池中的线程数量 >= 核心线程数,Executor 始终倾向于将请求排队而不是添加新线程
如果请求无法排队(比如队列满了),则会创建新线程,除非这会导致超出最大线程数,这种情况下,任务将被拒绝(不执行任务!!! 而不是暂时等一等)
得出结论
结论: Java的ThreadPoolExecutor线程池的处理算法: 任务来了,线程池先让核心线程执行,如果任务多了,放阻塞队列中,如果任务还多,线程增加至最大线程数,如果任务还多,就拒绝剩下的任务
回到本题
| 参数 | 值 |
|---|---|
| 核心线程数 | 5 |
| 最大线程数 | 10 |
| 队列容量 | 30 |
| 任务总数 | 45 |
| 单个任务耗时 | 50ms |
一开始创建线程池对象,由于尚未分配任务,线程池中没有线程在运行,线程池的线程数为0
来了45个任务,依据"核心线程是惰性分配的",而且核心线程数corePoolSize为5,那么线程池会立即创建5个核心线程去执行任务(0<5),但还有40个任务尚未执行,根据"推论: 线程池中的线程数==核心线程数,优先将任务放入阻塞队列 "和"如果线程池中的线程数量 >= 核心线程数,Executor 始终倾向于将请求排队而不是添加新线程",由于队列容量是30,那么就放30个任务进入队列:

还剩下40-30=10个任务,根据"如果请求无法排队(比如队列满了),则会创建新线程,除非这会导致超出最大线程数,这种情况下,任务将被拒绝(是不执行任务!!! 而不是暂时等一等)"
当前线程池的线程数量为5,最大线程数为10,那么会再创建5个线程执行任务,剩下的5个任务就拒绝执行了:

10个线程并发执行任务,阻塞队列中有30个任务在排队,t1=50ms
阻塞队列中的任务依次交给10个线程处理,t2=(30/10)*50ms=150ms,那么总耗时==t1+t2==200ms

