目录
线程互斥
进程线程间的互斥相关背景概念
互斥量mutex
测试
解决办法
互斥量接口
改进
互斥量实现原理探究
互斥量的封装
修改抢票
线程同步
条件变量
同步概念与竞态条件
同步
竞态条件
条件变量函数
初始化
销毁
等待条件满足
唤醒等待
案例
结果
生产者消费者模型
为何要使用生产者消费者模型
生产者消费者模型优点
基于BlockingQueue(阻塞队列)的生产者消费者模型
BlockingQueue
C++queue模拟阻塞队列的生产消费模型
测试
为什么pthread_cond_wait需要互斥量?
案例
条件变量使用规范
给条件发送信号
条件变量的封装
POSIX信号量
初始化信号量
销毁信号量
等待信号量
发布信号量
封装
基于环形队列的生产消费模型
线程池
日志与策略模式
什么是设计模式
日志
日志代码
步骤
线程池设计
线程池的应用场景
代码
线程安全的单例模式
什么是单例模式
懒汉模式与饿汉模式
懒汉模式
饿汉模式
代码
懒汉模式(线程安全)
单例模式线程池(懒汉)
线程安全与重入问题
线程安全
重入
可重入与线程安全的联系
可重入与线程安全的区别
死锁
死锁的四个必要条件
案例
线程互斥
进程线程间的互斥相关背景概念
临界资源:多个执行流共享的资源就叫临界资源
临界区:线程内访问临界资源的代码就叫临界区
互斥:任意时刻保证只有一个执行流进入临界区访问临界资源,通常对临界资源起保护作用
原子操作:不会被任何调度操作打断的操作,该操作只有两态,要么完成,要么未完成
互斥量mutex
1.大部分情况下,线程使用的数据都是局部数据,变量地址在线程栈空间内,这种情况下,其他线程就无法获取这种变量.
2.但有些时候,很多变量都需要在线程之间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
3.多个线程的并发操作会带来一些问题
测试
我们简单写一个模拟抢票的多线程程序来看看
#include<iostream>
#include<pthread.h>
#include<unistd.h>
int ticket=1000;
void* GetTicket(void* arg)
{while(ticket>0){usleep(1000);std::cout<<"获取到门票:"<<ticket--<<std::endl;}return nullptr;
}
int main()
{pthread_t t1,t2,t3,t4;pthread_create(&t1,nullptr,GetTicket,nullptr);pthread_create(&t2,nullptr,GetTicket,nullptr);pthread_create(&t3,nullptr,GetTicket,nullptr);pthread_create(&t4,nullptr,GetTicket,nullptr);while(ticket>0)sleep(1);pthread_join(t1,nullptr);pthread_join(t2,nullptr);pthread_join(t3,nullptr);pthread_join(t4,nullptr);return 0;
}

我们可以看到在多线程情况下程序会出现问题
产生的原因呢就是
1.因为if语句进行判断之后,在ticket--之前,当前线程被切换走了让其他线程继续执行,因为ticket还没有被修改,所以哪怕ticket本来应该为0了,但是由于进入了多个线程导致tiket最终变为负数
2.--操作本身也不是一个原子性操作,所以就算没有时间间隔,进入if语句就对tiket进行--,当前线程也可能会在完成之前被切走,因为--操作本身也代表着三条汇编指令:load、update、store
解决办法
为了解决上述问题,我们需要做到三点
1.代码必须要有互斥行为,当有线程进入临界区时,其他线程不能进入临界区
2.当多个线程同时要进入临界区且临界区当前没有线程进入时,只能要求一个线程进入临界区
3.当线程不在临界区执行时,不能阻止其他线程进入临界区
要做到以上三点,本质就是需要一把锁
Linux上提供的这把锁叫做互斥量

互斥量接口
分配
静态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER动态分配int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
参数:
mutex:要初始化的互斥量
attr:NULL
销毁
使⽤ PTHREAD_ MUTEX_ INITIALIZER 初始化的静态分配互斥量不需要销毁不要销毁⼀个已经加锁的互斥量已经销毁的互斥量,要确保后⾯不会有线程再尝试加锁int pthread_mutex_destroy(pthread_mutex_t *mutex);
加锁与解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);int pthread_mutex_unlock(pthread_mutex_t *mutex);返回值:成功返回0,失败返回错误号
调用pthread_mutex_lock时可能遇到两种情况:
1.互斥量正在被使用,或者多个线程同时尝试获取互斥量并且竞争失败了,那么尝试加锁就会陷入阻塞(执行流被挂起),等待互斥量解锁
2.互斥量处于未锁(即没有被其他线程使用),那么该函数将会锁定互斥量并返回成功
改进
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<sched.h>
int ticket=1000;
pthread_mutex_t mutex;
void* GetTicket(void* arg)
{while(true){pthread_mutex_lock(&mutex);if(ticket>0){usleep(1000);std::cout<<"获取到门票:"<<ticket--<<std::endl;pthread_mutex_unlock(&mutex);}else{pthread_mutex_unlock(&mutex);break;}}return nullptr;
}
int main()
{pthread_mutex_init(&mutex, NULL);//动态初始化pthread_t t1,t2,t3,t4;pthread_create(&t1,nullptr,GetTicket,nullptr);pthread_create(&t2,nullptr,GetTicket,nullptr);pthread_create(&t3,nullptr,GetTicket,nullptr);pthread_create(&t4,nullptr,GetTicket,nullptr);pthread_join(t1,nullptr);pthread_join(t2,nullptr);pthread_join(t3,nullptr);pthread_join(t4,nullptr);return 0;
}

互斥量实现原理探究
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和
内存单元的数据相交换,由于只有⼀条指令,保证了原子性,即使是多处理器平台,访问内存的总线周
期也有先后,⼀个处理器上的交换指令执行时另⼀个处理器的交换指令只能等待总线周期。现在
我们把lock和unlock的伪代码改⼀下

互斥量的封装
#include<iostream>
#include<string>
#include<pthread.h>
namespace LockModule
{class Mutex{public:Mutex(){pthread_mutex_init(&_mutex,nullptr);}Mutex(const Mutex& mutex)=delete;//锁不允许拷贝Mutex operator=(const Mutex& mutex)=delete;void Lock(){pthread_mutex_lock(&_mutex);}void Unlock(){pthread_mutex_unlock(&_mutex);}pthread_mutex_t* GetMutexOriginal(){return &_mutex;}~Mutex(){pthread_mutex_destroy(&_mutex);}private:pthread_mutex_t _mutex;};//RAII风格(资源获取即初始化)//将锁的初始化与对象的初始化进行绑定,锁的销毁与对象的析构进行绑定 class LockGuard{public:LockGuard(Mutex& mutex):_mutex(mutex){_mutex.Lock();}~LockGuard(){_mutex.Unlock();}private:Mutex& _mutex;};
}
修改抢票
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<sched.h>
#include"Lock.hpp"
int ticket=1000;
LockModule::Mutex mutex;
void* GetTicket(void* arg)
{while(true){LockModule::LockGuard guard(mutex);//RAII风格if(ticket>0){usleep(1000);std::cout<<"获取到门票:"<<ticket--<<std::endl;}else{break;}}return nullptr;
}
int main()
{pthread_t t1,t2,t3,t4;pthread_create(&t1,nullptr,GetTicket,nullptr);pthread_create(&t2,nullptr,GetTicket,nullptr);pthread_create(&t3,nullptr,GetTicket,nullptr);pthread_create(&t4,nullptr,GetTicket,nullptr);pthread_join(t1,nullptr);pthread_join(t2,nullptr);pthread_join(t3,nullptr);pthread_join(t4,nullptr);return 0;
}

线程同步
条件变量
当一个线程互斥地访问某个变量时,他可能发现在其他线程改变状态时它什么都做不了
例如某线程要访问队列,但是队列为空,则它只能等待其他线程在队列中放入数据。这种情况就要用到条件变量
同步概念与竞态条件
同步
在保证数据安全的前提下,能够让线程以某种特定顺序访问资源,从而有效避免饥饿问题
饥饿问题:由于不合理的调度或其他原因(如多个线程抢一把锁,但某些线程一直抢不到)使线程处于“饥饿状态”,任务无法完成
竞态条件
因为时序问题而导致程序异常,我们叫做竞态条件
条件变量函数
初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t* restrict attr);
参数:
cond:要初始化的条件变量
attr:NULL静态初始化:
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;动态:
pthread_cond_init(&cond,nullptr);
销毁
int pthread_cond_destroy(pthread_cond_t *cond)参数:
cond:要初始化的条件变量
等待条件满足
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t* restrict mutex);参数:
cond:要等待的条件变量
mutex:互斥量,一个条件变量指对应一把锁
这个接口的作用实际上就是将当前线程放入指定条件变量下等待
mutex实际上就是当前线程现在抢到了这把锁,现在要将线程放入条件变量下等待
线程需要先释放这把锁,然后在条件变量底下等待,在条件变量里就相当于就有了一个先后顺序。同时该条件变量记录唤醒线程时,线程需要获取的锁。
唤醒等待
唤醒线程之后,线程就会去尝试获取原先释放的锁,获取不到就阻塞等待
//唤醒在条件变量下等待的所有线程
int pthread_cond_broadcast(pthread_cond_t *cond);//唤醒一个线程
int pthread_cond_signal(pthread_cond_t *cond);
案例
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string.h>
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
void* active(void* arg)
{std::string name=static_cast<char*>(arg);while(true){pthread_mutex_lock(&mutex);pthread_cond_wait(&cond,&mutex);std::cout<<name<<"active..."<<std::endl;pthread_mutex_unlock(&mutex);}
}
int main()
{pthread_t t1,t2,t3,t4;pthread_create(&t1,nullptr,active,(void*)"thread_1");pthread_create(&t2,nullptr,active,(void*)"thread_2");pthread_create(&t3,nullptr,active,(void*)"thread_3");pthread_create(&t4,nullptr,active,(void*)"thread_4");sleep(3);while(true){pthread_cond_signal(&cond);//唤醒一个线程//pthread_cond_broadcast(&cond);//唤醒所有线程sleep(1);}return 0;
}
结果
如果唤醒一个线程的话那么每隔一秒钟只有一个线程打印消息
唤醒所有线程的话每个一秒钟所有线程都会打印消息
上面因为所有四个线程都被唤醒了,1秒之内,四个线程就会完成交替获取到锁,打印一次之后继续去条件变量底下等待
当然了,如果这里如果在条件变量底下等待的线程够多的话,那么就会出现一秒之内有些线程还没轮到,结果原先完成了任务的线程又被条件变量唤醒,继续争抢锁。
生产者消费者模型
一个生产者,一个消费者,一个交易场所
生产者将数据放入交易场所,消费者在交易场所有数据时取走数据
321原则(方便记忆)
三种关系,两种角色,一个交易场所
为何要使用生产者消费者模型
生产者消费者模式就是通过⼀个容器来解决生产者和消费者的强耦合问题。⽣产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于⼀个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
生产者消费者模型优点
1.解耦
2.支持并发
3.支持忙闲不均

基于BlockingQueue(阻塞队列)的生产者消费者模型
BlockingQueue
在多线程编程中阻塞队列(BlockingQueue)是⼀种常用于实现⽣产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)

C++queue模拟阻塞队列的生产消费模型
#pragma once
#include<iostream>
#include<pthread.h>
#include<string>
#include<queue>
template<typename T>
class BlockQueue
{public:BlockQueue(int cap):_cap(cap){_productor_wait_num=0;_consumer_wait_num=0;pthread_mutex_init(&_mutex,nullptr);pthread_cond_init(&_product_cond,nullptr);pthread_cond_init(&_consum_cond,nullptr);}void Enqueue(T& in)//生产者用的接口{pthread_mutex_lock(&_mutex);while(IsFull())//使用while判断,防止出现被其他条件变量优先取走任务,任务队列又为空{_productor_wait_num++;pthread_cond_wait(&_product_cond,&_mutex);_productor_wait_num--;}_block_queue.push(in);//std::cout<<"一个任务被放入"<<std::endl;if(_consumer_wait_num>0)pthread_cond_signal(&_consum_cond);pthread_mutex_unlock(&_mutex);}void Pop(T* out){pthread_mutex_lock(&_mutex);while(IsEmpty()){_consumer_wait_num++;pthread_cond_wait(&_consum_cond,&_mutex);_consumer_wait_num--;}*out=_block_queue.front();_block_queue.pop();//std::cout<<"一个任务被取走"<<std::endl;if(_productor_wait_num>0)pthread_cond_signal(&_product_cond);pthread_mutex_unlock(&_mutex);}bool IsEmpty(){return _block_queue.size()==0;}bool IsFull(){return _block_queue.size()==_cap;}~BlockQueue(){pthread_cond_destroy(&_product_cond);pthread_cond_destroy(&_consum_cond);pthread_mutex_destroy(&_mutex);}private:std::queue<T> _block_queue;int _cap;pthread_mutex_t _mutex;//保护block queue的锁pthread_cond_t _product_cond;//专⻔给⽣产者提供的条件变量pthread_cond_t _consum_cond;// 专⻔给消费者提供的条件变量int _productor_wait_num;int _consumer_wait_num;
};
测试
多生产单消费
#include"BlockQueue.hpp"
#include<functional>
using func_t =std::function<void()>;
BlockQueue<func_t> block_queue(3);
void* product(void* arg)
{std::string name=(char*)arg;for(int i=0;i<10;++i){func_t func=[name,i](){std::cout<<"消费者执行"<<name<<"派发的任务:"<<i<<std::endl;};block_queue.Enqueue(func);}return nullptr;
}void* execute(void* arg)
{for(int i=0;i<30;++i){func_t func;block_queue.Pop(&func);func();}return nullptr;
}
int main()
{pthread_t t1,t2,t3,t4;pthread_create(&t1,nullptr,product,(void*)"thread_1");pthread_create(&t2,nullptr,product,(void*)"thread_2");//可以修改为多个执行线程pthread_create(&t3,nullptr,product,(void*)"thread_3");pthread_create(&t4,nullptr,execute,(void*)"thread_4");pthread_join(t1,nullptr);pthread_join(t2,nullptr);pthread_join(t3,nullptr);pthread_join(t4,nullptr);return 0;
}

单生产多消费

为什么pthread_cond_wait需要互斥量?
条件等待时线程同步的一种方式,如果只有一个线程,如果只有一个线程,条件不满足,一直等待下去条件也不会满足。
所以需要其他线程通过某些操作修改共享变量,使原先不满足的条件满足,然后通知正在等待的进程。
条件不会无缘无故的满足了,这种操作必然会涉及对共享数据的修改,所以需要使用互斥锁来保护。否则就没法保证安全。

案例
那我们是不是可以通过在判断条件的时候上锁呢?发现条件不满足的时候再解锁
// 错误的设计
pthread_mutex_lock(&mutex);
while (condition_is_false) {pthread_mutex_unlock(&mutex);//解锁之后,等待之前,条件可能已经满⾜,信号已经发出,但是该信号可能被错过pthread_cond_wait(&cond);//然后就永久阻塞在这里了pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);
如上面的代码,如果刚解锁,还没去等待,就有其他线程火速获取到了锁并唤醒条件变量下的线程,那么这里的线程是不是就错过了唤醒的信号,直接永久阻塞在了这里。
因此,解锁与等待必须一次完成。(即解锁与等待这两个操作其实是一个原子操作)
条件变量使用规范
//等待条件变量代码
pthread_mutex_lock(&mutex);
while (条件为假)//因为可能被唤醒时其实已经被别的线程提前修改了pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);
比如说
1.当前线程收到信号被唤醒,但是有其他线程从上面先获取到了锁,当前线程被阻塞在了获取锁这一步。
2.其他线程修改完条件之后释放锁才被当前线程获取到。
这样的话我们就必须使用while循环判断,只有当条件明确满足时才轮到当前线程进行修改
给条件发送信号
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);
加锁的原因则是:
条件刚被设置为真,就被某没在条件变量下等待的线程修改为假了
条件变量的封装
#pragma once
#include"Lock.hpp"//上文写过的互斥量封装,包括lockguard
namespace CondModule
{using namespace LockModule;class Cond{public:Cond(){pthread_cond_init(&cond,nullptr);}~Cond(){pthread_cond_destroy(&cond);}void Wait(LockModule::Mutex& mutex){pthread_cond_wait(&cond,mutex.GetMutexOriginal());}void Notify(){pthread_cond_signal(&cond);}void NotifyAll(){pthread_cond_broadcast(&cond);}private:pthread_cond_t cond;};
}
POSIX信号量
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。但POSIX可以用于线程间同步。
初始化信号量
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
pshared:0表⽰线程间共享,⾮零表⽰进程间共享
value:信号量初始值
销毁信号量
int sem_destroy(sem_t *sem);
等待信号量
功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem);//P()
发布信号量
功能:发布信号量,表⽰资源使⽤完毕,可以归还资源了。将信号量值加1。
int sem_post(sem_t *sem);//V()
封装
#pragma once
#include<iostream>
#include<semaphore.h>
class Sem
{public:Sem(int n){sem_init(&_sem,0,n);}void P(){sem_wait(&_sem);}void V(){sem_post(&_sem);}~Sem(){sem_destroy(&_sem);}private:sem_t _sem;
};
基于环形队列的生产消费模型
上面我们写的生产消费者模型的空间大小是动态分配的。
这里我们使用信号量来写一个固定大小的环形队列重写生产消费模型
template<typename T>
class BlockQueue
{private:void Lock(pthread_mutex_t& mutex){pthread_mutex_lock(&mutex);}void UnLock(pthread_mutex_t& mutex){pthread_mutex_unlock(&mutex);}public:BlockQueue(int cap):_ring_queue(cap),_cap(cap),_room_sem(_cap),_data_sem(0),_productor_step(0),_consumer_step(0){pthread_mutex_init(&_productor_mutex,nullptr);pthread_mutex_init(&_consumer_mutex,nullptr);}void Enqueue(T& in)//生产者用的接口{Lock(_productor_mutex);_room_sem.P();_ring_queue[_productor_step++]=in;_productor_step%=_cap;std::cout<<"一个任务被放入"<<std::endl;UnLock(_productor_mutex);_data_sem.V();}void Pop(T* out){Lock(_consumer_mutex);_data_sem.P();*out=_ring_queue[_consumer_step++];_consumer_step%=_cap;std::cout<<"一个任务被取走"<<std::endl;UnLock(_consumer_mutex);_room_sem.V();}~BlockQueue(){pthread_mutex_destroy(&_productor_mutex);pthread_mutex_destroy(&_consumer_mutex);}private:std::vector<T> _ring_queue;int _cap;pthread_mutex_t _productor_mutex;pthread_mutex_t _consumer_mutex;Sem _room_sem;//生产者关心Sem _data_sem;//消费者关心int _productor_step;//生产者下标int _consumer_step;//消费者下标
};
线程池
好了,总算可以来我们的重点部分了。
前面的一大堆内容都在为这里的线程池服务
但是我们还需要做一点准备。
日志与策略模式
什么是设计模式
大佬们对一些经典常见场景给出的一些对应解决方案,就叫设计模式
日志
我们玩某些游戏的时候可能会出现游戏异常退出的情况,然后上网找原因,遇到最普遍的解答就是查看游戏日志。
计算机中的日志,是用来记录系统和软件运行过程中发生的事件的文件。
主要是用于监控状态,帮助程序员定位信息。
日志格式必须有以下几个指标:
1.时间戳
2.日志等级
3.日志内容
接下来几个指标是可选的(像行号什么的我个人认为还是有点必要加上的,不然真出现问题了都不知道上哪里找)
1.文件名
2.行号
3.进程,线程相关id
日志有现成的解决方案,但是在这里我们采用自定义日志的方式
这里我们采用设计模式-策略模式的方式进行日志的设计
例如,我们想要的日志格式如下:

日志代码
#pragma once
#include<iostream>
#include<string>
#include<fstream>
#include<memory>
#include<ctime>
#include<sstream>
#include<filesystem>
#include<unistd.h>
#include"Lock.hpp"
namespace LogModule
{using namespace LockModule;const std::string defaultpath="./log/";const std::string defaultname="log.txt";enum class LogLevel{DEBUG,INFO,WARNING,ERROR,FATAL};std::string LogLevelToString(LogLevel level){switch(level){case LogLevel::DEBUG:return "DEBUG";case LogLevel::INFO:return "INFO";case LogLevel::WARNING:return "WARNING";case LogLevel::ERROR:return "ERROR";case LogLevel::FATAL:return "FATAL";default:return "UNKNOWN";}return "UNKNOWN";}std::string GetCurrTime(){time_t tm=time(nullptr);struct tm curr;localtime_r(&tm,&curr);char timebuffer[64];snprintf(timebuffer,sizeof(timebuffer),"%4d-%02d-%02d:%02d:%02d:%02d",curr.tm_year+1900,curr.tm_mon+1,curr.tm_mday,curr.tm_hour,curr.tm_min,curr.tm_sec);return timebuffer;}//策略模式,策略接口class LogStrategy{public:virtual ~LogStrategy()=default;virtual void Synclog(const std::string Message)=0;};//控制台日志策略,在控制台打印消息class ConsoleLogStrategy:public LogStrategy{public:void Synclog(const std::string Message) override{LockGuard lockguard(_mutex);std::cerr<<Message<<std::endl;}private:Mutex _mutex;};class FileLogStrategy:public LogStrategy{public:FileLogStrategy(const std::string logpath=defaultpath,const std::string logname=defaultname):_logpath(logpath),_logfilename(logname){LockGuard guard(_mutex);if(std::filesystem::exists(_logpath))return;try{std::filesystem::create_directories(_logpath);}catch(const std::filesystem::filesystem_error& e){std::cerr << e.what() << '\n';}}void Synclog(const std::string Message) override{LockGuard lockguard(_mutex);std::string log=_logpath+_logfilename;std::ofstream out(log.c_str(),std::ios::app);if(!out.is_open())return;out<<Message<<'\n';return;}private:std::string _logpath;std::string _logfilename;Mutex _mutex;};class Logger{public:Logger(){UseConsoleStrategy();}void UseConsoleStrategy(){//unique_ptr不支持拷贝构造以及常规赋值,但是可以移动赋值//通过创建新的unique_ptr,释放旧的资源,接管新资源_strategy=std::make_unique<ConsoleLogStrategy>();}void UseFileStrategy(){_strategy=std::make_unique<FileLogStrategy>();}class LogMessage{private:LogLevel _type;std::string _curr_time;pid_t _pid;std::string _filename;int _line;Logger& _logger;std::string _log_info;public:LogMessage(LogLevel type,std::string& filename,int line,Logger& logger):_type(type),_curr_time(GetCurrTime()),_pid(getpid()),_filename(filename),_line(line),_logger(logger){std::stringstream ssbuffer;ssbuffer<<"["<<_curr_time<<"]"<<"["<<LogLevelToString(_type)<<"]"<<"["<<_pid<<"]"<<"["<<_filename<<"]"<<"["<<_line<<"]"<<"-";_log_info=ssbuffer.str();}template<typename T>LogMessage& operator<<(const T& info){std::stringstream ssbuffer;ssbuffer<<info;_log_info+=ssbuffer.str();return *this;}~LogMessage(){if(_logger._strategy)_logger._strategy->Synclog(_log_info);}};LogMessage operator()(LogLevel type,std::string filename,int line){return LogMessage(type,filename,line,*this);}private:std::unique_ptr<LogStrategy> _strategy;};//定义全局logger对象Logger logger;//__FILE__是当前文件名,__LINE__是行号#define LOG(type) logger(type,__FILE__,__LINE__)#define ENABLE_CONSOLE_STRATEGY() logger.UseConsoleStrategy()
#define ENABLE_FILE_STRATEGY() logger.UseFileStrategy()}
步骤
1.如上面的日志代码,我们定义虚基类LogStrategy,然后设计两个子类,分别为打印控制台策略与写入文件的策略,这样我们就可以利用多态完成我们日志策略的设计。
2.然后在具体的日志类Logger中,我们实现了一个内部类LogMessage,并且将我们的日志Logger指针传递给我们的LogMessage,这样,我们就可以让这个内部类在析构的时候将获取到的所有消息按照我们日志类确定的策略进行执行。
3.在LogMessage中,我们在构造时将前缀信息先准备好,然后我们对使用模板对'<<'进行运算符重载,这样就可以让所有类型的消息可以自然接在我们存放消息的字符串后面。
4.最终,我们在对日志类Log的'()'进行运算符重载,让他返回一个LogMessage对象,LogMessage对象将所有数据放入字符串,在析构时完成日志消息的存放(打印)。
5.我们最后可以定义一个全局的日志类对象,并对这个对象进行宏替换,让我们可以方便的进行日志策略模式的切换与日志的使用。
最终呈现的效果就是这样

线程池设计
线程池是一种线程使用模式。线程过多会带来调度开销,进而影响整体性能。
而线程池维护了多个线程,等待管理者分配并发任务,这避免了处理短时间任务时创建于销毁线程的开销。
线程池的应用场景
1.需要大量线程完成任务并且持续时间较短。比如WEB服务器处理网页请求任务。
但是对于长连接任务,线程池的优势就不明显了,因为连接时长远远比创建时间长的多。
2.对性能要求苛刻的应用,比如服务器快速响应用户请求
3.接受突发性的大量请求,但不至于使服务器产生大量线程的的应用。

代码
#pragma once
#include<vector>
#include<queue>
#include<memory>
#include<pthread.h>
#include"Log.hpp"
#include"Thread.hpp"
#include"Lock.hpp"
#include"Cond.hpp"
using namespace ThreadModule;
using namespace CondModule;
using namespace LockModule;
using namespace LogModule;
const static int gdefaultthreadnum=10;
template<typename T>
class ThreadPool
{private:void HandlerTask(){std::string name=GetThreadNameFromNptl();LOG(LogLevel::INFO)<<name<<"正在运行";while(true){_mutex.Lock();while(_task_queue.empty()&&_isrunning){_waitnum++;_cond.Wait(_mutex);_waitnum--;}if(_task_queue.empty()&&!_isrunning){_mutex.Unlock();break;}T t=_task_queue.front();_task_queue.pop();_mutex.Unlock();LOG(LogLevel::INFO)<<name<<"get a task";t();}}public:ThreadPool(int threadnum=gdefaultthreadnum):_threadnum(threadnum),_isrunning(false),_waitnum(0){LOG(LogLevel::INFO)<<"ThreadPool Construct";}void InitThreadPool(){for(int num=0;num<_threadnum;++num){_threads.emplace_back(std::bind(&ThreadPool::HandlerTask,this));LOG(LogLevel::INFO)<<"init thread "<<_threads.back().Name()<<" done";}}void start(){_isrunning=true;for(auto& thread:_threads){thread.Start();LOG(LogLevel::INFO)<<"start thread "<<thread.Name()<<" done";}}void stop(){//LockGuard guard(_mutex);//这里不能使用LockGuard,因为后面唤醒所有线程时,所有线程需要争抢手上这把锁//但是这把锁因为LockGuard,要最后才能释放//就导致该主线程又不释放锁,还在等其他线程拿到锁才能退出,直接死掉了//这种循环等待,和“死锁”不完全一样//“死锁”是互相等待对方线程的锁但自己又不释放自己的锁_mutex.Lock();_isrunning=false;_cond.NotifyAll();LOG(LogLevel::DEBUG)<<"线程池退出中";_mutex.Unlock();Wait();}void Wait(){for(auto& thread:_threads){thread.Join();LOG(LogLevel::DEBUG)<<thread.Name()<<"退出...";}}bool Enqueue(const T& in){bool ret=false;_mutex.Lock();if(_isrunning){_task_queue.push(in);if(_waitnum>0)_cond.Notify();LOG(LogLevel::DEBUG)<<"任务入队列成功";ret=true;}_mutex.Unlock();return ret;}private:int _threadnum;std::vector<Thread> _threads;std::queue<T> _task_queue;Mutex _mutex;Cond _cond;int _waitnum;bool _isrunning;
};
上文我在写代码时犯了个小小的错误,导致线程池无法退出,线程全部都被锁阻塞住了,具体原因我在代码的具体位置已经详细写出来了。
线程安全的单例模式
什么是单例模式
某些类,应该只有一个对象(实例),称为单例模式,例如上面使用的日志类其实使用的一直都只有一个logger类,我们就可以给他设计为单例模式。
在很多服务器开发场景中,经常要让服务器加载几百g的数据到内存中,此时往往需要使用一个类来管理这些数据。
懒汉模式与饿汉模式
懒汉:吃完饭后不洗碗,下次吃饭再洗
饿汉:吃完饭后马上洗碗,下次吃饭可以直接用碗
因此
懒汉模式
创建对象时不马上加载数据,真正使用时再加载数据,这样可以让服务器启动时比较快,但是后期还是要加载数据的,使用时难免会遇到卡顿。
核心思想为“延时加载”
饿汉模式
创建对象时马上加载数据,这样的话会导致服务器启动比较慢,但是使用时不会卡顿。
代码
#pragma once
#include<iostream>
//懒汉模式
// template<typename T>
// class Singleton
// {
// static T* inst;//对象创建时只创建一个空指针
// public:
// static T* GetInstance()//使用时再创建
// {
// if(inst==nullptr)
// inst=new T();
// return inst;
// }
// };//饿汉模式
// template<typename T>
// class Singleton
// {
// static T inst;//一开始就创建好对象
// public:
// static T* GetInstance()
// {
// return &inst;
// }
// };
但是懒汉模式这种延时加载有一个严重的线程安全问题
比如说两个线程同时访问,都检测到是个空指针,都需要创建对象
那我们就必须对懒汉模式进行一点点修改
懒汉模式(线程安全)
#include<iostream>
#include<mutex>
//懒汉模式
template<typename T>
class Singleton
{volatile static T* inst;//必须加上volatile,不能被优化掉static std::mutex _mutex;//cpp库里封装好的锁,和我们之前写的一样会自动初始化锁public:static T* GetInstance()//使用时再创建{if(inst==nullptr){_mutex.lock();if(inst==nullptr){inst=new T();}_mutex.unlock();}return inst;}
};
单例模式线程池(懒汉)
#pragma once
#include <vector>
#include <queue>
#include <memory>
#include <pthread.h>
#include "Log.hpp"
#include "Thread.hpp"
#include "Lock.hpp"
#include "Cond.hpp"
using namespace ThreadModule;
using namespace CondModule;
using namespace LockModule;
using namespace LogModule;
const static int gdefaultthreadnum = 10;// 改为懒汉模式
template <typename T>
class ThreadPool
{
private:ThreadPool(int threadnum = gdefaultthreadnum) : _threadnum(threadnum),_isrunning(false),_waitnum(0){LOG(LogLevel::INFO) << "ThreadPool Construct";}void InitThreadPool(){for (int num = 0; num < _threadnum; ++num){_threads.emplace_back(std::bind(&ThreadPool::HandlerTask, this));LOG(LogLevel::INFO) << "init thread " << _threads.back().Name() << " done";}}void start(){_isrunning = true;for (auto &thread : _threads){thread.Start();LOG(LogLevel::INFO) << "start thread " << thread.Name() << " done";}}void HandlerTask(){std::string name = GetThreadNameFromNptl();LOG(LogLevel::INFO) << name << "正在运行";while (true){_mutex.Lock();while (_task_queue.empty() && _isrunning){_waitnum++;_cond.Wait(_mutex);_waitnum--;}if (_task_queue.empty() && !_isrunning){_mutex.Unlock();break;}T t = _task_queue.front();_task_queue.pop();_mutex.Unlock();LOG(LogLevel::INFO) << name << "get a task";t();}}ThreadPool<T> &operator=(const ThreadPool<T> &threadpool) = delete;ThreadPool(const ThreadPool<T> &threadpool) = delete;public:static ThreadPool<T> *GetInstance(){if (_inst == nullptr){LockGuard guard(_mutex);if (_inst == nullptr){_inst = new ThreadPool<T>();//简单说下为什么静态成员函数可以访问类的构造函数//如果是普通的成员函数,需要一个具体的对象,因为需要this指针//而构造函数并不需要this指针//如果是public的话就算在类外,没有具体的对象也可以直接调用构造函数//但是如果时private的话,那么就只能通过类的成员函数进行调用了//这里的GetInstance只是没有this指针而已,无法访问其他类成员函数//但他本质还是一个类成员函数,所以可以访问作为私有成员函数的且不需要this指针的构造函数_inst->InitThreadPool();_inst->start();LOG(LogLevel::INFO) << "创建线程池单例";}}LOG(LogLevel::INFO) << "获取线程池单例";return _inst;}void stop(){// 这里不能使用LockGuard,因为后面唤醒所有线程时,所有线程需要争抢手上这把锁// 但是这把锁因为LockGuard,要最后才能释放// 就导致该主线程又不释放锁,还在等其他线程拿到锁才能退出,直接死掉了// 这种循环等待,和“死锁”不完全一样// “死锁”是互相等待对方线程的锁但自己又不释放自己的锁_mutex.Lock();_isrunning = false;_cond.NotifyAll();LOG(LogLevel::DEBUG) << "线程池退出中";_mutex.Unlock();Wait();}void Wait(){for (auto &thread : _threads){thread.Join();LOG(LogLevel::DEBUG) << thread.Name() << "退出...";}}bool Enqueue(const T &in){bool ret = false;_mutex.Lock();if (_isrunning){_task_queue.push(in);if (_waitnum > 0)_cond.Notify();LOG(LogLevel::DEBUG) << "任务入队列成功";ret = true;}_mutex.Unlock();return ret;}private:static ThreadPool<T> *_inst;int _threadnum;std::vector<Thread> _threads;std::queue<T> _task_queue;static Mutex _mutex;Cond _cond;int _waitnum;bool _isrunning;
};template <typename T>
ThreadPool<T> *ThreadPool<T>::_inst = nullptr;template <typename T>
Mutex ThreadPool<T>::_mutex;
中间也解释了下为什么静态成员函数可以访问私有构造函数

线程安全与重入问题
线程安全
多个线程访问同一个共享资源时能够正确的执行。
一般而言只有多个线程访问全局变量或者静态变量并且没有锁时会出现该问题
重入
同一个函数被不同的执行流执行,当前一个执行流还没执行完成时,后一个执行流又进入了,我们称为重入。
如果运行结果不会出现问题,则是可重入函数。
会出现问题,则是不可重入函数。
重入可以分为两种情况:
1.多线程重入
2.信号导致单执行流重入
可重入与线程安全的联系
1.函数是可重入的,那么线程就是安全的(记住这一句就够了)
2.函数不可重入,那么就不能由多个线程执行
3.如果一个函数有全局变量,那么这个函数就既不是线程安全,也不可重入
可重入与线程安全的区别
1.可重入函数是线程安全函数的一种
2.线程安全不一定是可重入的,而可重入函数一定线程安全(因为可以线程安全函数只被一个执行流访问,或者它压根就不访问全局,堆上的资源,只访问自己创建的栈上的资源)
3.如果对临界资源加上锁,那么这个函数就是线程安全的。但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的
线程安全侧重访问公共资源,表现得是并发线程的特点
可重入描述一个函数能否被重复进入,表示的是函数的特点
死锁
一组各个进程中均占有的不会释放的资源,但因互相申请被其他线程锁占用的资源而处于的一种永久等待状态
例如:
访问某个资源需要两把锁,但是线程A与B都持有其中的一把锁,并且都想访问这个资源。
两个线程都不释放锁,都去申请另一个线程持有的锁,那么就会形成死锁
死锁的四个必要条件
1.互斥条件:一个资源每次只能被一个执行流使用
2.请求与保持:一个执行流因请求资源而阻塞,对已获得资源保持不放
3.不剥夺条件:一个执行流已获得的资源,在使用完之前不能被强行剥离
4.循环等待:若干执行流之间形成一种头尾相接的循环等待资源的关系
要避免死锁,只需要破坏这四个条件就可以了
例如:
1.资源一次性分配
2.超时等待
3.加锁顺序一致
案例
程序员:给我offer
hr:给我简历
程序员:不给我offer我为啥给你简历
hr:不给我简历我怎么给你offer
好了,这样子就变成永久等待了
我们可以选择
1.明确写上,先给简历再发offer,没有简历就别来(确定顺序)
2.两个人吵久了,hr或者程序员受不了了走掉了(超时)
