生产者消费者模型
我们先来举一个简单的例子去理解一下我们的生产者与消费者模型,在大草原上,草在生长,这就是生产者在生产,羊在吃草,这就是消费者在消费,当我们草被吃得差不多了,牧羊人就阻止羊再继续吃草了,当草长得差不多了,牧羊人就带这羊来继续吃草了,这样循环往复,周而复始,其实就是我们的一个生产者与消费者模型,在我们内存块中,生产者为消费者提供任务,消费者拿到任务开始执行,这一过程,当内存块中的数据达到一个高水位线,操作系统就会通知生产者进行等待,通知消费者开始执行任务,当内存块的数据达到低水位线,操作系统就会通知消费者等待,通知生产者进行生产
我们的生产者,消费者,之间有着三种关系,生产者与生产者之间是互斥的,消费者与消费者之间是互斥的,生产者与消费者之间则是同步的
生产者消费者模型的优点
我们先来看这一段代码
我们再正常执行这个代码时,会进行串行执行,main函数将拿到的数据传递给Add来进行运算,按部就班,这样的效率其实是有些低的,那么我们使用了生产者消费者模型呢?
我们在执行程序时使用两个线程,一个线程在main中不断地生产数据,一个线程在Add中不断对数据进行运算,这样两个线程进行并行,效率会高很多,所以我们的生产者与消费者模型就是为了提高效率的
优点:实现线程的解耦,支持线程之间并行,提高效率,增加代码的可维护性
基于阻塞队列实现生产者消费者模型
在我们多线程编程中,我们常使用阻塞队列来实现生产者消费者模型,阻塞队列的特点是,当队列为空时,从队列获取元素的操作符将被阻塞,直到队列中被放入元素,而当队列满的时候,从队列中存放元素的操作符将被阻塞,直到有元素从队列中取出
我们来实现多生产者多消费者模型
1 #pragma once //防止头文件重复包含
2
3 #include<iostream>
4
5 #include<pthread.h>
6 #include<unistd.h>
7 #include<queue>
8 //using namespace std;
9
10 class Task
11 {
12 public:
13 int _x;
14 int _y;
15 public:
16 Task()
17 {}
18 Task(int x,int y)
19 :_x(x)
20 ,_y(y)
21 {}
22 int run()
23 {
24 return _x+_y;
25 }
26 ~Task()
27 {}
28 };
29 class BlockQueue
30 {
31 private:
32 // std::queue<int> q; //设置一个队列
33 std::queue<Task> q; //设置一个队列
34 int _cap; //容量
35 pthread_mutex_t lock; //设置一把互斥锁
36
37 pthread_cond_t c_cond; //满了的话通知消费者
38 pthread_cond_t p_cond; //空的话通知生产者
39
40 private: //封装起来
41 void LockQueue() //加锁
42 {
43 pthread_mutex_lock(&lock);
44 }
45 void UnLockQueue() //解锁
46 {
47 pthread_mutex_unlock(&lock);
48 }
49
50
51 bool IsEmpty() //判断队列是否为空
52 {
53 return q.size()==0;
54 }
55 bool IsFull() //判断队列是否满了
56 {
57 return q.size()==_cap;
58 }
59
60 void ProductWait() //生产者等待
61 {
62 pthread_cond_wait(&p_cond,&lock);
63 }
64 void ConsumerWait() //消费者等待
65 {
66 pthread_cond_wait(&c_cond,&lock);
67 }
68
69 void WakeUpProduct() //唤醒生产者
70 {
71 std::cout<<"wake up Product..."<<std::endl;
72 pthread_cond_signal(&p_cond);
73 }
74 void WakeUpConsumer() //唤醒消费者
75 {
76 std::cout<<"wake up Consumer..."<<std::endl;
77 pthread_cond_signal(&c_cond);
78 }
79
80 public:
81 BlockQueue(int cap) //构造函数初始化
82 :_cap(cap)
83 {
84 pthread_mutex_init(&lock,NULL);
85 pthread_cond_init(&c_cond,NULL);
86 pthread_cond_init(&p_cond,NULL);
87 }
88 ~BlockQueue() //析构函数销毁
89 {
90 pthread_mutex_destroy(&lock);
91 pthread_cond_destroy(&c_cond);
92 pthread_cond_destroy(&p_cond);
93 }
94
95 void put(Task in)
96 {
97 //Queue是临界资源,就要加锁,而且判断是否为满,把接口封装起来
98 LockQueue();
99 while(IsFull())
100 {
101 WakeUpConsumer();
102 std::cout<<"queue full,notify consume data,product stop!"<<std::endl;
103 ProductWait(); //生产者线程等待
104 }
105 q.push(in);
106
107 UnLockQueue();
108 }
109 void Get(Task& out)
110 {
111 LockQueue();
112 while(IsEmpty())
113 {
114 WakeUpProduct();
115 std::cout<<"queue empty,notify product data,consumer stop"<<std::endl;
116 ConsumerWait();
117 }
118 out=q.front();
119 q.pop();
120
121 UnLockQueue();
122 }
123
124 //线程接口函数
125 /*void* Product(void* arg)
126 {
127
128 }
129 void* Consumer(void* arg)
130 {
131
132 }*/
133
134 };
1 #include"BlockQueue.cpp"
2 using namespace std;
3 #include<stdlib.h>
4
5 pthread_mutex_t p_lock;
6 pthread_mutex_t c_lock;
7 void* Product_Run(void* arg)
8 {
9 BlockQueue* bq=(BlockQueue*)arg;
10
11 srand((unsigned int)time(NULL));
12 while(true)
13 {
14 pthread_mutex_lock(&p_lock);
15 // int data=rand()%10+1;
16 int x=rand()%10+1;
17 int y=rand()%100+1;
18 Task t(x,y);
19 bq->put(t);
20 pthread_mutex_unlock(&p_lock);
21 cout<<"product data is:"<<t.run()<<endl;
22 }
23 }
24 void* Consumer_Run(void* arg)
25 {
26 BlockQueue* bq=(BlockQueue*)arg;
27 while(true)
28 {
29 pthread_mutex_lock(&c_lock);
30 // int n=0;
31 Task t;
32 bq->Get(t);
33 pthread_mutex_unlock(&c_lock);
34 cout<<"consumer is:"<<t._x<<"+"<<t._y<<"="<<t.run()<<endl;
35 sleep(1);
36 }
37 }
38 int main()
39 {
40 BlockQueue* bq=new BlockQueue(10);
41 pthread_t c,p;
42
43 pthread_create(&c,NULL,Product_Run,(void*)bq);
44
45 pthread_create(&p,NULL,Consumer_Run,(void*)bq);
46
47 pthread_join(c,NULL);
48 pthread_join(p,NULL);
49
50 delete bq;
51 return 0;
52 }
我们可以看到,生产者在生产数据,当生产满了之后,唤醒消费者消费,消费完了之后再次唤醒生产者生产,再次循环,完成生产者消费者模型
posox信号量
其实我们的POSIX信号量本质上是一个计数器,用以描述临界资源有效个数的计数器
P操作和V操作
我们假设我们的临界资源可以分为5份,记为count=5,count就被称为信号量
count--,一个执行流占有一个部分的操作叫做P操作
count++,一个执行流结束,使用临界资源的一部分叫做V操作。
当信号量为0时,进行P操作,因为无信号量可以分配,此时便会进行阻塞等待
由于每一个线程看到的信号量都是同一份临界资源,所以我们需要保证PV都为原子的
二元信号量相当于互斥锁,二元信号量只有一个信号量,只要有一个线程占有,信号量的值就等于0,其他新城需要等待
POSIX信号量初始化
#include <semaphore.h>int sem_init(sem_t *sem, int pshared, unsigned int value);参数:pshared:0 表示线程间共享,非零表示进程间共享value :信号量初始值
销毁信号量
int sem_destroy(sem_t *sem);
等待信号量P操作
功能:等待信号量,会将信号量的值减 1int sem_wait(sem_t *sem);
发布信号量V操作
功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加 1 。int sem_post(sem_t *sem);
基于环形队列实现生产者消费者模型
环形队列采用数组模拟,用模运算来模拟环状特性环形结构起始状态和结束状态都是一样的,不好判断为空或者为满,所以可以通过加计数器或者标记位来判断满或者空。另外也可以预留一个空的位置,作为满的状态但是我们现在有信号量这个计数器,就很简单的进行多线程间的同步过程
1 #pragma once
2
3 #include<iostream>
4 #include<unistd.h>
5 #include<vector>
6 #include<semaphore.h>
7
8 #include<stdlib.h>
9 #define NUM 10
10
11 class RingQueue
12 {
13 private:
14 std::vector<int> v;
15 int _cap; //容量
16 sem_t sem_blank; //生产者
17 sem_t sem_data; //消费者
18
19 int c_index; //消费者索引
20 int p_index; //生产者索引
21
22 public:
23 RingQueue(int cap=NUM)
24 :_cap(cap)
25 ,v(cap)
26 {
27 sem_init(&sem_blank,0,cap);
28 sem_init(&sem_data,0,0);
29 c_index=0;
30 p_index=0;
31 }
32 ~RingQueue()
33 {
34 sem_destroy(&sem_blank);
35 sem_destroy(&sem_data);
36 }
37
38 void Get(int& out)
39 {
40 sem_wait(&sem_data);
41 //消费
42 out=v[c_index];
43 c_index++;
44 c_index=c_index%NUM; //防止越界,构成环形队列
45 sem_post(&sem_blank);
46 }
47 void Put(const int& in)
48 {
49 sem_wait(&sem_blank);
50 //生产
51 v[p_index]=in;
52 p_index++;
53 p_index=p_index%NUM;
54 sem_post(&sem_data);
55 }
56 };
1 #include"RingQueue.h"
2 using namespace std;
3
4
5 void* Consumer(void* arg)
6 {
7 RingQueue *bq=(RingQueue*)arg;
8 int data;
9 while(1)
10 {
11 bq->Get(data);
12 cout<<"i am:"<<pthread_self()<<" i consumer:"<<data<<endl;
13 }
14 }
15 void* Product(void* arg)
16 {
17 RingQueue* bq=(RingQueue*)arg;
18 srand((unsigned int)time(NULL));
19 while(1)
20 {
21 int data=rand()%100;
22 bq->Put(data);
23 cout<<"i am:"<<pthread_self()<<" i product:"<<data<<endl;
24 sleep(1);
25 }
26 }
27 int main()
28 {
29 RingQueue* pq=new RingQueue();
30 pthread_t c;
31 pthread_t p;
32 pthread_create(&c,NULL,Consumer,(void*)pq);
33 pthread_create(&p,NULL,Product,(void*)pq);
34
35 pthread_join(c,NULL);
36 pthread_join(p,NULL);
37 return 0;
38 }
1 main:main.cpp
2 g++ $^ -o $@ -lpthread
3 .PHONY:clean
4 clean:
5 rm -f main
此时我们可以看到,这个基于循环对列的生产者消费者模型是生产一个消费一个的
线程池
线程池 :* 一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。* 线程池的应用场景:* 1. 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB 服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet 连接请求,线程池的优点就不明显了。因为 Telnet 会话时间比线程的创建时间大多了。* 2. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。* 3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误.* 线程池的种类:* 线程池示例:* 1. 创建固定数量线程池,循环从任务队列中获取任务对象,* 2. 获取到任务对象后,执行任务对象中的任务接口
线程池实现
其实我们的线程池本质上也是一个生产者消费者模型,接受任务,执行任务
1 #include<iostream>
2 #include<math.h>
3 #include<unistd.h>
4 #include<stdlib.h>
5 #include<pthread.h>
6 #include<queue>
7
8 #define NUM 5
9 class Task
10 {
11 private:
12 int _b;
13 public:
14 Task()
15 {
16
17 }
18 Task(int b)
19 :_b(b)
20 {
21
22 }
23 ~Task()
24 {
25
26 }
27 void Run()
28 {
29 std::cout<<"i am:"<<pthread_self()<<" Task run.... :base# "<<_b<<" pow is "<<pow(_b,2)<<std::endl;
30 }
31 };
32 class ThreadPool
33 {
34 private:
35 std::queue<Task*> q;
36 int _max_num; //线程总数
37
38 pthread_mutex_t lock;
39 pthread_cond_t cond; //只能让消费者操作
40
41 private:
42 void LockQueue()
43 {
44 pthread_mutex_lock(&lock);
45 }
46 void UnLockQueue()
47 {
48 pthread_mutex_unlock(&lock);
49 }
50
51 bool IsEmpty()
52 {
53 return q.size()==0;
54 }
55 bool IsFull()
56 {
57 return q.size()==_max_num;
58 }
59
60 void ThreadWait()
61 {
62 pthread_cond_wait(&cond,&lock); //等待条件变量满足
63 }
64
65 void ThreadWakeUp()
66 {
67 pthread_cond_signal(&cond);
68 }
69 public:
70 ThreadPool(int max_num=NUM )
71 :_max_num(max_num)
72 {
73
74 }
75
76 static void* Routine(void* arg)
77 {
78 while(1)
79 {
80 ThreadPool *tp=(ThreadPool*)arg;
81 while(tp->IsEmpty())
82 {
83 tp->LockQueue(); //静态成员方法不能访问非静态成员方法,所以传(void*)this传过去
84 tp->ThreadWait(); //为空挂起等待
85 }
86
87 Task t;
88 tp->Get(t); //获取这个任务
89 tp->UnLockQueue();
90 t.Run(); //拿到这个任务运行
91 }
92 }
93
94 void ThreadPoolInit()
95 {
96 pthread_mutex_init(&lock,NULL);
97 pthread_cond_init(&cond,NULL);
98
99 int i=0;
100 pthread_t t;
101 for(i=0;i<_max_num;i++)
102 {
103 pthread_create(&t,NULL,Routine,(void*)this);
104 }
105 }
106 ~ThreadPool()
107 {
108 pthread_mutex_destroy(&lock);
109 pthread_cond_destroy(&cond);
110 }
111
112 //server 放数据
113 void Put(Task& in)
114 {
115 LockQueue();
116
117 q.push(&in);
118
119 UnLockQueue();
120
121 ThreadWakeUp();
122 }
123 //ThreadPool 取数据
124 void Get(Task& out)
125 {
126 //线程池里面直接拿不用加锁
127 Task* t=q.front();
128 q.pop();
129 out=*t;
130 }
131 };
132
1 #include"Thread_Pool.h"
2 using namespace std;
3
4
5 int main()
6 {
7 ThreadPool *tp=new ThreadPool();
8
9 tp->ThreadPoolInit();
10
11 while(true)
12 {
13 int x=rand()%10+1;
14 Task t(x);
15 tp->Put(t);
16 sleep(1);
17 }
18 return 0;
19 }
1 main:main.cpp
2 g++ $^ -o $@ -lpthread
3 .PHONY:clean
4 clean:
5 rm -f main
读者写者问题
读写锁
在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。[长时间等人和短时间等人的例子]
写独占,读共享,写锁优先级高
读写锁接口
int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);/*pref 共有 3 种选择PTHREAD_RWLOCK_PREFER_READER_NP ( 默认设置 ) 读者优先,可能会导致写者饥饿情况PTHREAD_RWLOCK_PREFER_WRITER_NP 写者优先,目前有 BUG ,导致表现行为和PTHREAD_RWLOCK_PREFER_READER_NP 一致PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 写者优先,但写者不能递归加锁*/
初始化
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t*restrict attr);
销毁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
加锁和解锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
线程安全的单例模式
饿汉实现方式和懒汉实现方式
吃完饭 , 立刻洗碗 , 这种就是饿汉方式 . 因为下一顿吃的时候可以立刻拿着碗就能吃饭 .吃完饭 , 先把碗放下 , 然后下一顿饭用到这个碗了再洗碗 , 就是懒汉方式 .
饿汉方式实现单例模式
template <typename T>
class Singleton
{
private:
static T data; //定义静态的类对象,程序加载类就加载对象
public:
static T* GetInstance()
{
return &data;
}
};
懒汉方式实现单例模式
class Singleton
{
static T* inst; //定义静态的类对象指针,程序运行时才加载对象
public:
static T* GetInstance()
{
if (inst == NULL)
{
inst = new T();
} r
eturn inst;
}
};
懒汉方式实现单例模式(线程安全版本)
template <typename T>
// 懒汉模式, 线程安全
template <typename T>
class Singleton {
volatile static T* inst; // 需要设置 volatile 关键字, 否则可能被编译器优化.
static std::mutex lock;
public:
static T* GetInstance()
{
if (inst == NULL) // 双重判定空指针, 降低锁冲突的概率, 提高性能. //判断两个线程不同时进去直接return
{
lock.lock(); // 使用互斥锁, 保证多线程情况下也只调用一次 new. //两个线程同时进去加锁
if (inst == NULL)
{
inst = new T();
}
lock.unlock();
}
return inst;
}
};
注意事项 :1. 加锁解锁的位置2. 双重 if 判定 , 避免不必要的锁竞争3. volatile 关键字防止过度优化