1. 线程互斥
1.1 互斥相关概念
- 共享资源
- 临界资源:多线程执行流被保护的共享的资源就叫做临界资源
- 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
- 互斥:任何时刻,互斥保证有且只有一个执行流进⼊临界区,访问临界资源,通常对临界资源起 保护作用
- 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成, 要么未完成
1.2 观察一个现象,引入锁
以下是模拟售票代码:
cpp
//售票系统代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>
int ticket = 1000;
void *route(void *args)
{
char *id = (char *)args;
while (1)
{
if (ticket > 0)
{
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
}
else
{
break;
}
}
return nullptr;
}
int main()
{
pthread_mutex_t lock;
pthread_mutex_init(&lock, nullptr);
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL, route, (void *)"thread 1");
pthread_create(&t2, NULL, route, (void *)"thread 2");
pthread_create(&t3, NULL, route, (void *)"thread 3");
pthread_create(&t4, NULL, route, (void *)"thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
return 0;
}

为什么可能无法获得争取结果?
- if 语句判断条件为真以后,代码可以并发的切换到其他线程
- usleep模拟漫长业务的过程,在这个过程中,可能有多个线程会进入该代码段
- --ticket 操作本身就不是一个原子操作
-- 操作 并不是原子操作,而是对应三条汇编指令:
- load载入 :将共享变量ticket从内存加载到寄存器中
- update减少 :更新寄存器里面的值,执行-1操作
- store写回:将新值,从寄存器写回共享变量ticket的内存地址
要解决以上问题,需要做到三点:
- 代码必须要有互斥行为:当代码进⼊临界区执行时,不允许其他线程进入该临界区
- 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程 进入该临界区
- 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
- 要做到这三点,本质上就是需要一把锁 。Linux上提供的这把锁叫互斥量mutex
1.3 互斥量的接口
初始化
初始化互斥量有两种方法:
-
法1,静态分配:
cpppthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER -
法2,动态分配:
cppint pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); 参数: mutex:要初始化的互斥量 attr:NULL
销毁
销毁互斥量需要注意:
-
使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
-
不要销毁一个已经加锁的互斥量
-
已经销毁的互斥量,要确保后面不会有线程再尝试加锁
cppint pthread_mutex_destroy(pthread_mutex_t *mutex);
加锁解锁
cpp
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
用 pthread_ lock 时,可能会遇到以下情况:
- 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
- 发起函数调用时,当其他线程已经加上锁 ,或者多个线程一起申请锁,但没有竞争到锁,那么pthread_lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁
改进的售票系统:
cpp
#include <iostream>
#include <pthread.h>
#include <unistd.h>
int ticket = 1000;
pthread_mutex_t lock;
void *route(void *args)
{
char *id = (char *)args;
while (1)
{
pthread_mutex_lock(&lock);
if (ticket > 0)
{
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
pthread_mutex_unlock(&lock);
}
else
{
pthread_mutex_unlock(&lock);
break;
}
}
return nullptr;
}
int main()
{
pthread_mutex_init(&lock, nullptr);
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL, route, (void *)"thread 1");
pthread_create(&t2, NULL, route, (void *)"thread 2");
pthread_create(&t3, NULL, route, (void *)"thread 3");
pthread_create(&t4, NULL, route, (void *)"thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
pthread_mutex_destroy(&lock);
return 0;
}

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

1.5 互斥量的封装
Mutex.hpp
cpp
#include <iostream>
#include <pthread.h>
namespace MutexModule
{
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&_mutex, nullptr);
}
void Lock()
{
int n = pthread_mutex_lock(&_mutex);
if (n != 0)
{
std::cout << "Mutex 初始化失败!" << std::endl;
}
}
void Unlock()
{
int n = pthread_mutex_unlock(&_mutex);
if (n != 0)
{
std::cout << "Mutex 解锁失败!" << std::endl;
}
}
~Mutex()
{
}
private:
pthread_mutex_t _mutex;
};
// 在封装一层, 采⽤RAII⻛格,进行锁管理
class LockGuard
{
public:
LockGuard(Mutex &mutex)
: _mutex(mutex)
{
_mutex.Lock();
}
~LockGuard()
{
_mutex.Unlock();
}
private:
Mutex &_mutex;
};
}
main.cpp
cpp
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include "Mutex.hpp"
using namespace MutexModule;
int ticket = 1000;
// pthread_mutex_t lock;
class ThreadData
{
public:
ThreadData(Mutex &lock, const std::string &n)
:lockp(&lock),name(n)
{
}
~ThreadData()
{
}
Mutex *lockp;
std::string name;
};
void *route(void *args)
{
//char *id = (char *)args;
ThreadData* td =static_cast<ThreadData*>(args);
while (1)
{
//pthread_mutex_lock(&lock);
//td->lockp->Lock();
LockGuard grand(*td->lockp);
if (ticket > 0)
{
usleep(1000);
printf("%s sells ticket:%d\n", td->name.c_str(), ticket);
ticket--;
//pthread_mutex_unlock(&lock);
// td->lockp->Unlock();
}
else
{
//pthread_mutex_unlock(&lock);
//td->lockp->Unlock();
break;
}
}
return nullptr;
}
int main()
{
// pthread_mutex_init(&lock, nullptr);
Mutex lock;
pthread_t t1, t2, t3, t4;
ThreadData* td1=new ThreadData(lock,"thread 1");
pthread_create(&t1, NULL, route, td1);
ThreadData* td2=new ThreadData(lock,"thread 2");
pthread_create(&t2, NULL, route, td2);
ThreadData* td3=new ThreadData(lock,"thread 3");
pthread_create(&t3, NULL, route,td3);
ThreadData* td4=new ThreadData(lock,"thread 4");
pthread_create(&t4, NULL, route, td4);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
//pthread_mutex_destroy(&lock);
return 0;
}
2. 线程同步
2.1 条件变量
当一个线程访问队列时,发现队列为空,只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量
2.1.1 同步概念与竞态条件
- 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免 饥饿问题,叫做同步
- 竞态条件:因为时序问题,导致程序异常,称之为竞态条件
2.1.2 条件变量函数
初始化
cpp
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t
*restrict attr);
参数:
cond:要初始化的条件变量
attr:NULL
销毁
cpp
int pthread_cond_destroy(pthread_cond_t *cond)
等待条件满足
cpp
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict
mutex);
参数:
cond:要在这个条件变量上等待
mutex:锁
唤醒等待
cpp
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
案例:
cpp
#include <iostream>
#include <pthread.h>
#include <vector>
#include <unistd.h>
#define NUM 5
int cnt = 1000;
pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t gcond = PTHREAD_COND_INITIALIZER;
等待是需要等,什么条件才会等呢?
当临界资源不足,等待之前,就要对资源的数量进行判定。
判定本身就是访问临界资源!,判断一定是在临界区内部的.
判定结果,也一定在临界资源内部。所以,条件不满足要休眠,
也一定是在临界区内休眠!
证明一件事情:条件变量,可以允许线程等待
可以允许一个线程唤醒在cond等待的其他线程
实现同步过程
void *threadrun(void *argc)
{
std::string name = static_cast<char *>(argc);
while (1)
{
pthread_mutex_lock(&glock);
// 直接让对用的线程进行等待?? 临界资源不满足导致我们等待的
pthread_cond_wait(&gcond, &glock);
// glock在pthread_cond_wait之前,会被自动释放掉
std::cout << name << " 计算: " << cnt << std::endl;
cnt++;
pthread_mutex_unlock(&glock);
}
return nullptr;
}
int main()
{
std::vector<pthread_t> threads;
for (int i = 0; i < NUM; i++)
{
pthread_t tid;
char *name = new char[64];
snprintf(name, 64, "thread-%d", i);
int n = pthread_create(&tid, nullptr, threadrun, name);
if (n != 0)
continue;
threads.push_back(tid);
sleep(1);
}
while (1)
{
std::cout << "唤醒一个线程... " << std::endl;
pthread_cond_signal(&gcond);
sleep(1);
}
for (auto &id : threads)
{
pthread_join(id, nullptr);
}
return 0;
}
2.1.3 生产者消费者模型
记忆:"321原则"

- 生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题
- 生产者和消费者彼此之间 不直接通讯,而通过阻塞队列来进行通讯,两者不用互相等待,阻塞队列就相当于一个缓冲区, 平衡了生产者和消费者的处理能力
- 阻塞队列就是给生产者和消费者解耦的
生产者消费者模型优点:
- 解耦
- 支持并发
- 支持忙闲不均
2.1.4 基于BlockingQueue的生产者消费者模型
在多线程编程中**阻塞队列(Blocking Queue)**是一种常用于实现生产者和消费者模型的数据结构
- 当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入元素
- 当队列为满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出
BlockQueue.hpp
cpp
#include <iostream>
#include <pthread.h>
#include <queue>
const int defaultcap = 5;
template <typename T>
class BlockQueue
{
private:
bool IsFull()
{
return _q.size() >= _cap;
}
bool IsEmpty()
{
return _q.empty();
}
public:
BlockQueue(int cap = defaultcap)
: _cap(cap), _csleep_num(0), _psleep_num(0)
{
pthread_mutex_init(&_mutex,nullptr);
pthread_cond_init(&_full_cond,nullptr);
pthread_cond_init(&_empty_cond,nullptr);
}
void Equeue(const T &in)
{
// 生产者调用
pthread_mutex_lock(&_mutex);
while (IsFull())
{ // 满了就进行等待
// hread_cond_wait调用成功,挂起当前线程之前,要先自动释放锁
// 当线程被唤醒的时候,默认就在临界区内唤醒!要从pthread_cond_wait
// 成功返回,需要当前线程,重新申请_mutex锁!!!
// 如果被唤醒,但是申请锁失败了??就会在锁上阻塞等待
_psleep_num++;
pthread_cond_wait(&_full_cond, &_mutex);
_psleep_num--; // pthread_cond_wait 是阻塞函数!
// 调用它之后,线程会直接休眠,卡在这一行,完全不会往下执行 --!
// 只有线程被唤醒了,wait 函数返回了,才会走到下一行的
}
_q.push(in);
if (_csleep_num > 0)
{
pthread_cond_signal(&_empty_cond);
std::cout << "唤醒消费者" << std::endl;
}
pthread_mutex_unlock(&_mutex);
}
T Pop()
{
// 消费者调用
pthread_mutex_lock(&_mutex);
while(IsEmpty())
{
_csleep_num++;
pthread_cond_wait(&_empty_cond,&_mutex);
_csleep_num--;
}
T data =_q.front();
_q.pop();
if(_psleep_num>0)
{
pthread_cond_signal(&_full_cond);
std::cout << "唤醒生产者" << std::endl;
}
pthread_mutex_unlock(&_mutex);
return data;
}
~BlockQueue()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_full_cond);
pthread_cond_destroy(&_empty_cond);
}
private:
std::queue<T> _q; // 临界资源
int _cap;
pthread_mutex_t _mutex;
pthread_cond_t _full_cond; // 唤醒消费者
pthread_cond_t _empty_cond; // 唤醒生产者
int _csleep_num; // 消费者休眠的个数
int _psleep_num; // 生产者休眠的个数
};
结果:
生产了一个任务: 1+1=?
生产了一个任务: 2+2=?
生产了一个任务: 3+3=?
生产了一个任务: 4+4=?
生产了一个任务: 5+5=?
生产了一个任务: 6+6=? 队列容量是 5,现在满了!生产者阻塞!
唤醒生产者 消费者开始消费,腾出位置
消费了一个任务: 1+1=2 消费最早进入队列的 1+1
生产了一个任务: 7+7=?
唤醒生产者
生产了一个任务: 8+8=?
消费了一个任务: 2+2=4
唤醒生产者
消费了一个任务: 3+3=6
生产了一个任务: 9+9=?
2.1.5 为什么pthread_cond_wait需要互斥量?
条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足, 所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好 的通知等待在条件变量上的线程
条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保 护。没有互斥锁就无法安全的获取和修改共享数据
必须先加锁再操作条件变量,如果先cont_wait,再加锁:
- cont_wait 要求必须持有锁 才能执行,无锁调用会触发未定义行为(程序崩溃 / 卡死);
- 即使侥幸没崩溃,线程会先阻塞在 wait,永远无法执行后面的 lock,形成死锁

由于解锁和等待不是原子操作。调用解锁之后, pthread_cond_wait 之前,如果已经有其他线程获取到互斥量,摒弃条件满足,发送了信号,那么 pthread_cond_wait 将错过这个信号,可能会导致线程永远阻塞在这个 pthread_cond_wait 作
2.1.6 条件变量的封装
Cond.hpp
cpp
#pragma once
#include <iostream>
#include <pthread.h>
#include "Mutex.hpp"
using namespace MutexModule;
namespace CondModule
{
class Cond
{
public:
Cond()
{
pthread_cond_init(&_cond, nullptr);
}
void Wait(Mutex & mutex)
{
int n = pthread_cond_wait(&_cond, mutex.Get());
(void)n;
}
void Signal()
{
int n = pthread_cond_signal(&_cond);
(void)n;
}
void Broadcast()
{
int n = pthread_cond_broadcast(&_cond);
(void)n;
}
~Cond()
{
pthread_cond_destroy(&_cond);
}
private:
pthread_cond_t _cond;
};
}
2.2 POSIX信号量(sem)
**POSIX 信号量是「资源计数工具」,**和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源方的。但POSIX可以用于线程间同步
初始化
cpp
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
pshared:0表示线程间共享,非零表示进程间共享
value:信号量初始值
销毁
cpp
int sem_destroy(sem_t *sem);
等待
cpp
功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem); //P()
发布
cpp
功能:发布信号量,表⽰资源使⽤完毕,可以归还资源了。将信号量值加1。
int sem_post(sem_t *sem);//V()
2.2.2 基于环形队列的生产消费模型(sem_ring)

把信号量看作计数器,来进行多线程间的同步过程
封装sem.hpp
cpp
#include <iostream>
#include <pthread.h>
#include <semaphore.h>
namespace SemModule
{
const int defaultvalue = 1;
class Sem
{
public:
Sem(unsigned int sem_value = defaultvalue)
{
sem_init(&_sem, 0, sem_value);
}
void P()
{
int n = sem_wait(&_sem);
(void)n;
}
void V()
{
int n = sem_post(&_sem);
(void)n;
}
~Sem()
{
sem_destroy(&_sem);
sem_destroy(&_sem);
}
private:
sem_t _sem;
};
}
RingQueue.hpp
cpp
#pragma once
#include <iostream>
#include <vector>
#include "Sem.hpp"
#include "Mutex.hpp"
using namespace MutexModule;
using namespace SemModule;
static const int gcap = 5;
template <typename T>
class RingQueue
{
public:
RingQueue(int cap = gcap)
: _cap(cap),
_rq(cap),
_blank_sem(cap),
_data_sem(0),
_c_step(0),
_p_step(0)
{
}
void Equeue(const T &in)
{
_blank_sem.P();
{
LockGuard lockguard(_pmutex);
// 1. 生产
_rq[_p_step] = in;
// 2. 更新下标
++_p_step;
// 3. 维持环形特性
_p_step %= _cap;
}
_data_sem.V();
}
void Pop(T *out)
{
_data_sem.P();
{
LockGuard lock(_cmutex);
*out=_rq[_c_step];
++_c_step;
_c_step%=_cap;
}
_blank_sem.V();
}
~RingQueue()
{
}
private:
std::vector<T> _rq;
int _cap;
Sem _blank_sem;
Sem _data_sem;
Mutex _cmutex;
Mutex _pmutex;
//生产和消费的下标
int _c_step;
int _p_step;
};
Sem_Ring实现
https://gitee.com/dwaekkiyo/my_linux_code/tree/master/260329/4.Sem_Ring
3. 线程池
进行线程池的封装需要做如下准备:
- 线程的封装
- 锁和条件变量的封装
- 引入日志,对线程进行封装
3.1 日志与策略模式
**日志认识:**计算机中的日志是记录系统和软件运行中发生事件的文件,主要作用是监控运行状态、记录异常信息,帮助快速定位问题并支持程序员进行问题修复。是系统维护、故障排查和安全管理的重要工具
日志格式以下几个指标是必须得有的:
- 时间戳
- 日志等级
- 日志内容
- 以下几个指标是可选 :
- 文件名行号
- 进程,线程相关id信息等

日志有现成的解决方案,如:spdlog、glog、Boost.Log、Log4cxx等等
这里采用设计模式-策略模式来进行日志的设计:
Log.hpp
cpp
#ifndef __LOG_HPP
#define __LOG_HPP
#include <iostream>
#include <filesystem>
#include <fstream>
#include <unistd.h>
#include <memory> //智能指针头文件
#include <sstream> //std::stringstream头文件
#include <ctime>
#include "Mutex.hpp"
using namespace MutexModule;
namespace LogModule
{
// 策略模式,策略接口
class LogStrategy
{
public:
~LogStrategy()
{
}
virtual void Synclog(const std::string &message) = 0;
};
// 显示器打印日志的策略 : 子类
class ConsoleLogStrategy : public LogStrategy
{
public:
ConsoleLogStrategy()
{
}
void Synclog(const std::string &message) override
{
// 要原子写入
LockGuard lock(_mutex);
std::cout << message << std::endl;
}
~ConsoleLogStrategy()
{
}
private:
Mutex _mutex;
};
// 文件打印日志的策略 : 子类
const std::string defaultpath = "./log";
const std::string defaultfile = "my.log";
class FileLogStrategy : public LogStrategy
{
public:
FileLogStrategy(const std::string &path = defaultpath, const std::string &file = defaultfile)
: _path(path),
_file(file)
{
// 构造时自动加锁,析构时自动解锁,保证线程安全
LockGuard lock(_mutex);
if (std::filesystem::exists(_path))
{ // C++17 标准跨平台判断路径是否存在
return;
}
try
{
std::filesystem::create_directories(_path);
}
catch (const std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << std::endl;
}
}
void Synclog(const std::string &message) override
{
LockGuard lock(_mutex);
std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file;
// ofstream是iostream子类(文件专用,可以直接创建对象
// std::ios::app追加写入
std::ofstream out(filename, std::ios::app);
if (!out.is_open())
{
return;
}
out << message << "\r\n";
out.close();
}
~FileLogStrategy()
{
}
private:
std::string _path; // 日志文件所在路径
std::string _file; // 日志文件本身
Mutex _mutex;
};
// 形成一条完整的日志&&根据上面的策略,选择不同的刷新方式
// 1. 形成日志等级
enum class LogLevel
{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
std::string Level2str(LogLevel level)
{
switch (level)
{
case LogLevel::DEBUG:
return "DEBUG";
case LogLevel::ERROR:
return "ERROR";
case LogLevel::FATAL:
return "FATAL";
case LogLevel::INFO:
return "INFO";
case LogLevel::WARNING:
return "WARNING";
default:
return "UNKNOWN";
}
}
std::string GetTimeStamp()
{
// 获取当前的时间戳
time_t curr = time(nullptr);
struct tm curr_tm;
localtime_r(&curr, &curr_tm);
char timebuffer[128];
snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d",
curr_tm.tm_year + 1900,
curr_tm.tm_mon + 1,
curr_tm.tm_mday,
curr_tm.tm_hour,
curr_tm.tm_min,
curr_tm.tm_sec);
return timebuffer;
}
// Logger类形成日志,根据不同传入调用策略,完成刷新
class Logger
{
public:
Logger()
{
// 默认是向屏幕写入
EnableConsoleLogStrategy();
}
void EnableConsoleLogStrategy()
{
// std::make_unique C++14创建unique智能指针
_fflush_strategy = std::make_unique<ConsoleLogStrategy>();
}
void EnableFileLogStrategy()
{
_fflush_strategy = std::make_unique<FileLogStrategy>();
}
// 这个内部类表示一条日志的格式
class LogMessage
{
public:
LogMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger)
: _curr_time(GetTimeStamp()),
_level(level),
_pid(getpid()),
_src_name(src_name),
_line_number(line_number),
_logger(logger)
{
// 日志的左边部分,合并起来
// std::stringstream 是字符串流,
// 核心用于字符串与其他类型互转、拼接、分割
std::stringstream ss;
ss << '[' << _curr_time << ']'
<< '[' << Level2str(_level) << ']'
<< '[' << _pid << ']'
<< '[' << _src_name << ']'
<< '[' << _line_number << ']'
<< '-';
_loginfo = ss.str();
}
// 因为会有下面这种连续输入场景
// 需要重载<<,也需要返回当前对象
// LogMessage() << "hell world" << "XXXX" << 3.14 << 1234
template <typename T>
LogMessage &operator<<(const T &info)
{
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this;
}
~LogMessage()
{
// 表示一条日志
// 每次析构可以进行一次刷新
if (_logger._fflush_strategy)
{
_logger._fflush_strategy->Synclog(_loginfo);
// 刷新特定方式
}
}
private:
std::string _curr_time;
LogLevel _level;
pid_t _pid;
std::string _src_name;
int _line_number;
std::string _loginfo;
Logger &_logger;
};
~Logger()
{
}
// 为了能够直接调用Logger,要重载(),返回生成logmessage对象
// 对象出周期后自动刷新
// 故意拷贝,形成LogMessage临时对象,后续在被<<时,会被持续引用
// 直到完成输⼊,才会子动析构临时LogMessage
LogMessage operator()(LogLevel level, std::string name, int line)
{
return LogMessage(level, name, line, *this);
//、⾄此也完成了驲志的显示或者刷
}
private:
// 智能指针--->不用考虑析构问题
std::unique_ptr<LogStrategy> _fflush_strategy;
};
// 全局日志对象
Logger logger;
// 调用逻辑:logger-->logger()-->Logmessage--> << -->析构刷新打印
// 使用宏,简化用户操作,获取文件名和行号
#define LOG(level) logger(level, __FILE__, __LINE__)
#define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()
#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()
}
#endif
3.2 线程池设计
线程池:
一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可⽤线程数量应该取决于可⽤的并 发处理器、处理器内核、内存、网络sockets等的数量
线程池的应用场景:
- 需要大量的线程来完成任务,且完成任务的时间比较短。如WEB服务器完成网页请求这样的任 务,因为单个任务小,而任务数量巨大。但对于长时间的任务,如一个Telnet连接请求,线程池的优点就不明显了。因为 Telnet会话时间比线程的创建时间⼤多了
- 对性能要求苛刻的应用,如要求服务器迅速响应客户请求
- 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误
线程池的种类
- 创建固定数量线程池,循环从任务队列中获取任务对象,获取到任务对象后,执行任务对象中的任务接口
- 浮动线程池,其他同上此处
这里选择固定线程个数的线程池
3.3 线程安全的单例模式
1)单例概念
**确保一个类只能创建唯一一个对象,全局共享,且提供统一的访问方式,**在很多服务器开发场景中,经常需要让服务器加载很多的数据(上百G)到内存中。此时往往要用一个单例的类来管理这些数据
2)单例特点
- 构造函数私有化:private 修饰构造函数,禁止外部 new 创建对象
- 删除拷贝构造 + 赋值运算符:C++ 特有!防止对象被拷贝 / 赋值,破坏单例
- 全局唯一实例:类内维护一个静态实例,整个程序只有一份
- 静态获取接口:public static 方法获取实例
- 线程安全:多线程下不会创建多个实例(重点)
3)饿汉式 vs 懒汉式
- 饿汉式 :饿了马上吃 → 程序一启动(类加载时)就创建唯一实例,立即加载
- 懒汉式 :懒到极致 → 第一次使用时才创建实例,延迟加载
4)饿汉方式实现单例
cpp
template <typename T>
class Singleton {
static T data;
public:
static T* GetInstance() {
return &data;
}
};
// 必须在类外初始化静态成员
template<typename T>
T Singleton<T>::data;
缺点程序启动就创建对象,不管你用不用,都会创建,浪费资源
5)懒汉方式实现单例
cpp
template <typename T>
class Singleton
{
static T *inst;
1 2 3public : static T *GetInstance()
{
if (inst == NULL)
{
inst = new T();
}
return inst;
}
};
**单线程安全、多线程不安全。**如果两个线程同时调用,可能会创建出两份T对象的实例。但是后续再次调用,就没有问题了
6)懒汉方式实现单例模式(线程安全版本)
cpp
// 懒汉模式, 线程安全
template <typename T>
class Singleton {
// 1. volatile 关键字:禁止编译器指令重排 + 禁止缓存优化(DCL核心!)
volatile static T* inst;
// 2. 互斥锁:保证临界区(new对象)原子操作,只执行一次
static std::mutex lock;
public:
static T* GetInstance() {
// 第一次判空:无锁判断,99%场景直接返回,大幅提升性能
if (inst == NULL) {
// 加锁:只有第一次创建时才加锁,避免多线程同时new
lock.lock();
// 第二次判空:防止多个线程同时通过第一次判断,重复创建对象
if (inst == NULL) {
inst = new T();
}
lock.unlock(); // 解锁
}
return inst;
}
};
3.4 线程池实现(单例版本)
ThreadPool.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include <queue>
#include <vector>
#include "Log.hpp"
#include "Thread.hpp"
#include "Cond.hpp"
#include "Task.hpp"
namespace ThreadPoolModule
{
using namespace ThreadModlue;
using namespace LogModule;
using namespace MutexModule;
using namespace CondModule;
static const int gnum = 5;
template <typename T>
class ThreadPool
{
private:
ThreadPool(const int num = gnum)
: _num(num),
_isrunning(false),
_sleepernum(0)
{
for (int i = 0; i < _num; i++)
{ // 构造线程,让线程启动后就去跑
// 每个线程绑定 Lambda
_threads.emplace_back(
[this]()
{
HandlerTask();
});
}
}
void Start()
{
if (_isrunning)
return;
_isrunning = true;
for (auto &thread : _threads)
{
thread.Start();
LOG(LogLevel::INFO) << "start new thread success: " << thread.Name();
}
}
void WakeUpAllThread()
{
LockGuard lock(_mutex);
//必须加锁:是逻辑需要,为了安全读取_sleepernum
if (_sleepernum)
{
_cond.Broadcast();
}
LOG(LogLevel::DEBUG) << "唤醒所有线程";
}
void WakeUpOne()
{
//LockGuard lock(_mutex);
_cond.Signal();
LOG(LogLevel::DEBUG)<<"唤醒一个线程";
}
ThreadPool(const ThreadPool<T> &) = delete;
ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;
public:
static ThreadPool<T> *GetInstance()
{
if (inc == nullptr)
{
LockGuard lock(_lock);
LOG(LogLevel::DEBUG) << "获取单例..";
if (inc == nullptr)
{
inc = new ThreadPool<T>();//先创建线程
LOG(LogLevel::DEBUG) << "首次使用单例,创建..";
inc->Start();//再开启线程池
}
}
return inc;
}
void Stop()
{
if (!_isrunning)
return;
_isrunning = false;
WakeUpAllThread();
}
void Join()
{
for (auto &thread : _threads)
{
thread.Join();
}
}
void HandlerTask()
{
char name[128];
pthread_getname_np(pthread_self(), name, sizeof(name));
while (1)
{
T t;
{
LockGuard lock(_mutex);
while (_taskq.empty() && _isrunning)
{ // while 是防止虚假唤醒,Linux 多线程标准写法
_sleepernum++;
_cond.Wait(_mutex);
_sleepernum--;
}
// 当线程中任务全部被取完&&_isrunning==false时才能退出
if (_taskq.empty() && _isrunning == false)
{
LOG(LogLevel::INFO) << name << " 退出了, 线程池退出&&任务队列为空";
break;
}
// 一定有任务
t = _taskq.front();
// 从q中获取任务,任务已经是线程私有的了
_taskq.pop();
}
t(); // 处理任务,不需要在临界区内部处理
}
}
bool Enqueue(const T &in)
{
if (_isrunning)
{
LockGuard lock(_mutex);
_taskq.push(in);
if (_threads.size() == _sleepernum)
WakeUpOne();
return true;
}
return false;
}
~ThreadPool()
{
}
private:
std::vector<Thread> _threads;
int _num; // 线程池中,线程的个数
std::queue<T> _taskq;
Cond _cond;
Mutex _mutex;
bool _isrunning;
int _sleepernum;
static ThreadPool<T> *inc;
static Mutex _lock;
};
template <typename T>
ThreadPool<T> *ThreadPool<T>::inc = nullptr;
template <typename T>
Mutex ThreadPool<T>::_lock;
}
线程池的实现代码
https://gitee.com/dwaekkiyo/my_linux_code/tree/master/260403/5.ThreadPool
4. 线程安全和重入问题
4.1 概念
线程安全:多个线程 同时并发执行一个函数,不会出现数据竞争、结果错误、死锁等问题,这个函数就是线程安全的
可重入:同一个线程 中,函数还没执行完 ,就被再次调用 (递归调用、信号 / 中断打断),依然能正确执行、不破坏数据,这个函数就是可重入的
重入其实可以分为两种情况
- 多线程重入函数
- 信号导致一个执行流重复进入函数
4.2 结论
可重入与线程安全联系 :
- 函数是可重入的,那就是线程安全的 (其实知道这一句话就够了)
- 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
- 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的
可重入与线程安全区别:
- 可重入函数是线程安全函数的一种
- 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
- 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重一函数若锁还未释放则会产生死锁,因此是不可重入的
5. 死锁
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会 释放的资源而处于的一种永久等待状态
为了方便表述,假设现在线程A,线程B必须同时持有锁1和锁2,才能进行后续资源的访问
死锁的四个必要条件
- **互斥条件:**资源同一时刻只能被一个进程使用
- **请求与保持条件:**进程已持有资源,又去申请新资源,且不释放已占有的
- **不可剥夺条件:**进程已获得的资源,只能自己用完释放,不能被强行抢走
- **循环等待条件:**多个进程形成环形等待,每个等下一个释放资源
6. STL,智能指针和线程安全
6.1 STL中的容器是否是线程安全的?
- 不是
- 原因是:STL的设计初衷是将性能挖掘到极致,旦涉及到加锁保证线程安全,会对性能造成巨大的影响。而且对于不同的容器,加锁方式的不同,性能可能也不同(例如hash表的锁表和锁桶)
- 因此 STL默认不是线程安全。如果需要在多线程环境下使用,往往需要调用者自行保证线程安全
6-2 智能指针是否是线程安全的?
- 对于 unique_ptr,由于只是在当前代码块范围内生效,因此不涉及线程安全问题。
- 对于 shared_ptr,多个对象需要共用一个引用计数变量,所以会存在线程安全问题,但是标准库实现的时候考虑到了这个问题,基于原子操作(CAS)的方式保证 shared_ptr 能够高效,原子的操作引用计数