Linux--10.多线程(下)

生产者消费者模型

我们先来举一个简单的例子去理解一下我们的生产者与消费者模型,在大草原上,草在生长,这就是生产者在生产,羊在吃草,这就是消费者在消费,当我们草被吃得差不多了,牧羊人就阻止羊再继续吃草了,当草长得差不多了,牧羊人就带这羊来继续吃草了,这样循环往复,周而复始,其实就是我们的一个生产者与消费者模型,在我们内存块中,生产者为消费者提供任务,消费者拿到任务开始执行,这一过程,当内存块中的数据达到一个高水位线,操作系统就会通知生产者进行等待,通知消费者开始执行任务,当内存块的数据达到低水位线,操作系统就会通知消费者等待,通知生产者进行生产

我们的生产者,消费者,之间有着三种关系,生产者与生产者之间是互斥的,消费者与消费者之间是互斥的,生产者与消费者之间则是同步的

生产者消费者模型的优点

我们先来看这一段代码

我们再正常执行这个代码时,会进行串行执行,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操作

功能:等待信号量,会将信号量的值减 1
int 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;
	}
};

只要通过 Singleton 这个包装类来使用 T 对象 , 则一个进程中只有一个 T 对象的实例

懒汉方式实现单例模式

class Singleton 
{
	static T* inst;  //定义静态的类对象指针,程序运行时才加载对象
public:
	static T* GetInstance() 
	{
		if (inst == NULL) 
		{
			inst = new T();
		} r
			eturn inst;
	}
};
存在一个严重的问题 , 线程不安全 .
第一次调用 GetInstance 的时候 , 如果两个线程同时调用 , 可能会创建出两份 T 对象的实例 .
但是后续再次调用 , 就没有问题了 .

懒汉方式实现单例模式(线程安全版本)

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 关键字防止过度优化
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值