目录
线程池:
什么是线程池:
一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理内核、内存、网络sockets等的数量。
应用场景:
- 需要大量的线程来完成任务,且完成任务的时间比较短。web服务器完成网页请求这样的任务,使用线程池技术非常合适。因为单个任务小,而任务数量巨大,可以想象一个热门网站的点击次数。但对于长时间的任务,比如一个Telent链接请求,线程池的优点就不明显了。因为Telent会话时间比线程的创建时间大多了。
- 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
- 接收突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误。
原理:
1、线程安全队列:
线程安全: 互斥 + 同步
队列:先进先出
元素:待处理的数据、处理数据的方法
2、一堆线程
代码实现:
要完成的事:
- 创建固定数量的线程池,循环从任务队列中获取任务对象
- 获取到任务对象后,执行任务对象中的任务接口
1 #include<stdio.h>
2 #include<pthread.h>
3 #include<queue>
4 #include<unistd.h>
5
6 using namespace std;
7
8 typedef void (*Handler)(int data);
9 //创建队列处理数据
10 class QueueData{
11 public:
12 QueueData(){}
13 QueueData(int data, Handler Handler){
14 _data = data;
15 _handler = Handler;
16 }
17 void run(){
18 _handler(_data);
19 }
20 private:
21 int _data;
22 Handler _handler;
23 };
24 #if 1
25 //线程池
26 class Threadpool{
27 public:
28 Threadpool(){}
29 Threadpool(int capa, int thread_count){
30 _capacity = capa;
31 _thread_count = thread_count;
32 pthread_mutex_init(&_lock, NULL);
33 pthread_cond_init(&_cons_cond, NULL);
34 pthread_cond_init(&_prod_cond, NULL);
35 flag_exit = 0;
36 }
37 ~Threadpool(){
38 pthread_mutex_destroy(&_lock);
39 pthread_cond_destroy(&_cons_cond);
40 pthread_cond_destroy(&_prod_cond);
41 }
42 int OnInit(){
43 int count; //线程创建失败的数量
44 for(int i = 0; i < _thread_count; i++){
45 pthread_t tid;
46 int ret = pthread_create(&tid, NULL, ThreadPollStart, (void*)this);
47 if(ret < 0){
W> 48 count++;
49 }
50 }
51 return _thread_count -= count;
52 }
53 void push(QueueData qData){
54 pthread_mutex_lock(&_lock);
55 while(_que.size() >= _capacity){
56 if(flag_exit){
57 //告诉生产者别生产了
58 return;
59 }
60 pthread_cond_wait(&_prod_cond, &_lock);
61 }
62 _que.push(qData);
63 pthread_mutex_unlock(&_lock);
64 pthread_cond_signal(&_cons_cond);
65 }
66 void pop(QueueData* qd){
67 *qd = _que.front();
68 _que.pop();
69 }
70 //因为线程入口函数只能有一个参数,成员函数会有一个默认的this指针,所以设置为静态函数
71 static void* ThreadPollStart(void* arg){
72 pthread_detach(pthread_self());
73 //要把线程池传进来,也就是this指针
74 Threadpool* tp = (Threadpool*)arg;
75 while(1){
76 pthread_mutex_lock(&tp->_lock);
77 while(tp->_que.empty()){
78 if(tp->flag_exit){
79 tp->_thread_count--;
80 pthread_mutex_unlock(&tp->_lock);
81 pthread_exit(NULL);
82 }
83 pthread_cond_wait(&tp->_cons_cond, &tp->_lock);
84 }
85 QueueData qd;
86 tp->pop(&qd);
87 pthread_mutex_unlock(&tp->_lock);
88 pthread_cond_signal(&tp->_prod_cond);
89 qd.run();
90 }
91 return NULL;
92 }
93 //线程退出
94 void ThreadpoolExit(){
95 flag_exit = 1;
96 while(_thread_count){
97 //循环通知消费者线程,此时生产者停止了生产,等待队列中可能还有消费者线程
98 pthread_cond_signal(&_cons_cond);
99 }
100 }
101 private:
102 //一个线程安全队列
103 queue<QueueData> _que;
104 size_t _capacity;
105
106 pthread_mutex_t _lock;
107 pthread_cond_t _cons_cond;
108 pthread_cond_t _prod_cond;
109
110 int _thread_count;
111 int flag_exit;
112 };
113 void DealData(int data){
114 printf("data = %d\n", data);
115 }
116
117 #endif
118 int main(){
119 Threadpool* tp = new Threadpool(10, 10);
120 if(tp == NULL){
121 printf("create fail\n");
122 return 0;
123 }
124 if(tp->OnInit() <= 0){
125 printf("create thread fail\n");
126 return 0;
127 }
128 for(int i = 0; i < 10000; i++){
129 QueueData qd(i, DealData);
130 tp->push(qd);
131 }
132
133 tp->ThreadpoolExit();
134 delete tp;
135 return 0;
}
单例模式
1、设计模式
什么是设计模式:
设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的实验和错误总结出来的。
分类:
- 创建型模式:这些模式提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用new运算符直接实例化对象。这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活。例如:工厂模式(Factory Pattern)、单例模式(Singleton Pattern)
- 结构型模式:这些设计模式关注类和对象的组合。继承的概念被用来组合接口和定义组合对象获得新功能的方式。例如:适配器模式(Adapter Pattern)、桥接模式(Bridge Pattern)
- 行为型模式:这些设计模式特别关注对象之间的通信。例如:命令模式(Command Pattern)、观察者模式(Observer Pattern)
2、单例模式
1、单例类只能有一个实例。(在整个软件当中就只有一个实例对象)
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁地创建与销毁。
何时使用:当你想控制实例数目,节省系统资源地时候。
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。构造函数时私有的。
3、饿汉模式
在程序启动的时候就创建了唯一的实例对象,饿汉模式不需要加锁。
1 #include<stdio.h>
2
3 class singleton{
4 private:
5 singleton(){}
6 static singleton* st;
7 public:
8 static singleton* GetInstance();
9 void print(){
10 printf("hhhh\n");
11 }
12 };
13 //程序一旦启动,就创建了全局唯一的实例对象
14 singleton* singleton::st = new singleton();
15 singleton* singleton::GetInstance(){
16 return st;
17 }
18
19 int main(){
20 singleton* st1 = singleton::GetInstance();
21 singleton* st2 = singleton::GetInstance();
22 st1->print();
23 if(st1 == st2){
24 printf("st1 == st2\n");
25 }
26 return 0;
27 }
~
4、懒汉模式
当第一次使用时才创建一个唯一的实例对象,从而实现延迟加载效果。
懒汉模式在第一次使用单例对象时才完成初始化工作,因此可能存在多线程竞态环境,如果不加锁会导致重复构造和构造不完全问题。
//懒汉模式
24 class sigleton{
25 private:
26 sigleton(){}
27 static sigleton* st;
28 static pthread_mutex_t _lock;
29 public:
30 static sigleton* GetInstance();
31 void print(){
32 printf("hhhhh\n");
33 }
34 };
35 sigleton* sigleton::st = NULL;
36 pthread_mutex_t sigleton::_lock = PTHREAD_MUTEX_INITIALIZER;
37
38 sigleton* sigleton::GetInstance(){
39 if(st == NULL){
40 pthread_mutex_lock(&_lock); //当第二个线程紧跟第一个时,加锁等待,降低锁冲突概率,提高性能
41 if(st == NULL){
42 st = new sigleton;
43 }
44 pthread_mutex_unlock(&_lock);
45 }
46 return st;
47 }
乐观锁和悲观锁
概念:
悲观锁:针对某个线程访问理解资源区修改数据的时候,都会认为可能有其他线程并行修改的情况发生,所以在线程修改数据之前就进行加锁,让多个线程互斥访问。悲观锁有:互斥锁、读写锁、自旋锁等
乐观锁:针对某个线程访问临界区修改数据的时候,乐观的认为只有该线程在修改,大概率不会存在并行的情况。所以修改数据不加锁,但是在修改完毕进行更新的时候,进行判断。例如:版本号控制,CAS无锁编程
自旋锁和互斥锁:
- 自旋锁加锁时,加不到锁,线程不会切换(时间没有到的时候,时间片到了,也会切换),会持续的尝试拿锁,直到拿到自旋锁。
- 互斥锁加锁时,加不到锁,线程会切换,进入睡眠状态,当其他线程释放互斥锁之后,被唤醒。再切换回来,进行抢锁。
- 自旋锁优点:因为自旋锁不会引起调用者睡眠,所以自旋锁的效率远高于互斥锁。
- 自旋锁的缺点:自旋锁一直占用CPU,他在未获得锁的情况下,一直运行(自旋),所以占用着CPU,如果不能在很短时间内获得锁,这无疑会使CPU效率降低。
- 适用于临界区代码较短时(直白的说,临界区代码执行时间短)的情况,使用自旋锁效率比较高。因为线程不用来回切换。
- 当临界区当中执行时间较长,自旋锁就不适用了,因为拿不到锁会占用CPU一直抢锁。
无锁编程
版本号控制:修改之前的版本号,和修改之后的版本号一致,允许修改,修改完毕,产生新的版本号。
CAS机制中的三个操作数:
v:内存地址
A:旧的预期值
B:要将内存地址值修改成新值
在修改V对应的内存内容的时候,先进行比较v的值和a的值是否相等。如果相等:说明没有人修改过,则将B的值赋值给V
如果不相等:说明有人修改过,则重新获取v的值,重新进行判断,直到v的值和A的值相等。