💖作者:小树苗渴望变成参天大树🎈
🎉作者宣言:认真写好每一篇博客💤
🎊作者gitee:gitee✨
💞作者专栏:C语言,数据结构初阶,Linux,C++ 动态规划算法🎄
如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点 点 关 注 吧!
文章目录
- 前言
- 一、线程池
- 二、单例模式
- 三、STL,智能指针和线程安全
- 四、其他常见的各种锁
- 五、总结
前言
今天我们讲解线程的收尾工作,前面博主花了很长时间给大家讲解线程,确实线程这部分要将的东西太多了,大家把前面的掌握好了就不容易了,这篇博主要讲解的是带大家写一个线程池,还有一些c++中带线程,话不多说,我们开始进入正文的讲解。
提示:以下是本篇文章正文内容,下面案例可供参考
一、线程池
大家还记得我写的进程池代码吗??我们当初实现的进程池,是通过父进程创建多个子进程,父进程给子进程派发任务,子进程处理数据,那我们的线程池也是这样去做的,当时线程池比进程池要麻烦一点,他要多数据进行保护,接下来直接看代码,里面有注释:任务还是之前写的计算器任务。
ThreadPool.hpp:
#include<iostream>
#include<pthread.h>
#include<vector>
#include<queue>
using namespace std;
struct ThreadInfo
{
string threadname;
pthread_t threadid;
};
template<class T>
class ThreadPool
{
const static int num=3;//线程信息数组的默认容量,也就是线程数亩
public:
void lock(pthread_mutex_t* lock)
{
pthread_mutex_lock(lock);
}
void unlock(pthread_mutex_t* lock)
{
pthread_mutex_unlock(lock);
}
void wait(pthread_cond_t* cond,pthread_mutex_t*lock)
{
pthread_cond_wait(cond,lock);
}
void signal(pthread_cond_t* cond)
{
pthread_cond_signal(cond);
}
string getThreadName(pthread_t tid1)//通过获取线程自己的tid,在线程信息数组找匹配的线程名
{
string name;
for(const auto & tid:threadinfo_)
{
if(tid.threadid==tid1)
{
return tid.threadname;
}
}
return "None";
}
bool IsEmpty()
{
return tasks_.empty();
}
public:
ThreadPool(int threadcap=num):threadinfo_(num)
{
//给互斥锁和条件变量进行初始化
pthread_mutex_init(&lock_,NULL);
pthread_cond_init(&cond_,NULL);
}
static void* threadfunc(void* arg)//因为线程执行的函数必须是一个参数,不用静态的,就会有一个隐藏的this,但有需要this来调用,所以创建线程的时候就直接将this传进来。
{
ThreadPool<T>* tp=static_cast<ThreadPool<T>*>(arg);
string name=tp->getThreadName(pthread_self());
while(true)
{
tp->lock(&tp->lock_);
while(tp->IsEmpty())
{
tp->wait(&tp->cond_,&tp->lock_);
}
T task=tp->pop();
tp->unlock(&tp->lock_);
task();//处理任务,线程拿到任务就是自己的,所以不需要在加锁里面。
cout<<name<<" is working on task: "<<task.GetTask()<<endl;
}
}
void start()//启动线程(创建线程)
{
for(int i=0;i<threadinfo_.size();i++)//这个线程数初始化的时候就定好了,所以存放线程信息的数组大小也提前开辟好
{
threadinfo_[i].threadname="thread"+to_string(i);
pthread_create(&(threadinfo_[i].threadid),NULL,threadfunc,this);
}
}
T pop()//获取队列中的任务
{
T t=tasks_.front();
tasks_.pop();
return t;
}
void push(const T& task)
{
//加锁的目的让队列只能有一个线程访问,我主线程发任务你其他线程先等着,不然我放一办你就读取,读到的数据不完整
lock(&lock_);
tasks_.push(task);
signal(&cond_);//当主线程发布一个任务后,任务队列肯定不为空,就可以唤醒线程来处理了
unlock(&lock_);
}
~ThreadPool()
{
pthread_mutex_destroy(&lock_);
pthread_cond_destroy(&cond_);
}
private:
vector<ThreadInfo> threadinfo_;//存储创建的每个线程(处理主线程发来任务的线程)信息的数组
queue<T> tasks_;//存放主线程发来任务的队列
pthread_mutex_t lock_;//用于处理任务线程直接的互斥和同步
pthread_cond_t cond_;//条件变量
};
main.cc:
#include"ThreadPool.hpp"
#include<ctime>
#include<unistd.h>
#include"task.hpp"
int main()
{
cout<<"main start"<<endl;
ThreadPool<Task>* tp=new ThreadPool<Task>(3);
srand(time(nullptr)^getpid());
tp->start();//启动线程池
while(true)
{
int x=rand()%10+1;
int y=rand()%10;
char op=opers[rand()%opers.size()];
Task t(x,y,op);//创建任务
tp->push(t);//提交任务,其余的处理就让线程池去做吧
sleep(1);
cout<<"result:"<<t.GetResult()<<endl;
}
return 0;
}
此线程池一秒往线程里面发布一个任务,而线程需要实现同步互斥,所以结果会看到他们按照顺序来执行主线程发布的任务。
二、单例模式
之前在C++的博客中写过单例模式
单例模式是一种 "经典的, 常用的, 常考的设计模式.,IT行业这么火, 涌入的人很多. 俗话说林子大了啥鸟都有. 大佬和菜鸡们两极分化的越来越严重. 为了让菜鸡们不太拖大佬的后腿, 于是大佬们针对一些经典的常见的场景, 给定了一些对应的解决方案, 这个就是 设计模式
单例模式的特点
某些类, 只应该具有一个对象(实例), 就称之为单例.
例如一个男人只能有一个媳妇.在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据.
单例通常有两种设计模式,一种是俄汉,一种是懒汉,今天以懒汉为例:
懒汉方式实现单例模式(线程安全版本)
我们将刚才的线程池改成懒汉模式:
ThreadPooldanli.hpp:
#include<iostream>
#include<pthread.h>
#include<vector>
#include<queue>
using namespace std;
struct ThreadInfo
{
string threadname;
pthread_t threadid;
};
template<class T>
class ThreadPool
{
const static int num=3;//线程的默认容量
public:
void lock(pthread_mutex_t* lock)
{
pthread_mutex_lock(lock);
}
void unlock(pthread_mutex_t* lock)
{
pthread_mutex_unlock(lock);
}
void wait(pthread_cond_t* cond,pthread_mutex_t*lock)
{
pthread_cond_wait(cond,lock);
}
void signal(pthread_cond_t* cond)
{
pthread_cond_signal(cond);
}
string getThreadName(pthread_t tid1)
{
string name;
for(const auto & tid:threadinfo_)
{
if(tid.threadid==tid1)
{
return tid.threadname;
}
}
return "None";
}
bool IsEmpty()
{
return tasks_.empty();
}
public:
static void* threadfunc(void* arg)
{
ThreadPool<T>* tp=static_cast<ThreadPool<T>*>(arg);
string name=tp->getThreadName(pthread_self());
while(true)
{
tp->lock(&tp->lock_);
while(tp->IsEmpty())
{
tp->wait(&tp->cond_,&tp->lock_);
}
T task=tp->pop();
tp->unlock(&tp->lock_);
task();
cout<<name<<" is working on task: "<<task.GetTask()<<endl;
}
}
void start()//启动线程(创建线程)
{
for(int i=0;i<threadinfo_.size();i++)
{
threadinfo_[i].threadname="thread"+to_string(i);
pthread_create(&(threadinfo_[i].threadid),NULL,threadfunc,this);
}
}
T pop()
{
T t=tasks_.front();
tasks_.pop();
return t;
}
void push(const T& task)
{
lock(&lock_);
tasks_.push(task);
signal(&cond_);
unlock(&lock_);
}
static ThreadPool<T>* getInstance()//对外提供单例对象接口,必须静态的,才能使用类名去调用
{
if(nullptr==instance_)//双重检查,第一次申请锁后,创建对象,后面的线程连第一个判断就进不去,就不会每个线程都会有申请锁释放锁的过程,增加效率。
{
pthread_mutex_lock(&mutex_);
if(nullptr==instance_)
{
instance_=new ThreadPool<T>();
}
pthread_mutex_unlock(&mutex_);
}
return instance_;
}
//防拷贝
ThreadPool(const ThreadPool&)=delete;
const ThreadPool& operator=(const ThreadPool&)=delete;
private://将构造函数私有化,就创建不了对象了。
ThreadPool(int threadcap=num):threadinfo_(num)
{
//给互斥锁和条件变量进行初始化
pthread_mutex_init(&lock_,NULL);
pthread_cond_init(&cond_,NULL);
}
~ThreadPool()
{
pthread_mutex_destroy(&lock_);
pthread_cond_destroy(&cond_);
}
private:
vector<ThreadInfo> threadinfo_;//存储每个线程信息的数组
queue<T> tasks_;//存放主线程发来任务的队列
pthread_mutex_t lock_;//互斥锁
pthread_cond_t cond_;//条件变量
static ThreadPool<T>* instance_;//定一个懒汉的单例对象,不能是栈区的变量,会套娃
static pthread_mutex_t mutex_;//防止多线程同时创建对象,在刚判断完为不为空的时候,被切走,下次进来直接创建对象,这样就不止一个对象了
};
template<class T>
ThreadPool<T>* ThreadPool<T>::instance_=nullptr;//给单利对象进行初始化
template<class T>
pthread_mutex_t ThreadPool<T>::mutex_=PTHREAD_MUTEX_INITIALIZER;//初始化互斥锁
maindanli.cc:
#include"ThreadPooldanli.hpp"
#include<ctime>
#include<unistd.h>
#include"task.hpp"
int main()
{
cout<<"main start"<<endl;
srand(time(nullptr)^getpid());
ThreadPool<Task>::getInstance()->start();//启动线程池
while(true)
{
int x=rand()%10+1;
int y=rand()%10;
char op=opers[rand()%opers.size()];
Task t(x,y,op);//创建任务
ThreadPool<Task>::getInstance()->push(t);//提交任务
sleep(1);
cout<<"result:"<<t.GetResult()<<endl;
}
return 0;
}
效果和刚才的一样的。
最重要的代码:
static ThreadPool<T>* getInstance()//对外提供单例对象接口,必须静态的,才能使用类名去调用
{
if(nullptr==instance_)//双重检查,第一次申请锁后,创建对象,后面的线程连第一个判断就进不去,就不会每个线程都会有申请锁释放锁的过程,增加效率。
{
pthread_mutex_lock(&mutex_);
if(nullptr==instance_)
{
instance_=new ThreadPool<T>();
}
pthread_mutex_unlock(&mutex_);
}
return instance_;
}
三、STL,智能指针和线程安全
STL中的容器是否是线程安全的?
不是.
原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响.而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶).因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全.
智能指针是否是线程安全的?
对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题.对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数/
四、其他常见的各种锁
悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
自旋锁: 自旋锁适用于访问临界区时间短的。我们之前使用的锁只要申请失败就被挂起了,等释放后,唤醒在去竞争锁,挂起于欧唤醒浪费时间,自旋锁是一只申请,申请失败还申请,直到申请成功,当一个线程拿到锁,访问临界资源的时间过长,还不如让他挂起,频繁去申请锁不好,所以自旋锁适用于访问临界资源时间少的场景。(trylock函数)
这两个就是自旋锁加锁的方式,第一个是阻塞,第二个是非阻塞,为什么还阻塞呢?原因是他申请不到锁会一直申请,在用户看来是被阻塞住了,非阻塞的意思是,申请失败就返回,没有阻塞效果。
五、总结
到这里我们的多线程部分就讲解到这里,也宣告我们系统部分就讲解到这里了,后面博主贵更新网络相关的知识,就是可以通过网络来获取数据来,让代码变得好玩起来了,我们这票就到这里了,我们下篇再见。