1. 线程池概念及应用场合
线程池:
一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
线程池的价值
- 有任务,立马有线程进行服务,省掉了线程创建的时间
- 有效防止,server中线程过多,导致系统过载的问题。
线程池的应用场景:
- 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。
但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。- 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
- 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误.
2. 线程池完整代码
让main.cc来充当server端,不断的往任务队列中派送任务,然后线程池里面的线程不断的去任务队列中拿任务,进行处理。
ThreadPool.hpp
#pragma once
#include<iostream>
#include<queue>
#include<math.h>
#include<unistd.h>
#define NUM 5
class Task
{
public:
int base;
public:
Task()
{}
Task(int _b):base(_b)
{}
void Run()
{
std::cout<<"thread is ["<< pthread_self() <<"] task run ... done: base# "<< base << " pow is # "<< pow(base,2)<<std::endl;
}
~Task()
{}
};
class ThreadPool
{
private:
std::queue<Task*> q; //线程池中需要一个任务队列,server端不断的发数据,线程池中的线程不断的处理数据,那么有可能你的Task越来越大,导致性能的损失,所以为了避免改用指针
int max_num;//你需要线程池里面有多少个线程
pthread_mutex_t lock;
pthread_cond_t cond; //你需要server和线程池中的线程保持同步的关系,当没有任务的时候,需要让线程等待,但是不可以让server端等待,如果它等待问题就大了
public:
void LockQueue()
{
pthread_mutex_lock(&lock);
}
void UnlockQueue()
{
pthread_mutex_unlock(&lock);
}
bool isEmpty()
{
return q.size() == 0;
}
void ThreadWait()
{
pthread_cond_wait(&cond,&lock);
}
void ThreadWakeUp()
{
pthread_cond_signal(&cond);
}
public:
ThreadPool(int _max = NUM):max_num(_max)
{}
//加了static 那么这个成员函数就属于类,只有一份,也就没有了this指针
//且static只能够访问static函数,没有的不能访问
static void *Routine(void *arg)// 此时他作为内部成员函数,第一个默认的参数是this指针,所以这里其实是2个参数 正常来说pthread_create的最后一个参数应该是一个,但是此时却变为了2个
{
ThreadPool* this_p = (ThreadPool*)arg;
//这是线程池里面的线程,要他们一直都存在,不销毁
while(true)
{
//线程池创造出来的目的就是去解决任务,但是又面临这同组线程之间的竞争以及在取任务的时候,server端放,所以需要一把锁
this_p->LockQueue();
while(this_p->isEmpty())//这里有可能是会被假唤醒的,所以需要循环的去判断
{
this_p->ThreadWait();
}
//如果任务队列中不是空,那就开始消化任务
Task t;
this_p->Get(t);
this_p->UnlockQueue();
t.Run();//这一步很厉害,我已经把任务从队列中拿出来了,就不需要在占有锁,在临界资源内处理了,可以先进行释放,让任务队列继续工作,我自己处理我拿出来的任务
}
}
void ThreadPoolInit()
{
pthread_mutex_init(&lock,nullptr);
pthread_cond_init(&cond,nullptr);
pthread_t t[NUM];
for(int i = 0;i<max_num;++i)
{
pthread_create(t+i,nullptr,Routine,this);
}
}
//server
void Put(Task& in)
{
//要往任务队列中塞任务
LockQueue();
q.push(&in);
UnlockQueue();
//至少要唤醒一个线程
//pthread_cond_signal pthread_cond_broadcast()唤醒所有的线程
ThreadWakeUp();
}
void Get(Task& out)
{
//调用这个函数的时候他本来就已经在临界资源了,所以不需要在加锁了
Task* t = q.front();
q.pop();
out = *t;
}
~ThreadPool()
{
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&cond);
}
};
main.cc
#include"ThreadPool.hpp"
int main()
{
ThreadPool* tp = new ThreadPool();
tp->ThreadPoolInit();
//server
while(true)
{
int x = rand() % 10 + 1;
Task t(x);
tp->Put(t);
sleep(1);
}
return 0;
}
3. 惊群效应
pthread_cond_broadcast(&cond);
一次将所有的线程都唤醒。
惊群效应(thundering herd)是指多进程(多线程)在同时阻塞等待同一个事件的时候(休眠状态),如果等待的这个事件发生,那么他就会唤醒等待的所有进程(或者线程),但是最终却只能有一个进程(线程)获得这个时间的“控制权”,对该事件进行处理,而其他进程(线程)获取“控制权”失败,只能重新进入休眠状态,这种现象和性能浪费就叫做惊群效应。
惊群效应消耗了什么?
对于CPU来说增加了负载,对于操作系统来说增加了调度的压力。