一种线程使用模式。可以避免大面积请求引起的服务器宕机。
线程池的应用场景:
这个线程池类,需要一个队列,来存取任务,创建5个线程,让main线程塞任务,这5个线程去执行任务。多线程所以他还要有个锁,main线程放任务,需要让多个线程需要同步,所以需要一个条件变量。
ThreadPool.hpp
#include
#include
#include
#include
#include
using namespace std;
#define NUM 5
class Task
{
private:
int _a;
public:
Task(){
}
Task(int a)
:_a(a)
{
};
void run()
{
cout<<"pthread["<<pthread_self()<<"] : "<<_a <<":"<<pow(_a,2)<<endl;
}
};
class Pool
{
private:
queue<Task*> q;
int max_num;
pthread_mutex_t lock;
pthread_cond_t cond;
public:
Pool(int max=NUM)
:max_num(max)
{
}
bool IsEmpty()
{
return q.size()==0;
}
void Threadwait()
{
pthread_cond_wait(&cond,&lock);
}
void ThreadWakeUp()
{
//一次唤醒一个
// pthread_cond_signal(&cond);
pthread_cond_broadcast(&cond);
}
//外部放任务
void put(Task& in)
{
LockQueue();
q.push(&in);
UnlockQueue();
//放任务就把你唤醒,一次唤醒一个
ThreadWakeUp();
}
//线程池线程拿任务
void Get(Task& out)
{
//调用的地方加了
Task *t=q.front();
q.pop();
out=*t;
}
static void* routine(void* args)
{
Pool* this_p=(Pool*)args;
this_p->LockQueue();
while(1)
{
//为空时不拿
pthread_detach(pthread_self());
while(this_p->IsEmpty())
{
this_p->Threadwait();
}
Task t;
this_p->Get(t);
this_p->UnlockQueue();
t.run();
}
}
void PoolInit()
{
pthread_mutex_init(&lock,NULL);
pthread_cond_init(&cond,NULL);
//不关心线程id,所以用一个变量当参数就行了
pthread_t tid;
for(int i=0;i<max_num;i++)
{
//因为成员函数routine中有两个参数,这里要传函数名和形参,形参只能传一个。用static
// 又因为静态函数没有this指针,而里面又要调用成员函数,所以传递this指针
pthread_create(&tid,NULL,routine,this);
}
}
void LockQueue()
{
pthread_mutex_lock(&lock);
}
void UnlockQueue()
{
pthread_mutex_unlock(&lock);
}
};
main.cc
#include"ThreadPool.hpp"
int main()
{
Pool p;
p.PoolInit();
sleep(2);
while(true)
{
int x=rand()%100+1;
Task t(x);
p.put(t);
sleep(1);
}
return 0;
}
可以看到由于我们是一次唤醒一个线程(signal),所以他是按顺序一次执行任务,假如队列为空然后等待被唤醒,这也正说明了条件变量cond中存在一个等待队列。任务执行完成。
假如一次唤起一群的话(broadcast),由于你只有一个任务执行,其他线程又会休眠,引起性能震荡,也叫做惊群效应。
线程池,耗费少量资源,但是程序健壮性不强。(一个线程异常,整个进程崩溃)
进程池,耗费较多资源,程序健壮性较强。(进程之间具有独立性,互不影响)
在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据。
template <typename T>
class Singleton {
static T data;
public:
static T* GetInstance()
{
return &data;
}
};
由于他是一个静态对象,所以类加载的时候,他就会创建出来,用的时候通过这个类的成员方法,直接可以拿到地址使用。但是存在一个问题,假如程序中存在大量的不同单例,类加载的时候,启动会十分慢
template <typename T>
class Singleton {
static T* inst;
public:
static T* GetInstance() {
if (inst == NULL)
{
inst = new T();
}
return inst;
}
};
他是一个静态指针,所以类加载的时候不会创建对象,当需要用这个对象时,调用方法才会创建出来供我们使用。所以它的核心思想是"延时加载",从而能够优化服务器的启动速度。
饿汉是不存在线程安全的,因为类加载,对象已经创建地址唯一,即使多个线程进来也只是拿到他的地址。
而懒汉呢,当多个线程进来,对象还没有创建,假如同时判空,这样就创建了多个对象。那就不是单例模式了。所以需要加锁。
// 懒汉模式, 线程安全
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;
}
};
双重判空是因为,假如不同时进来,就没有必要加锁,所以在外面再加一个判定,假如另一个线程进来想创建对象就会自己返回,假如同时进来大不了我再加个锁。其实不同时进来的概率是更高的,同时进来我们的锁也可以处理,所以效率更高。
STL是为有线程安全问题的,原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响.而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶).
因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全.
对于 unique_ptr
, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题.
对于 shared_ptr
, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数.
在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。所以之前用的到全部是悲观锁
每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,
会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
Compare and Swap
当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
对于这个问题,也可以简单总结。
3种关系:
与生产者,消费者不同的是,消费者是会取走数据的。
有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁
因为应用场景,可以分为3类,读优先,写优先,公平。但是常见于读多写少的场景。
以读优先为例,他是怎么实现的呢?
当第一个读者进来,rc==1,所以对写者加锁,这样就实现了写者线程阻塞,读者线程优先。注意判断的时候由于可能多个线程同时进入判断,需要加锁。当读取完数据后,在进行判断如果读者线程为0,那么就对写者线程解锁。
之前,信号量,互斥锁,条件变量,申请不到就一直阻塞。
而自旋锁,spin,因为占有临界资源的线程,在临界区呆的时间特别短,无需挂起,让当前线程处于自旋状态,不断去检测锁的状态。自旋锁为我们提供了上述功能。锁在操作系统都是汇编实现的。而实际应用场景可以根据任务要求在临界区的时间长短,来区分使用哪种锁。