全文约 3036 字,预计阅读时长: 9分钟
线程池
线程池:一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着
监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
线程池的应用场景:1. 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个
Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。2. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误.
- 线程池示例:
- 创建固定数量线程池,循环从任务队列中获取任务对象,
- 获取到任务对象后,执行任务对象中的任务接口。
- 小结:创建线程是有成本的,需要时再创建是比较慢的。所以线程池一次预先创建一大批线程,让这些线程处于“待机状态”;一旦由数据或者任务,直接可以交给线程去处理。
懒汉模式
- 设计模式:大佬们针对一些经典的常见的场景, 给定了一些对应的解决方案。
- 单例模式:某些类, 只应该具有一个对象(实例)。
- 饿汉模式:吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭。
- 懒汉方式.:吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗。
- 懒汉方式最核心的思想是 “延时加载”. 从而能够优化服务器的启动速度。
volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。
// 懒汉模式, 线程安全
template <typename T>
class Singleton {
volatile static T* inst; // 需要设置 volatile 关键字, 否则可能被编译器优化.
static std::mutex lock;
public:
static T* GetInstance()
{
if (inst == NULL)
{ // 双重判定空指针, 降低锁冲突的概率, 提高性能.
lock.lock(); // 使用互斥锁, 保证多线程情况下也只调用一次 new.
if (inst == NULL)
{
inst = new T();
}
lock.unlock();
}
return inst;
}
};
- 懒汉模式与内存池的结合:
- 由于在类的内部使用成员函数,所以 要将线程的 handler 设置成 static。
- 由于 static 的函数只能访问 static 的变量或函数,创建线程时函数的参数传 this ,就可以访问类的内部成员。
pthread_detach()和pthread_join()就是控制子线程回收资源的两种不同的方式。同一进程间的线程具有共享和独立的资源,其中共享的资源有堆、全局变量、静态变量、文件等公用资源。而独享的资源有栈和寄存器,这两种方式就是决定子线程结束时如何回收独享的资源。
如果是joinable状态,则该线程结束后(通过pthread_exit结束或者线程执行体任务执行完毕)不会释放线程所占用堆栈和线程描述符(总计8K多)等资源,除非在主线程调用了pthread_join函数之后才会释放。pthread_join函数一般应用在主线程需要等待子线程结束后才继续执行的场景。(pthread_join是一个阻塞函数,调用方会阻塞到pthread_join所指定的tid的线程结束后才被回收,但是在此之前,调用方是霸占系统资源的。 )
如果是unjoinable状态,则该线程结束后会自动释放占用资源。实现方式是在创建时指定属性,或者在线程执行体的最开始处添加一行:pthread_detach(pthread_self());不会阻塞,调用它后,线程运行结束后会自动释放资源,后者非常方便。
#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
template <class T>
class ThreadPool{
private:
std::queue<T> q; //给线程池派发任务的地点, 临界资源
pthread_mutex_t lock;
pthread_cond_t cond;
private:
ThreadPool()
{
pthread_mutex_init(&lock, nullptr);
pthread_cond_init(&cond, nullptr);
}
ThreadPool(const ThreadPool<T>&) = delete;
ThreadPool<T>& operator = (const ThreadPool<T>&) = delete;
static ThreadPool<T> *instance;
public:
static ThreadPool<T> *get_instance()
{
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; //静态初始化
if(nullptr == instance)
{
pthread_mutex_lock(&mtx);
if(nullptr == instance)
{
instance = new ThreadPool<T>();
}
pthread_mutex_unlock(&mtx);
}
return instance;
}
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);
}
void PopTask(T *out)
{
*out = q.front();
q.pop();
}
//Routinue是类中的一个成员方法!包含了一个隐士参数this!ThreadPool*
//实际上,这里是包含了两个参数的!
static void *Routinue(void *args/*,ThreadPool *this*/)
{
pthread_detach(pthread_self()); //线程分离
ThreadPool *tp = (ThreadPool*)args;
while(true){
tp->LockQueue();
//1. 检测是否有任务
while(tp->IsEmpty())
{
tp->ThreadWait();
}
//2. 取任务的过程
T t;
tp->PopTask(&t);
tp->UnlockQueue();
//3。处理任务
t();
}
}
void InitThreadPool(int num)
{
for(auto i = 0; i < num; i++){
pthread_t tid;
pthread_create(&tid, nullptr, Routinue, this);
}
}
void PushTask(const T &in)
{
//放任务
LockQueue();
q.push(in);
ThreadWakeup();
UnlockQueue();
}
~ThreadPool()
{
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&cond);
}
};
template<class T>
ThreadPool<T>* ThreadPool<T>::instance = nullptr;
- Task.hpp
pthread_detach()即主线程与子线程分离,两者相互不干涉,子线程结束同时子线程的资源自动回收。
pthread_join()即是子线程合入主线程,主线程会一直阻塞,直到子线程执行结束,然后回收子线程资源,并继续执行。
#pragma once
#include <iostream>
#include <pthread.h>
class Task{
private:
int x;
int y;
char op; //+-*/%
public:
Task() //无参构造一定要有的
{}
Task(int _x, int _y, char _op):x(_x),y(_y),op(_op)
{}
void operator()()
{
run();
}
void run()
{
int z = -1;
switch(op){
case '+':
z = x + y;
break;
case '-':
z = x - y;
break;
case '*':
z = x * y;
break;
case '/':
if(0 != y) z = x / y;
else std::cout << "Warning: div zero!" <<std::endl;
break;
case '%':
if(0 != y) z = x % y;
else std::cout << "Warning: div zero!" <<std::endl;
break;
default:
std::cout << "unknow operator!" << std::endl;
break;
}
std::cout <<"thread "<< "[" << pthread_self() << "] handler task done : "<< x << op << y << "=" << z << std::endl;
}
~Task(){}
};
- main.cc
#include "thread_pool.hpp"
#include "task.hpp"
#include <time.h>
#include <unistd.h>
#define NUM 5
int main()
{
srand((unsigned)time(nullptr));
ThreadPool<Task> *tp = ThreadPool<Task>::get_instance();
tp->InitThreadPool(5);
sleep(3);
const std::string ops = "+-*/%";
while(true){
int x = rand() % 50 + 1;
int y = rand() % 50 + 1;
char op = ops[rand()%5];
Task t(x, y, op);
tp->PushTask(t);
sleep(1);
}
return 0;
}
自旋锁
- 是否采用自旋锁,取决于上一个线程在临界区中执行的时长。挂起是有成本的。
读写锁
在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。
- 读者与读者之间没有关系;写者和写者之间是互斥关系;读者和写者是同步互斥的关系。
- 读者优先:读者和写者一起到来时,会让读者优先进入。
- 写者优先:当写者到来时,后续读者就暂时不能进入临界资源进行读取了,所有正在读取的线程执行完毕,写着再进入。
- 综上:谁优先,取决于是要读到新数据,还是旧数据。
寄语
先这样吧…