Linux多线程_(线程池,读者写者,自旋锁)
Posted LHlucky_2
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux多线程_(线程池,读者写者,自旋锁)相关的知识,希望对你有一定的参考价值。
文章目录
1.线程池概念
- 一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。
线程池不仅能够保证内核的充分利用,还能防止过分调度
。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
2.线程池概念图
- 没有线程池好比银行业务窗口只有一个,那么人越多,等待时间越长,效率越低。内核就好比单一窗口里面的服务人员,长时间大量处理业务,难免会出现问题。线程池的概念就好比多开放几个窗口,来节省时间,提高效率。
- 在服务器接收来自用户发来的任务时,需要创建线程去处理任务,而这都需要花时间为代价,如果任务队列接受一个任务创建一个线程的去处理任务,那么在面对大量任务的同时,对用户来说,需要等待很长时间,效率很低,对内核来说,需要频繁的内核申请,创建线程,销毁线程。造成内核过度调用,降低性能与效率。
3.线程池的应用场景
需要大量的线程来完成任务,且完成任务的时间比较短。
WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。对性能要求苛刻的应用
,比如要求服务器迅速响应客户请求。接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用
。
4.线程池的种类
- 创建固定数量线程池,循环从任务队列中获取任务对象,
- 获取到任务对象后,执行任务对象中的任务接口
5.代码示例
//ThreadPool.hpp
#pragma once
#include<iostream>
#include<queue>
#include<math.h>
#include<unistd.h>
#define NUM 5 //线程数量
class Task //任务(求一个数的平方)
{
private:
int base;
public:
Task()
{}
Task(int _b):base(_b)
{}
void Run()
{
std::cout<< "pthread id:["<< pthread_self()<<"] "<<base<< ":" <<"pow is: "<< pow(base,2) <<std::endl;
}
~Task()
{}
};
class ThreadPool
{
private:
std::queue<Task*> q; //任务队列
int max_num; //线程数量
pthread_t *t; //线程数组
pthread_mutex_t lock; //互斥锁
pthread_cond_t cond;//只有消费者需要环境变量,生产者不需要,如果需要说明环形队列任务已满。
public:
void LockQueue()
{
pthread_mutex_lock(&lock);
}
bool Isempty()
{
return q.size()==0;
}
void ThreadWait()
{
pthread_cond_wait(&cond,&lock);
}
void UnLockQueue()
{
pthread_mutex_unlock(&lock);
}
void ThreadWakeup()
{
//pthread_cond_signal(&cond);
pthread_cond_broadcast(&cond);
}
public:
ThreadPool(int _max = NUM):max_num(_max)
{}
static void *Rountine(void* arg)
{
ThreadPool *tp = (ThreadPool*)arg;
while(1)
{
sleep(2);
tp->LockQueue(); // 给任务队列上锁,保持互斥
while(tp->Isempty())
{
tp->ThreadWait(); //如果为空则挂起(没有任务线程无序处理等待任务到来)
}
Task t;
tp->Get(t); // 有任务并拿走任务
tp->UnLockQueue(); // 解锁
t.Run(); // 处理任务
}
sleep(2);
}
void ThreadPoolInit()
{
pthread_mutex_init(&lock,nullptr); //初始化锁
pthread_cond_init(&cond,nullptr); //初始化环境变量
pthread_t *t=new pthread_t[max_num]; // 创建线程数组
for(int i=0;i<max_num;i++)
{
pthread_create(t+i,nullptr,Rountine,this); //创建线程
}
}
void Put(Task &in) // 给任务队列里面放任务
{
LockQueue();
q.push(&in);
UnLockQueue();
ThreadWakeup();
}
void Get(Task &out) //拿任务
{
Task *t = q.front();
q.pop();
out = *t;
}
~ThreadPool()
{
pthread_mutex_destroy(&lock); //销毁锁
pthread_cond_destroy(&cond); //销毁环境变量
delete []t; //销毁数组
}
};
//main.cpp
#include "ThreadPool.hpp"
int main()
{
ThreadPool *tp = new ThreadPool(5); //创建线程池
tp->ThreadPoolInit(); //初始换线程池
while(1) //主线程创建任务
{
int x=rand()%10 + 1; //计算1到10之间随机数的平方
Task t(x);
tp->Put(t); //将任务放进任务队列
sleep(1);
}
return 0;
}
5.1 运行结果
5.1.1 只唤醒单个线程
5.1.2 一次唤醒所有线程
惊群效应
:- 惊群效应(thundering herd)是指多进程(多线程)在同时阻塞等待同一个事件的时候(休眠状态),如果等待的这个事件发生,那么他就会唤醒等待的所有进程(或者线程),但是最终却只能有一个进程(线程)获得这个时间的“控制权”,对该事件进行处理,而其他进程(线程)获取“控制权”失败,只能重新进入休眠状态,
这种现象和性能浪费就叫做惊群效应
。
6.线程池存在价值
- 有任务,立马有线程进行服务,省掉了线程创建的时间。
- 有效防止服务器中线程过多,导致系统过载的问题。
6.1 线程池 VS 进程池
- 线程池占用资源更少,但是健壮性(鲁棒性)不强。
- 进程池占用资源更多,但是健壮性(鲁棒性)很强。
7. 线程安全的单例模式
7.1 什么是单例模式
- 单例模式是一种 “经典的, 常用的, 常考的”
设计模式
。
7.2 什么是设计模式
- IT行业这么火, 涌入的人很多. 俗话说林子大了啥鸟都有. 大佬和菜鸡们两极分化的越来越严重. 为了让菜鸡们不太拖大佬的后腿, 于是大佬们针对一些经典的常见的场景, 给定了一些对应的解决方案, 这个就是 设计模式。
7.4 单例模式的特点
- 某些类, 只应该具有一个对象(实例), 就称之为单例,例如一个男人只能有一个媳妇。
- 在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据。
8. 其他常见的各种锁
悲观锁
:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。乐观锁
:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。CAS操作
:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。自旋锁,公平锁,非公平锁
。
9. 读者写者问题
- 在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。
注意:写独占,读共享,读锁优先级高
。
9.1 生产者消费者 VS 读者写者
读者写者
:
- 读者读者之间是共享关系。读者写者之间是互斥同步关系。写者之间互斥关系。
- 生产者消费者VS读者写者:消费者会取走数据,而读者不会,只看到数据就行。
9.2 读写锁接口
读写锁的初始化与销毁
:
- 参数rwlock表示的是一个读写锁,attr是读写锁的属性,一般设置为NULL;
读加锁
:
写加锁
:
- rdlock若申请不到锁,则自旋,tryrdlock若申请不到锁,则返回,由用户自旋;
解锁
:
10.自旋锁
自旋锁的类型
:
- 普通自旋锁
- 读写自旋锁
- big-reader自旋锁
10.1 自旋锁接口
初始化以及销毁锁
:
上锁
:
解锁
:
10.2 自旋锁缺点
- 如果线程执行的任务需要非常长的时间,或者线程对共享数据的竞争相当激烈,那么使用自旋锁的效率就很低。因为自旋的过程中,一直无法获取到锁,一直在白白的浪费CPU的资源。
- 可能引起死锁
- 过多的占用CPU资源
10.3 自旋锁与互斥锁的异同:
- 自旋锁与互斥锁都是为对临界资源进行保护而创造的一种锁机制。不同之处在于,执行单元在持有互斥锁期间,其它需要该资源的执行单元是要进入睡眠状态的。而执行单元持有自旋锁期间,其它需要该资源的执行单元是进行不断的尝试的,直到持有自旋锁的单元释放自旋锁,则可以获得该资源。自旋锁这个名字也是由此得来。而且,由于需要获得该锁的单元不断进行尝试,所以,自旋锁的效率是远高于互斥锁的。
以上是关于Linux多线程_(线程池,读者写者,自旋锁)的主要内容,如果未能解决你的问题,请参考以下文章
LINUX多线程(线程池,单例模式,线程安全,读者写者模型)