1. 线程池
2. 可处理任务的线程池模型
这个模型类似于基于阻塞队列的线程池模型:点击查看博客详情
为什么说他们相似呢,因为它们都是通过队列来作缓冲区,并且都有生产者和消费者。
不同的是,这个模型的线程不是由自己一个个创建,而是由线程池创建,在线程池初始化的时候,就生产出一批线程。
当你开始生产数据的时候,就会有一部分线程被调用过去生产数据,一部分线程会被调去领取任务。
构建一个线程池,线程池里有一个队列,该队列用来接收任务。池内的线程不断生产任务,池外的线程不断领取任务。
也就是外部的请求来了,线程池内早早就准备好了线程,用来随时处理任务。
- 初始化线程池
生产出一批线程,没有任务的时候就在挂起等待
void pthreadPoolInit()
{
//每次开始前,让一堆线程去执行rountine
//或者在条件变量cond_下等待(没任务)
pthread_t tid;
for (int i = 0; i < num_; i++)
{
pthread_create(&tid, nullptr, rountine, (void *)this);
}
}
等待的原理和阻塞队列的模型一样,通过条件变量:
void wait()
{
pthread_cond_wait(&cond_,&mutex_);
}
唤醒:
void wakeUp()
{
pthread_cond_signal(&cond_);
}
因为其他都类似,这里就不一一列举了。
有个值得注意的问题是,创建线程时需要执行的方法rountine是一个静态成员函数。因为让线程在类成员函数内直接调用成员方法,是无法实现的。 所以要将rountine设置为static方法。
还有,线程领取完任务以后,应该先释放锁再执行自己的任务。因为此时的任务已经不属于队列,而属于线程自己,所以应该先释放锁然后能让别的线程早点拿到锁。
头文件:
#pragma once
#include <queue>
#include <iostream>
#include <unistd.h>
using std::cout;
using std::endl;
namespace zcb
{
const int g_num = 10;
template <class T>
class pthreadPool
{
private:
int num_;
std::queue<T> task_queue_;//临界资源
pthread_mutex_t mutex_;
pthread_cond_t cond_;
public:
pthreadPool() : num_(g_num)
{
pthread_mutex_init(&mutex_,nullptr);
pthread_cond_init(&cond_,nullptr);
}
~pthreadPool()
{
pthread_mutex_destroy(&mutex_);
pthread_cond_destroy(&cond_);
}
public:
static void *rountine(void *args)
{
// 每个线程分离,不用等待
pthread_detach(pthread_self());
pthreadPool<T> *tmp = (pthreadPool<T> *)args;
//每个线程不停地竞争锁,竞争到了以后如果没有任务就挂起
//挂起过程不带有锁
while (1)
{
//访问临界资源,加锁
tmp->lock();
while (tmp->isEmpty())
{
//如果任务队列为空,线程进行挂起
//只有当任务队列有任务时,才能继续
//如果不等待,而是直接break退出的话
//会导致线程一直重复访问队列是否为空,虽然没错但是效率低
tmp->wait();
}
//此时一定拿到了任务
T t;
cout<< "我是线程:" << pthread_self() << "我拿到了任务:"<< tmp->task_queue_.front()<<endl;
tmp->popTask(&t);
tmp->unlock();
//释放完锁再去执行任务
//因为此时的任务已经属于该进程,而不是线程池内的了
//早点释放锁让别的进程能够及时抢锁
sleep(1);
cout<<"线程:"<< pthread_self()<<"任务已被执行完毕"<<endl;
}
}
void pthreadPoolInit()
{
//每次开始前,让一堆线程去执行rountine
//或者在条件变量cond_下等待(没任务)
pthread_t tid;
for (int i = 0; i < num_; i++)
{
pthread_create(&tid, nullptr, rountine, (void *)this);
}
}
void pushTask(const T& in)
{
cout << "我是线程:" <<pthread_self() << "我生产任务:" << in << endl;
lock();
task_queue_.push(in);
unlock();
wakeUp();
}
void popTask(T* out)
{
//lock();
*out = task_queue_.front();
task_queue_.pop();
//unlock();
}
void wakeUp()
{
pthread_cond_signal(&cond_);
}
void lock()
{
pthread_mutex_lock(&mutex_);
}
void unlock()
{
pthread_mutex_unlock(&mutex_);
}
bool isEmpty()
{
return task_queue_.size() == 0;
}
void wait()
{
pthread_cond_wait(&cond_,&mutex_);
}
};
}
源文件:
#include <pthread.h>
#include "pool.hpp"
using namespace zcb;
int main()
{
pthreadPool<int>* tmp = new pthreadPool<int>();
//初始化线程池
tmp->pthreadPoolInit();
srand((long long)time(nullptr));
while(1)
{
sleep(1);
int data = rand()%20 + 1;
tmp->pushTask(data);
}
return 0;
}
运行结果就是在上一个线程执行完任务之前,下一个线程就已经领取到任务了:
2. 单例模式
某些类, 只应该具有一个对象(实例), 就称之为单例。
既然是只有一个对象,那么主要就是将类内的构造函数给设置为私有,让该类无法直接定义对象即可。
有什么作用呢?通俗的理解就是当某个类对象需要的存储空间很多,而又需要经常定义对象,就会导致每次开辟空间的时间太多。那么就可以让它只开辟一次,也即是说每次申请,用的对象都是同一个。
这个实现方式是通过一个静态成员指针变量ins和**一个静态成员方法GetInstance()**来完成的,如果该类从未创建过对象,那么该指针是nullptr,直到其第一次创建对象,就对该指针初始化,而后由于该指针不为nullptr了,每次调用GetInstance返回的都是同一个对象,这也就大大降低了每次创建对象所需要的开辟空间的时间,提高性能。
3. 饿汉方式和懒汉方式
单例模式包括饿汉模式和懒汉模式。
单例模式,也就是只定义一个对象。
那么定义一个对象,有一种情况是在你需要该对象的时候才定义,另一种是在你启动程序就直接定义好。分别是懒汉模式和饿汉模式。
饿汉模式就是在运行一段程序的时候,上来就直接创建对象。比如为设置该对象为static,在加载程序的时候直接就定义好了。如果该对象内部存在大量的空间,保存了大量的数据,或者允许发生各种拷贝,内存中就会存在冗余数据。
懒汉模式就是在运行一段程序的时候,需要了某个对象,才给程序创建出来。
比如定义一个指针,这个指针是某个对象的地址,如果该指针一直为空,说明该对象一直没被创建,只有当主动调用该封装函数去将对象new出来时,才会起到真正的定义对象的作用。这种方式的主要思想是延迟加载,能够提高程序启动的速度。
所以,要实现单例模式,首先要将构造函数私有化,然后定义一个对象指针。然后提供一个接口,用来获取对象。
单例模式实现:
头文件:
//部分代码,其他和上述模型一样
namespace zcb
{
const int g_num = 10;
template <class T>
class pthreadPool
{
private:
int num_;
std::queue<T> task_queue_;//临界资源
pthread_mutex_t mutex_;
pthread_cond_t cond_;
static pthreadPool<T> *ins;//新增指针,类外初始化
private:
//禁用拷贝构造和构造
pthreadPool(const pthreadPool<T> &tp) = delete;
//赋值语句
pthreadPool<T> &operator=(pthreadPool<T> &tp) = delete;
//构造函数必须实现并且私有化
pthreadPool() : num_(g_num)
{
pthread_mutex_init(&mutex_,nullptr);
pthread_cond_init(&cond_,nullptr);
}
public:
//新增接口获得对象指针
static ThreadPool<T> *GetInstance()
{
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
//设置锁,防止这个函数被多线程重入
// 当前单例对象还没有被创建
if (ins == nullptr) //双判定,减少锁的争用,提高获取单例的效率!
{
pthread_mutex_lock(&lock);
if (ins == nullptr)
{
ins = new ThreadPool<T>();
ins->InitThreadPool();//创建对象的同时初始化线程池列表
std::cout << "首次加载对象" << std::endl;
}
pthread_mutex_unlock(&lock);
}
return ins;
}
};
}
源文件:
#include "pool_single.hpp"
#include <ctime>
#include <cstdlib>
using namespace zcb;
int main()
{
std::cout << "当前正在运行我的进程其他代码..." << std::endl;
sleep(5);
srand((long long)time(nullptr));
while(true)
{
sleep(1);
int data = rand()%20+1;
threadPool<int>::GetInstance()->pushTask(data);
//单例本身会在任何场景,任何环境下被调用
//GetInstance():被多线程重入,进而导致线程安全的问题,所以要设置锁
std::cout << threadPool<Task>::GetInstance() << std::endl;
}
return 0;
}
4. 读者写者模型
在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。 通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极
大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。
- 条件
- 对数据,大部分的操作是读取,少量的操作是写入;
- 判断依据是:进行数据读取(消费)的一端是否会将数据取走,如果不取走,就可以考虑读写者模型。
- 模型
这个模型有
三种关系:读者与读者、读者与写者、写者与写者
两种角色:读者与写者
一个交易场所:一段缓冲区
- 读写者关系
-
读者VS写者:互斥、同步(写的时候无法读,读的时候无法写,但是写完可以马上读,读完可以马上写)
-
写者VS写者:互斥(写的时候其他线程无法写)
-
读者VS读者:无关系(只读不拿数据,所以可以同时进行)
这三种关系本质是使用锁来维护。
4.1 读写锁相关接口
创建一个读写锁:
释放一个读写锁:
以读者身份加锁:
以写者身份加锁:
- 优先级
和消费生产模型一样,读写者模型也有优先级。但是不同点是:消费生产模型的优先级并不是固定的,而是通过队列的空与满来决定谁先谁后;而对于读写者模型,指定了谁先,那就是谁先,无论有没有读/写。
-
读者优先:读者写者同时来的时候,优先让读者进入访问。
-
写者优先:读者写者同时来的时候,比当前写者晚来的所有读者,都不要进入临界区访问了,等临界区中没有读者的时候,让写者先写入。理论上这样能避免写饥饿问题。但是目前有 BUG,导致表现行为和读者优先一致。
默认设置是读者优先,会导致写者饥饿情况。
但是读写者模型这样的处理会带来一个情况:读者多,写者少的问题。
5. 悲观锁与自旋锁
悲观锁也叫挂起等待特性锁,当线程访问临界资源需要的时间比较长时,其他线程会挂起等待。悲观锁是日常使用比较多的锁。
反之,自旋锁不会挂起等待,而是不断地循环,检测锁的状态,第一时间拿到锁资源。
选择悲观锁和自旋锁取决于线程访问临界资源的时间。
如果一个线程访问临界资源的时间很快,那么显然其他线程也能很快的得到锁并且访问临界资源,那其他线程就没必要挂起等待了,那么这个锁就适合于自旋锁。反之,如果一个线程访问临界资源的时间很长,采用循环检测的方法显然不合适,所以直接挂起等待是比较适合的。
那么线程如何得知自己会在临界区待多长时间呢?
只有程序员知道,因为线程进入临界区之后需要做什么,是由程序员实现的。
- 自旋锁接口
申请和销毁:
加锁和解锁: