目录
[4. 线程池模块](#4. 线程池模块)
[4.1 线程池的初步编写](#4.1 线程池的初步编写)
[4.3 线程安全的单例模式](#4.3 线程安全的单例模式)
[4.3.1 单例模式的特点](#4.3.1 单例模式的特点)
[4.3.2 单例的两种实现方法](#4.3.2 单例的两种实现方法)
[4.3.2.1 饿汉方式实现单例模式](#4.3.2.1 饿汉方式实现单例模式)
[4.3.2.2 懒汉方式实现单例模式](#4.3.2.2 懒汉方式实现单例模式)
[4.4 将上面写的线程池改成单例的](#4.4 将上面写的线程池改成单例的)
[4.4.1 简单的单例模式线程的代码:](#4.4.1 简单的单例模式线程的代码:)
[4.4.2 升级的单例模式线程的代码:](#4.4.2 升级的单例模式线程的代码:)
[5. 线程安全和重入问题](#5. 线程安全和重入问题)
[6. 常见锁概念](#6. 常见锁概念)
[6.1 死锁](#6.1 死锁)
[6.2 死锁的四个必要条件](#6.2 死锁的四个必要条件)
[6.3 避免死锁](#6.3 避免死锁)
[7. STL,智能指针和线程安全](#7. STL,智能指针和线程安全)
[7.1 STL中的容器是否是线程安全的?](#7.1 STL中的容器是否是线程安全的?)
[7.2 智能指针(本身,不是智能指针指向的对象)是否是线程安全的?](#7.2 智能指针(本身,不是智能指针指向的对象)是否是线程安全的?)
[8. 其它常见的各种锁(了解)](#8. 其它常见的各种锁(了解))
4. 线程池模块
线程池是一种池化技术,预先创建出一批线程,预先得让每一个线程有自己要完成的任务,给外部的接口就一个外部给线程池入数据的生产的接口,消费的接口不用提供,由内部的线程池自动完成。这就是典型的生产消费模型。
4.1 线程池的初步编写
cpp
//Mutex.hpp
#pragma once
#include <iostream>
#include <mutex>
#include <pthread.h>
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&_lock, nullptr);
}
void Lock()
{
pthread_mutex_lock(&_lock);
}
void Unlock()
{
pthread_mutex_unlock(&_lock);
}
pthread_mutex_t *Get()
{
return &_lock;
}
~Mutex()
{
pthread_mutex_destroy(&_lock);
}
private:
pthread_mutex_t _lock;
};
//LockGuard 是一个类 需要将锁传进来,对锁进行了包装
class LockGuard
{
public:
// 传进来一个外部锁的地址
LockGuard(Mutex *_mutex):_mutexp(_mutex)
{
_mutex->Lock();
}
~LockGuard()
{
_mutexp->Unlock();
}
private:
Mutex *_mutexp;
};
cpp
//Cond.hpp
#pragma once
#include <iostream>
#include <pthread.h>
#include "Mutex.hpp"
class Cond
{
public:
Cond()
{
pthread_cond_init(&_cond, nullptr); //局部性的
}
void Wait(Mutex &lock)
{
int n = pthread_cond_wait(&_cond,lock.Get());
}
void NotifyOne() //唤醒/通知一个线程
{
int n = pthread_cond_signal(&_cond); // pthread_大部分开头的返回值=0 成功
(void)n;
}
void NotifyAll() //唤醒/通知所有线程
{
int n = pthread_cond_broadcast(&_cond);
(void)n;
}
~Cond()
{
pthread_cond_destroy(&_cond);
}
private:
pthread_cond_t _cond; //条件变量对象,局部性的
};
cpp
//Thread.hpp
#ifndef __THREAD_HPP
#define __THREAD_HPP
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
#include <functional>
#include <sys/syscall.h>
#include "Logger.hpp"
#define get_lwp_id() syscall(SYS_gettid)
using func_t = std::function<void()>;
const std::string threadnamedefault = "None-name";
class Thread
{
public:
Thread(func_t func, const std::string &name = threadnamedefault) : _name(name), _func(func), _isrunning(false)
{
LOG(LogLevel::INFO) << _name << "create obj thread success";
// std::cout << "create obj thread success" << std::endl;
}
static void *start_routine(void *args)
{
Thread *self = static_cast<Thread *>(args);
self->_isrunning = true;
self->_lwpid = get_lwp_id();
self->_func();
pthread_exit((void *)0);
}
void Start()
{
int n = pthread_create(&_tid, nullptr, start_routine, this);
if (n == 0)
{
LOG(LogLevel::INFO) << _name << "running sucess";
// std::cout << "run thread success" << std::endl;
}
}
void Stop()
{
int n = pthread_cancel(_tid);
(void)n;
}
// Join() --- 检测线程结束并且回收的功能
void Join()
{
if (!_isrunning)
return;
int n = pthread_join(_tid, nullptr);
if (n == 0)
{
LOG(LogLevel::INFO) << _name << "pthread_join success";
// std::cout << "pthread_join success" << std::endl;
}
}
~Thread()
{
// LOG(LogLevel::INFO) << _name << "destroy thread obj success";
}
private:
bool _isrunning;
pthread_t _tid;
pid_t _lwpid;
std::string _name;
func_t _func;
};
#endif
cpp
//logger.hpp
#pragma once
#include <iostream>
#include <string>
#include "Mutex.hpp"
#include <filesystem> // C+++17 文件操作
#include <fstream>
#include <ctime>
#include <sys/types.h>
#include <unistd.h>
#include <sstream>
// 规定出常见的日志等级 -- 枚举,枚举出来的就是一个整数
enum class LogLevel
{
DEBUG, // 0
INFO, // 1
WARNING, // 2
ERROR, // 3
FATAL // 4
};
std::string Level2String(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";
}
}
// 2026-06-06 06:06:06
std::string GetCurrentTime()
{
// 1. 获取时间戳
time_t currentime = time(nullptr);
// 2.如何将时间戳转换成为年月日时分秒的格式
struct tm currt_tm;
localtime_r(¤time, &currt_tm);
// 3. 将年月日时分秒转换成字符串
char timebuffer[64];
snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d",
currt_tm.tm_year + 1900,
currt_tm.tm_mon + 1,
currt_tm.tm_mday,
currt_tm.tm_hour,
currt_tm.tm_min,
currt_tm.tm_sec);
return timebuffer;
}
////////////////////////////////////////////////////////////////////////////////////
// 1. 日志刷新的问题 -- 假设我们已经有了一条完整的日志,string ->设备(显示器,文件)
// 基类方法
class LogStrategy
{
public:
virtual ~LogStrategy() = default;
virtual void SyncLog(const std::string &logmessage) = 0;
};
// 显示器刷新
class ConsoleLogStrategy : public LogStrategy
{
public:
~ConsoleLogStrategy()
{
}
void SyncLog(const std::string &logmessage) override
{
// 日志可被任何线程同时访问,显示器就是临界资源 -- 加锁,进行安全打印
LockGuard lockguard(&_lock);
std::cout << logmessage << std::endl;
}
private:
Mutex _lock;
};
const std::string logdefaultdir = "log";
const static std::string logfilename = "test.log";
// 文件刷新 在文件中追加式的写入
class FileLogStrategy : public LogStrategy
{
public:
FileLogStrategy(const std::string &dir = logdefaultdir,
const std::string filename = logfilename)
: _dir_path_name(dir), _filename(filename)
{
LockGuard lockguard(&_lock);
// 目录不存在,新建;存在,构建过程就算完成
if (std::filesystem::exists(_dir_path_name))
{
return;
}
try
{
std::filesystem::create_directories(_dir_path_name);
}
catch (const std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << "\r\n";
}
}
void SyncLog(const std::string &logmessage) override
{
{
LockGuard lockguard(&_lock);
std::string target = _dir_path_name;
target += "/";
target += _filename;
std::ofstream out(target.c_str(), std::ios::app); // app -- append
if (!out.is_open()) // 打开失败
{
return;
}
out << logmessage << "\n"; // 等价于 out.wirte
out.close();
}
}
~FileLogStrategy()
{
}
private:
std::string _dir_path_name; // 目录路径的名字
std::string _filename; // 形成日志文件的文件名
Mutex _lock;
};
// 网络刷新
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// logger -- 1. 要有刷新策略 2. 要有构建一条完整日志的能力
class Logger
{
public:
Logger()
{
}
void EnableConsoleLogStrategy()
{
_strategy = std::make_unique<ConsoleLogStrategy>();
}
void EnableFileLogStrategy()
{
_strategy = std::make_unique<FileLogStrategy>();
}
// 形成一条完整日志的方式 -- 内部类
class LogMessage // 代表一条具体的message
{
public:
LogMessage(LogLevel level,std::string &filename, int line,Logger &logger)
:_curr_time(GetCurrentTime()),
_level(level),
_pid(getpid()),
_filename(filename),
_line(line),
_logger(logger)
{
std::stringstream ss;
ss << "[" << _curr_time << "] "
<< "[" << Level2String(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _filename << "] "
<< "[" << _line << "]"
<< " - ";
_loginfo = ss.str();
}
template<typename T>
LogMessage& operator << (const T&info)
{
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this;
}
~LogMessage()
{
if(_logger._strategy)
{
_logger._strategy->SyncLog(_loginfo);
}
}
private:
std::string _curr_time; // 日志时间
LogLevel _level; // 日志等级
pid_t _pid; // 进程pid
std::string _filename;
int _line; // 行号
std::string _loginfo; //一条合并完成的,完整的日志信息
Logger &_logger; //内部类使用外部logger类进行刷新,提供刷新策略的具体做法
};
LogMessage operator()(LogLevel level,std::string filename, int line)
{
return LogMessage(level,filename,line,*this);
}
~Logger()
{
}
private:
std::unique_ptr<LogStrategy> _strategy;
};
Logger logger;
#define LOG(level) logger(level,__FILE__,__LINE__)
#define EnableConsoleLogStrategy() logger.EnableConsoleLogStrategy()
#define EnableFileLogStrategy() logger.EnableFileLogStrategy()
cpp
//ThreadPool.hpp
#pragma once
#include <iostream>
#include <queue>
#include <vector>
#include <unistd.h>
#include "Thread.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"
const static int defaultthreadnum = 3;
template <class T>
class ThreadPool
{
private:
void hello()
{
// for test
while (true)
{
LOG(LogLevel::INFO) << "hello thread";
// std::cout << "hello thread" << std::endl;
sleep(1);
}
}
public:
ThreadPool(int threadnum = defaultthreadnum)
: _threadnum(threadnum), _is_running(false)
{
// 构造函数中一旦进入{}中,当前对象已经存在,就可访问类类属性和类类方法了
// 1. 创建线程对象
for (int i = 0; i < _threadnum; i++)
{
// 方法1:
// auto f = std::bind(hello, this);
// 方法2:
std::string name = "thread-" + std::to_string(i + 1);
_threads.emplace_back([this]()
{ this->hello(); }, name);
// Thread t([this](){
// this->hello();
// },name);
// _threads.push_back(std::move(t));
}
LOG(LogLevel::INFO) << " thread pool obj create success";
}
void Start()
{
if (_is_running)
return;
_is_running = true;
for (auto &t : _threads)
{
t.Start();
}
LOG(LogLevel::INFO) << " thread pool running success";
}
void Stop()
{
if (!_is_running) // 没启动
return;
_is_running = false;
for (auto &t : _threads)
{
t.Stop();
}
LOG(LogLevel::INFO) << " thread pool stop success";
}
void Wait()
{
for (auto &t : _threads)
{
t.Join();
}
LOG(LogLevel::INFO) << " thread pool wait success";
}
void Enqueue(const T &t)
{
}
~ThreadPool()
{
}
private:
// 任务队列
std::queue<T> _q; // 整体使用的临界资源
// 多个线程
std::vector<Thread> _threads; // 1. 创建线程对象 2.让线程对象启动
int _threadnum;
// 保护机制
Mutex _lock;
Cond _cond; // 一个条件变量,方便内部的线程池去等
// 其他属性
bool _is_running; // 表示当前线程池是否被启动
};
cpp
//main.cc
#include "ThreadPool.hpp"
#include <memory>
int main()
{
EnableConsoleLogStrategy();
std::unique_ptr<ThreadPool<int>> tp = std:: make_unique<ThreadPool<int>>(5);
tp->Start();
sleep(5);
tp->Stop();
tp->Wait();
// tp->Enqueue();
return 0;
}
运行结果:


4.2继续改进线程池:
cpp
//Thread.hpp
#ifndef __THREAD_HPP
#define __THREAD_HPP
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
#include <functional>
#include <sys/syscall.h>
#include "Logger.hpp"
#define get_lwp_id() syscall(SYS_gettid)
using func_t = std::function<void(const std::string &name)>;
const std::string threadnamedefault = "None-name";
class Thread
{
public:
Thread(func_t func, const std::string &name = threadnamedefault) : _name(name), _func(func), _isrunning(false)
{
LOG(LogLevel::INFO) << _name << " create obj thread success";
// std::cout << "create obj thread success" << std::endl;
}
static void *start_routine(void *args)
{
Thread *self = static_cast<Thread *>(args);
self->_isrunning = true;
self->_lwpid = get_lwp_id();
self->_func(self->_name);
pthread_exit((void *)0);
}
void Start()
{
int n = pthread_create(&_tid, nullptr, start_routine, this);
if (n == 0)
{
LOG(LogLevel::INFO) << _name << " running sucess";
// std::cout << "run thread success" << std::endl;
}
}
void Stop()
{
int n = pthread_cancel(_tid); // 太简单粗暴了
(void)n;
}
// Join() --- 检测线程结束并且回收的功能
void Join()
{
if (!_isrunning)
return;
int n = pthread_join(_tid, nullptr);
if (n == 0)
{
LOG(LogLevel::INFO) << _name << " pthread_join success";
// std::cout << "pthread_join success" << std::endl;
}
}
~Thread()
{
// LOG(LogLevel::INFO) << _name << "destroy thread obj success";
}
private:
bool _isrunning;
pthread_t _tid;
pid_t _lwpid;
std::string _name;
func_t _func;
};
#endif
cpp
//Task.hpp
#pragma once
#include <iostream>
#include <unistd.h>
#include <functional>
#include <sstream>
class Task
{
public:
Task()
{
}
Task(int x, int y) : a(x), b(y)
{
}
void Execute()
{
result = a + b;
}
void operator()()
{
// sleep(1);
Execute();
}
void Print()
{
std::cout << a << " + " << b << " = " << result << std::endl;
}
std::string Result2String()
{
std::stringstream ss;
ss << a << " + " << b << " = " << result;
return ss.str();
}
private:
int a;
int b;
int result;
};
//任务中也是可以放函数对象的 --- 将方法直接交给线程
// using func_t = std::function<void()>;
// void PrintLog()
// {
// std::cout << "我是一个日志任务" << std::endl;
// }
cpp
//ThreadPool.hpp
#pragma once
#include <iostream>
#include <queue>
#include <vector>
#include <unistd.h>
#include "Thread.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"
const static int defaultthreadnum = 3;
template <class T>
class ThreadPool
{
private:
bool QueueIsEmpty()
{
return _q.empty();
}
void Routine(const std::string &name)
{
// for test
while (true)
{
// 把任务从线程获取到线程私有!临界区 -> 私有的栈
T t;
{
LockGuard Lockguard(&_lock);
while (QueueIsEmpty() && _is_running)
{
_wait_thread_num++;
_cond.Wait(_lock);
_wait_thread_num--;
}
if (!_is_running && QueueIsEmpty())
{
LOG(LogLevel::INFO) << " 线程退出 && 任务队列为空," << name << "退出";
break;
}
//
// 队列中一定有任务了!!但是
// 1. 线程池退出 -- 消耗历史任务
// 2. 线程池没有退出 -- 正常处理任务
t = _q.front();
_q.pop();
}
t(); // 规定未来的任务,必须这样处理!处理任务是不需要在临界区内部进行!!!
LOG(LogLevel::DEBUG) << name << " handler task: " << t.Result2String();
}
}
public:
ThreadPool(int threadnum = defaultthreadnum)
: _threadnum(threadnum), _is_running(false), _wait_thread_num(0)
{
// 构造函数中一旦进入{}中,当前对象已经存在,就可访问类类属性和类类方法了
// 1. 创建线程对象
for (int i = 0; i < _threadnum; i++)
{
// 方法1:
// auto f = std::bind(hello, this);
// 方法2:
std::string name = "thread-" + std::to_string(i + 1);
_threads.emplace_back([this](const std::string &name)
{ this->Routine(name); }, name);
// Thread t([this](){
// this->hello();
// },name);
// _threads.push_back(std::move(t));
}
LOG(LogLevel::INFO) << " thread pool obj create success";
}
void Start()
{
if (_is_running)
return;
_is_running = true;
for (auto &t : _threads)
{
t.Start();
}
LOG(LogLevel::INFO) << " thread pool running success";
}
// 核心思想:我们应该让线程走正常的唤醒逻辑退出
// 线程池要退出
// 1. 如果被唤醒 && 任务队列没有任务 = 让线程退出
// 2. 如果被唤醒 && 任务队列有任务 = 线程不能立即退出,而是应该让线程将任务处理完,再退出
// 3. 线程本身没有被休眠,我们应该让他把他能处理的任务全部处理完毕,再退出
// 3 || 2 -> 1
// 注意: 如果任务队列中有任务,线程是不会休眠的!!!
void Stop()
{
if (!_is_running) // 任务队列为空
return;
_is_running = false;
if (_wait_thread_num)
_cond.NotifyAll();
// 在这种做法不推荐
// if (!_is_running) // 没启动
// return;
// _is_running = false;
// for (auto &t : _threads)
// {
// t.Stop();
// }
// LOG(LogLevel::INFO) << " thread pool stop success";
}
void Wait()
{
for (auto &t : _threads)
{
t.Join();
}
LOG(LogLevel::INFO) << " thread pool wait success";
}
void Enqueue(const T &t)
{
if (!_is_running)
return;
{
LockGuard lockguard(&_lock);
// 保证线程池是运行状态才push
_q.push(t);
if (_wait_thread_num > 0)
_cond.NotifyOne();
}
}
~ThreadPool()
{
}
private:
// 任务队列
std::queue<T> _q; // 整体使用的临界资源
// 多个线程
std::vector<Thread> _threads; // 1. 创建线程对象 2.让线程对象启动
int _threadnum;
int _wait_thread_num;
// 保护机制
Mutex _lock;
Cond _cond; // 一个条件变量,方便内部的线程池去等
// 其他属性
bool _is_running; // 表示当前线程池是否被启动
};
cpp
// main.cc
#include "Task.hpp"
#include "ThreadPool.hpp"
#include <memory>
#include <time.h>
int main()
{
srand(time(nullptr) ^ getpid());
EnableConsoleLogStrategy();
std::unique_ptr<ThreadPool<Task>> tp = std::make_unique<ThreadPool<Task>>(5);
tp->Start();
// sleep(5);
int cnt = 10;
while (cnt--)
{
// 生产任务
int x = rand() % 10 + 1;
usleep(rand() % 73);
int y = rand() % 5 + 1;
Task t(x, y);
// Push到线程池中,处理
tp->Enqueue(t);
sleep(1);
}
tp->Stop();
tp->Wait();
// tp->Enqueue();
return 0;
}
运行结果:

4.3 线程安全的单例模式
cpp
std::unique_ptr<ThreadPool<Task>> tp = std::make_unique<ThreadPool<Task>>(5);
std::unique_ptr<ThreadPool<Task>> tp2 = std::make_unique<ThreadPool<Task>>(5);
std::unique_ptr<ThreadPool<Task>> tp3 = std::make_unique<ThreadPool<Task>>(5);
以上代码就相当于,我们创建了三个独立的线程池,每个都管理着5个工作线程。表面上看起来没问题,但让我们思考一下:如果一个模块用 tp 提交任务,另一个模块用 tp2 提交任务,那么整个系统实际上有多少个线程在运行?答案是15个。但你的配置文件中可能只期望5个线程。所以就得将线程池设计成单例模式!!!
4.3.1 单例模式的特点
- 要让我们的类,一次只能创建一个对象,就称之为单例。
- 为了防止多线程滥用,线程池只允许在整个工程里面被创建一份,以单例的方式访问即可。
- 在很多的服务器开发场景中,经常需要让服务器加载很多的数据(上百G)到内存中,此时往往要用一个单例的类管理这些数据。此时这种场景就很适合单例。
4.3.2 单例的两种实现方法
1. 饿汉方式实现单例模式:
吃完饭,立即去洗碗。这种就是饿汉方式,因为下一顿吃的时候可以立刻拿着碗就能吃饭了。
2. 懒汉方式实现单例模式:
吃完饭,先把碗放下,然后等到下一顿反用到这个碗的时候再去洗碗,这就是懒汉方式。
懒汉方式最核心的思想就是 "延时加载" , 从而能够优化服务器(程序)的启动速度。
4.3.2.1 饿汉方式实现单例模式
template <typename T>
class Singleton // 定义一个类,T就是将来要创建的对象
{
static T data; // 静态变量将来会编译到进程地址空间的全局区域
// 语言角度上,可做创建类和类加载,在系统角度,创建类和类加载又是什么意思
// 虽然是个类,但是依旧是static,static T data一旦被定义就在全局数据区进行开辟了
// 所以在函数内部,定义static变量,作用域属于该函数,生命周期变为全局变量了
// 所以 static T data; 这个变量会在进程运行时,在逻辑上,在虚拟地址空间上认为该对象已经被加载了
// 因为是静态变量,是全局的,在创建进程的时候加载到内存时就已经存在了
// 我们还没使用全局变量,但是就已经将变量空间申请了,在虚拟地址空间上有 --- 没有吃下一顿饭就将碗洗了 --> 饿汉模式
public:
static T *GetInstance() // 获取类类属性
{
return &data;
}
};
进程加载时,类对象直接被创建,其实就是全局变量
但是为什么说是单例的呢?
- 因为全局变量的变量名不能重复。
- 只要是单例,对象的构造函数,拷贝构造等等,去掉或者时设置为私有的,不要让它创建对象。它可以将拷贝构造函数设置为私有的,这样外部就调用不了,将赋值语句,拷贝构造函数全部都删掉,外部是访问不了构造函数的,所以这个对象就创建不出来。
4.3.2.2 懒汉方式实现单例模式
template <typename T>
class Singleton
{
static T *inst; // 静态的指针
// 对象先不进行创建,先创建静态指针
public:
// 获取单例时,才会new出来对应的对象,延迟创建对应的对象 --- 懒汉方式
static T *GetInstance()
{
if (inst == NULL)
{
inst = new T();
}
return inst;
}
};
懒汉方式最核心的思想就是 "延时加载" , 从而能够优化服务器(程序)的启动速度。
如果需要让服务器加载很多的数据(上百G)到内存中,如果采用饿汉的方式,进程在启动的时候,要在虚拟地址空间上开辟出来类对象,同时还要将管理起来的数据,再加载进程的同时,也要将数据加载到内存,程序的启动速度会变得比较慢。
采用懒汉方式,只需要创建一个指针,所以程序的启动速度比较快,用对象的时候才会被创建,从而能够优化服务器(程序)的启动速度。
4.4 将上面写的线程池改成单例的
如何保证一个类只创建一个对象,将构造函数私有化,形成单例,并不是不让这个类不创建对象,而是只创建一个对象,所以单例所对应的类必须要有构造函数。
4.4.1 简单的单例模式线程的代码:
cpp
//ThreadPool.hpp
#pragma once
#include <iostream>
#include <queue>
#include <vector>
#include <unistd.h>
#include "Thread.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"
// 单例线程池 -- 懒汉模式 -- 推荐
const static int defaultthreadnum = 3;
template <class T>
class ThreadPool
{
private:
bool QueueIsEmpty()
{
return _q.empty();
}
void Routine(const std::string &name)
{
// for test
while (true)
{
// 把任务从线程获取到线程私有!临界区 -> 私有的栈
T t;
{
LockGuard Lockguard(&_lock);
while (QueueIsEmpty() && _is_running)
{
_wait_thread_num++;
_cond.Wait(_lock);
_wait_thread_num--;
}
if (!_is_running && QueueIsEmpty())
{
LOG(LogLevel::INFO) << " 线程退出 && 任务队列为空," << name << "退出";
break;
}
//
// 队列中一定有任务了!!但是
// 1. 线程池退出 -- 消耗历史任务
// 2. 线程池没有退出 -- 正常处理任务
t = _q.front();
_q.pop();
}
t(); // 规定未来的任务,必须这样处理!处理任务是不需要在临界区内部进行!!!
LOG(LogLevel::DEBUG) << name << " handler task: " << t.Result2String();
}
}
ThreadPool(int threadnum = defaultthreadnum)
: _threadnum(threadnum), _is_running(false), _wait_thread_num(0)
{
// 构造函数中一旦进入{}中,当前对象已经存在,就可访问类类属性和类类方法了
// 1. 创建线程对象
for (int i = 0; i < _threadnum; i++)
{
// 方法1:
// auto f = std::bind(hello, this);
// 方法2:
std::string name = "thread-" + std::to_string(i + 1);
_threads.emplace_back([this](const std::string &name)
{ this->Routine(name); }, name);
// Thread t([this](){
// this->hello();
// },name);
// _threads.push_back(std::move(t));
}
LOG(LogLevel::INFO) << " thread pool obj create success";
}
ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;
ThreadPool(const ThreadPool<T> &) = delete;
public:
void Start()
{
if (_is_running)
return;
_is_running = true;
for (auto &t : _threads)
{
t.Start();
}
LOG(LogLevel::INFO) << " thread pool running success";
}
// 核心思想:我们应该让线程走正常的唤醒逻辑退出
// 线程池要退出
// 1. 如果被唤醒 && 任务队列没有任务 = 让线程退出
// 2. 如果被唤醒 && 任务队列有任务 = 线程不能立即退出,而是应该让线程将任务处理完,再退出
// 3. 线程本身没有被休眠,我们应该让他把他能处理的任务全部处理完毕,再退出
// 3 || 2 -> 1
// 注意: 如果任务队列中有任务,线程是不会休眠的!!!
void Stop()
{
if (!_is_running) // 任务队列为空
return;
_is_running = false;
if (_wait_thread_num)
_cond.NotifyAll();
// 在这种做法不推荐
// if (!_is_running) // 没启动
// return;
// _is_running = false;
// for (auto &t : _threads)
// {
// t.Stop();
// }
// LOG(LogLevel::INFO) << " thread pool stop success";
}
void Wait()
{
for (auto &t : _threads)
{
t.Join();
}
LOG(LogLevel::INFO) << " thread pool wait success";
}
void Enqueue(const T &t)
{
if (!_is_running)
return;
{
LockGuard lockguard(&_lock);
// 保证线程池是运行状态才push
_q.push(t);
if (_wait_thread_num > 0)
_cond.NotifyOne();
}
}
static std::string ToHex(ThreadPool<T> *addr)
{
char buffer[64];
snprintf(buffer, sizeof(buffer), "%p", addr);
return buffer;
}
// 方法 -- 在首次的时候创建对象,获取单例
// 类的成员方法是可以访问类类的静态属性 --- 这个函数是可以直接访问instance指针的
// 但是静态方法是不能访问类类的常规属性和方法的!!!
// 但是成员方法怎样才能访问?在外部有对象才能调用该成员方法(GetInstance()),以对象的方式 -- 所以必须创建对象
// 但是今天不可能在类外创建出不对象 -- 构造函数也被私有,这就是为什么获取单例要通过方法来获取,因为方法内部要对是否为单例做判断
// 所以也得将该方法变为static,所以要求_instance也是静态的,因为静态方法只能访问静态属性
// 静态方法虽然是不能够直接访问类类的直接属性/方法,但是只要该方法本身在类内,就可以直接调用类内的方法
static ThreadPool<T>* GetInstance()
{
if(!_instance) //为空,创建线程池对象
{
_instance = new ThreadPool<T>();
LOG(LogLevel::DEBUG) << "线程池单例首次被使用,创建并初始化, addr: " << ToHex(_instance);
_instance->Start();
}
else
{
LOG(LogLevel::DEBUG) << "线程池单例已经存在,直接获取, addr: "<< ToHex(_instance);
}
return _instance;
}
~ThreadPool()
{
}
private:
// 任务队列
std::queue<T> _q; // 整体使用的临界资源
// 多个线程
std::vector<Thread> _threads; // 1. 创建线程对象 2.让线程对象启动
int _threadnum;
int _wait_thread_num;
// 保护机制
Mutex _lock;
Cond _cond; // 一个条件变量,方便内部的线程池去等
// 其他属性
bool _is_running; // 表示当前线程池是否被启动
// 单例中静态指针
static ThreadPool<T> *_instance; // 静态指针获取类对象的,因为static静态方法属于类,不属于对象,通过类名访问指针,
//但是不能直接访问指针,要通过包装好的函数,让上层通过函数来访问
};
//静态的,对该静态指针,要在类外进行初始化
template<class T>
ThreadPool<T>* ThreadPool<T>::_instance = nullptr;
cpp
//main.cc
#include "Task.hpp"
#include "ThreadPool.hpp"
#include <memory>
#include <time.h>
int main()
{
srand(time(nullptr) ^ getpid());
EnableConsoleLogStrategy();
// 不允许在外部直接创建对象 --- 是实现单例的第一步
// std::unique_ptr<ThreadPool<Task>> tp = std::make_unique<ThreadPool<Task>>(5);
// tp->Start();
int cnt = 10;
while (cnt--)
{
// 生产任务
int x = rand() % 10 + 1;
usleep(rand() % 73);
int y = rand() % 5 + 1;
Task t(x, y);
// Push到线程池中,处理
ThreadPool<Task>::GetInstance()->Enqueue(t);
sleep(1);
}
ThreadPool<Task>::GetInstance()->Stop();
ThreadPool<Task>::GetInstance()->Wait();
// tp->Enqueue();
return 0;
}
运行结果:

4.4.2 升级的单例模式线程的代码:
如果线程池本身被多线程访问呢?
cpp
//ThreadPool.hpp
static ThreadPool<T> *GetInstance()
{
// 多线程进入,导致对象被创建多份的情况 --- 获取单例原子化
{
// 线程安全,提高效率式的获取单例
if (!_instance) //在保证安全的情况下,提高获取单例的效率
{
LockGuard lockguard(&_singleton_lock);
if (!_instance) // 为空,创建线程池对象
{
_instance = new ThreadPool<T>();
LOG(LogLevel::DEBUG) << "线程池单例首次被使用,创建并初始化, addr: " << ToHex(_instance);
_instance->Start();
}
}
}
// else
// {
// LOG(LogLevel::DEBUG) << "线程池单例已经存在,直接获取, addr: "<< ToHex(_instance);
// }
return _instance;
}
运行结果:

5. 线程安全和重入问题
概念
**线程安全:**就是多个线程在访问共享资源时,能够正确地执行,不会相互干扰或者破坏彼此的执行结果,这样情况被称之为线程安全。一般而言,多个线程并发同一段只有局部变量的代码时,不会出现不同的结果。但是对全局变量或者静态变量进行操作,并且没有锁保护的情况下,容易出现该问题。
**重入:**同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其它的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
- 重入描述的是函数的状态和特性,一个函数被多个执行流同时执行,被称为函数被重入了,不出错就被称为该函数是可重入的,出错了该函数不可被重入。
- 线程安全和重入是描述并发问题的两个视角,线程安全是站在线程的视角,重入是站在函数的视角,是两个不同的视角。
重入可被分为两种情况:
- 多线程重入函数
- 信号导致一个执行流重复进入函数(不考虑)
常见的线程不安全的情况
- 不保护共享变量的函数
- 函数状态随着被调用,状态发生变化的函数
- 返回指向静态变量指针的函数
- 调用线程不安全函数的函数
常见的线程安全的情况
- 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
- 类或者接口对于线程来说都是原子操作
- 多个线程之间的切换不会导致该接口的执行结果存在二义性
常见不可重入的情况
- 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
- 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
- 可重入函数体内是用来静态的数据结构
常见可重入的情况
- 不使用全局变量或静态变量
- 不使用用malloc或者new开辟出的空间
- 不调用不可重入函数
- 不返回静态或全局数据,所有数据都有函数的调用者提供
- 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
线程安全和函数重入,线程是因,不安全和不可重入是果,线程一个因产生了两个果,线程安不安全一定是调用了某种函数导致的,线程在执行时只会调用函数!!!研究线程安不安全其实就是在研究这个函数重入或者不可重入。
结论
可重入与线程安全联系
- 函数是可重入的,那就是线程安全的(重点,知道这句话就够了)
- 函数是不可重入的,那就不能由多个线程使⽤,有可能引发线程安全问题
- 如果⼀个函数中有全局变量,那么这个函数既不是线程安全也不是可重⼊的。
可重入与线程安全区别
- 可重入函数是线程安全函数的⼀种
- 线程安全不⼀定是可重入的,而可重入函数则⼀定是线程安全的。
- **如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。**一个执行流是不能将锁申请两次的,一把锁也是可以产生死锁的。
- 信号场景当中,线程安全不⼀定是可重入的;在多线程领域线程安全就是要求该函数就是可重入的
注意:
- 如果不考虑 信号导致⼀个执行流重复进入函数 这种重入情况,线程安全和重入在安全角度不做区分
- 但是线程安全侧重说明线程访问公共资源的安全情况,表现的是并发线程的特点
- 可重入描述的是⼀个函数是否能被重复进⼊,表示的是函数的特点
6. 常见锁概念
6.1 死锁
- 死锁是指在⼀组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的⼀种永久等待状态。一把锁也是会产生死锁的。
- 为了方便表述,假设现在线程A,线程B必须同时持有锁1和锁2,才能进行后续资源的访问。

申请一把锁是原子的,但是申请两把锁就不一定了

线程A申请完锁1,紧接着就要去申请锁2,锁1刚被申请成功,就被切换,线程B就申请并持有锁2,再去申请锁1,就会被挂起,此时将线程A给唤醒,再去申请锁2,锁2已经被线程B给拿走了。将对方线程挂起在自己的锁上,没有人来释放锁1锁2的话,双方之间就不可能得到自己想要的第二把锁,进而就会导致死锁问题。
造成的结果:

6.2 死锁的四个必要条件
- **互斥条件:**一个资源每次只能被一个执行流使用
- 解决死锁问题:破坏死锁的必要条件
- 破坏互斥:不要锁
- **请求与保持条件:**一个执行流因请求资源而阻塞是,对已获得的资源保持不放
请求:线程A给线程B说,将你的锁给我。申请对方的锁。
保持:保持着自己的锁不给对方。不释放自己的锁。

- 解决死锁问题:破坏死锁的必要条件
- 破坏请求与保持:只需要破坏保持就可以,如果发现申请不到,将自己的锁释放掉,重新在申请就可以了
- 如何将曾经申请的锁释放掉:
- int pthread_mutex_trylock(pthread_mutex_t *mutex);
- 申请锁成功,返回值为0,继续往后走;申请锁失败,不会阻塞,也会立即返回,返回值表示出错码。trylock申请第一把锁成功,trylock申请第二把锁失败,会将之前申请了的锁释放掉,此时就可以解决保持的这一个条件了
- int pthread_mutex_trylock() 和 int pthread_mutex_lock()释放锁用的都是 int pthread_mutex_unlock()
- **不剥夺条件:**一个执行流已获得的资源,在未使用完之前,不能强行剥夺

- 解决死锁问题:破坏死锁的必要条件
- 不保持和剥夺导致的结果都是一样的:都会让对方把锁给我,释放到系统当中,但是核心上是不一样的,保持和不保持是自己保持或者是不保持,剥夺和不剥夺是对于对方的,抢对方的。
- 破坏不剥夺条件:线程A申请线程B的锁2不成功,线程A强行将线程B的锁2释放掉,自己将锁2抢过来,此时就是剥夺
- **循环等待条件:**若干执行流之间形成一种头尾相接的循环等待资源的关系

- 解决死锁问题:破坏死锁的必要条件
- 破坏循环等待条件:同时存在两把锁,线程A和线程B同时申请同一把锁,申请锁的顺序保持一致,就不会出现循环等待的问题了,尽量保持加锁的顺序是一致的


6.3 避免死锁
- 破坏死锁的四个必要条件
- 破坏循环等待条件问题:资源一次性分配,使用超时机制,加锁顺序一致
cpp
// 下面的理解就可以
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
#include <unistd.h>
// 定义两个共享资源(整数变量)和两个互斥锁
int shared_resource1 = 0;
int shared_resource2 = 0;
std::mutex mtx1, mtx2;
// 一个函数,同时访问两个共享资源
void access_shared_resources()
{
// 复现错误场景
// std::unique_lock<std::mutex> lock1(mtx1, std::defer_lock);
// std::unique_lock<std::mutex> lock2(mtx2, std::defer_lock);
// 解决方案
// // 使用 std::lock 同时锁定两个互斥锁,不会出现循环等待的问题
// std::lock(lock1, lock2); //预防循环等待问题
// 现在两个互斥锁都已锁定,可以安全地访问共享资源
int cnt = 10000;
while (cnt)
{
++shared_resource1;
++shared_resource2;
cnt--;
}
// 当离开 access_shared_resources 的作用域时,lock1 和 lock2 的析构函数会被自动调用
// 这会导致它们各⾃的互斥量被⾃动解锁
}
// 为了说明问题方便
void access_shared_resources()
{
// 复现错误场景
// std::unique_lock<std::mutex> lock2(mtx2, std::defer_lock);
// std::unique_lock<std::mutex> lock1(mtx1, std::defer_lock);
// 解决方案
// // 使用 std::lock 同时锁定两个互斥锁,不会出现循环等待的问题
// std::lock(lock2, lock1);
// 现在两个互斥锁都已锁定,可以安全地访问共享资源
int cnt = 10000;
while (cnt)
{
++shared_resource1;
++shared_resource2;
cnt--;
}
// 当离开 access_shared_resources 的作用域时,lock1 和 lock2 的析构函数会被自动调用
// 这会导致它们各⾃的互斥量被⾃动解锁
}
// 模拟多线程同时访问共享资源的场景
void simulate_concurrent_access()
{
std::vector<std::thread> threads;
// 创建多个线程来模拟并发访问
for (int i = 0; i < 10; ++i)
{
threads.emplace_back(access_shared_resources);
}
// 等待所有线程完成
for (auto& thread : threads)
{
thread.join();
}
// 输出共享资源的最终状态
std::cout << "Shared Resource 1: " << shared_resource1 << std::endl;
std::cout << "Shared Resource 2: " << shared_resource2 << std::endl;
}
int main()
{
simulate_concurrent_access();
return 0;
}
$ . / a.out // 不一次申请
Shared Resource 1: 94416
Shared Resource 2 : 94536
$ . / a.out // 一次申请
Shared Resource 1 : 100000
Shared Resource 2 : 100000
7. STL,智能指针和线程安全
7.1 STL中的容器是否是线程安全的?
不是。
原因是, STL 的设计初衷是将性能挖掘到极致, 而⼀旦涉及到加锁保证线程安全, 会对性能造成巨大的影响。
而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶)。
因此 STL 默认不是线程安全. 如果需要在多线程环境下使⽤, 往往需要调用者自行保证线程安全。
7.2 智能指针(本身,不是智能指针指向的对象)是否是线程安全的?
对于 unique_ptr, 由于只是在当前代码块范围内⽣效, 因此不涉及线程安全问题。
对于 shared_ptr, 多个对象需要共⽤⼀个引用计数变量, 所以会存在线程安全问题.。但是标准库实现的时候考虑到了这个问题, 基于原⼦操作(CAS)的方式保证 shared_ptr 能够⾼效, 原⼦的操作引用计数。
8. 其它常见的各种锁(了解)
- 悲观锁:在每次取数据时,总是担⼼数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,⾏锁等),当其他线程想要访问数据时,被阻塞挂起。
- 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进⾏修改。主要采⽤两种⽅式:版本号机制和CAS操作。
- CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则⽤新值更新。若不等则失败,失败则重试,⼀般是⼀个⾃旋的过程,即不断重试。
- ⾃旋锁,读写锁......
**自旋锁:**和互斥锁的接口差不多
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
互斥锁是申请锁失败后,会阻塞。自旋锁一旦申请锁失败,立马再次申请该锁,申请没成功立马再申请锁。得不到你这个锁,线程不会被挂起,不会被阻塞,一直会重复的申请。自旋锁再应用场景上会稍微少一点。自旋锁在OS内核中用的挺多的。