目录
线程安全的单例模式
饿汉实现方式和懒汉实现方式
饿汉方式实现单例模式
懒汉方式实现单例模式
懒汉方式实现单例模式(线程安全版本)
普通版本的线程池
实现单例模式
线程池单例模式(线程安全版本)代码
STL、智能指针和线程安全
其他常见的各种锁
系统编程中的锁
自旋锁
读者写者问题
读写锁
伪代码
读写锁的加锁、解锁过程
读写锁接口
#问:什么是单例模式?
【C++】-- 特殊类设计_川入的博客-CSDN博客
单例模式相当于,一个类最终只能定义一个对象。单例模式常见的就是:
因为,凡是需要被设置成单例的,往往就是因为其本身不需要被频繁加载到内存。可能在系统里只要有一份就行了 / 可能其本身占的空间太大(将软件运行起来,加载的时候,其会创建一堆的全局变量、全局对象。并且如果我们在代码当中用同一个类,定义了多个对象,如果对象都是一样,就极有可能出现重复。这样是不合理的)。所以,通过单例,实现特定的类只能实现一个对象。
[洗完的例子]
- 吃完饭,立刻洗碗,这种就是饿汉方式。因为下一顿吃的时候可以立刻拿着碗就能吃饭。
- 吃完饭,先把碗放下,然后下一顿饭用到这个碗了再洗碗,就是懒汉方式。
懒汉方式最核心的思想是 "延时加载",从而能够优化服务器的启动速度。
template
class Singleton {
static T data;
public:
static T* GetInstance() {
return &data;
}
};
template
class Singleton {
static T* inst;
public:
static T* GetInstance() {
if (inst == NULL)
inst = new T();
return inst;
}
};
#:简单线程池的实现?
【Linux】-- 线程池_川入的博客-CSDN博客
log.hpp
#pragma once
#include
#include
// 日志是有日志级别的
#define DEBUG 0
#define NORMAL 1 // 正常
#define WARNING 2 // 警告 -- 没出错
#define ERROR 3 // 错误 -- 不影响后续执行(一个功能因为条件等,没有执行)
#define FATAL 4 // 致命 -- 代码无法继续向后执行
const char* gLevelMap[] = {
"DEBUG",
"NORMAL",
"WARNING",
"ERROR",
"FATAL",
};
#define LOGFILE "./threafpool.log"
// 完整的日志功能,至少:日志等级 时间 日志内容 支持用户自定义
void logMessage(int level, const char* format, ...)// level:日志等级; format, ...:用户传参、日志对应的信息等。
{
#ifndef DEBUG_SHOW
if(level == DEBUG) return;
#endif
char stdBuffer[1024]; //标准部分
time_t timestamp = time(nullptr);
//struct tm* localtime = localtime(×tamp); //- 详细时间输出操作
//localtime->tm_year;
snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", gLevelMap[level], timestamp);
char logBuffer[1024]; //自定义部分
va_list args;
va_start(args, format);
// 这个时候就有一个可变参数列表的起始地址
// 向屏幕中直接打印
// vprintf(format, args);
// 向缓冲区logBuffer中打印
vsnprintf(logBuffer, sizeof logBuffer, format, args);
va_end(args);
// 向屏幕
printf("%s%s\n", stdBuffer, logBuffer);
// 向文件
FILE *fp = fopen(LOGFILE, "a");
fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
fclose(fp);
}
Task.hpp
#pragma once
#include "log.hpp"
#include
#include
#include
typedef std::function func_t;
class Task
{
public:
Task(){}
Task(int x, int y, func_t func):_x(x), _y(y), _func(func)
{}
void operator()(std::string& name)
{
//std::cout << "线程" << name << "处理完成,结果是" << _x << "+" << _y << "=" << _func(_x, _y) << std::endl;
logMessage(WARNING, "%s处理完成: %d+%d=%d | %s | %d",
name.c_str(), _x, _y, _func(_x, _y), __FILE__, __LINE__); // __FILE__, __LINE__:预处理符。
}
public:
int _x;
int _y;
// int type;
func_t _func;
};
thread.hpp
#pragma once
#include
#include
#include
// 对线程的封装 - 不是完全必要,但是这样便于后期的统一管理
typedef void*(*fun_t)(void*);
// 整合线程的数据
class ThreadData
{
public:
std::string _name;
void* _args;
};
class Thread
{
public:
Thread(int num, fun_t callback, void* args):_func(callback)
{
char nameBuffer[64];
snprintf(nameBuffer, sizeof(nameBuffer), "Thread-%d", num);
_name = nameBuffer;
_tdata._args = args;
_tdata._name = _name;
}
void start()
{
pthread_create(&_tid, nullptr, _func, (void*)&_tdata);
}
void join()
{
pthread_join(_tid, nullptr);
}
// 未来不再使用线程id了,因为其是一个地址不便于我们查看
std::string name()
{
return _name;
}
~Thread()
{}
private:
std::string _name;
fun_t _func;
ThreadData _tdata;
pthread_t _tid;
};
lockGuard.hpp
#pragma once
#include
#include
// RAII风格的加锁方式
class lockGuard
{
public:
lockGuard(pthread_mutex_t *mtx):mtx_(mtx)
{
pthread_mutex_lock(mtx_);
}
~lockGuard()
{
pthread_mutex_unlock(mtx_);
}
private:
pthread_mutex_t *mtx_;
};
threadPool.hpp
#include "thread.hpp"
#include "lockGuard.hpp"
#include "log.hpp"
#include
#include
#include
#include
const int g_thread_num = 3;
template
class ThreadPool
{
public:
// 返回锁的地址
pthread_mutex_t *getMutex()
{
return &_lock;
}
bool isEmpty()
{
return _task_queue.empty();
}
void waitCond()
{
pthread_cond_wait(&_cond, &_lock);
}
T getTask()
{
T t = _task_queue.front();
_task_queue.pop();
return t;
}
public:
ThreadPool(int thread_num = g_thread_num) : _num(thread_num)
{
for (int i = 1; i <= _num; i++)
{
_threads.push_back(new Thread(i, routine, this));
}
pthread_mutex_init(&_lock, nullptr);
pthread_cond_init(&_cond, nullptr);
}
// 1.run - 将线程跑起来
void run()
{
for (auto &iter : _threads)
{
iter->start();
// std::cout << iter->name() << " 启动成功" << std::endl;
logMessage(NORMAL, "%s %s", iter->name().c_str(), "启动成功");
}
}
// 未来所有执行流所执行的方法 - 核心取任务、执行任务的逻辑
static void *routine(void *args) // 因为在类当中,如果是一个成员方法,其会有一个隐藏的参数this指针,所以我们需要使用static进行修饰。
{
ThreadData *td = (ThreadData *)args;
ThreadPool *tp = (ThreadPool *)td->_args;
while (true)
{
T task; // 对应任务的对象
{
// lock
lockGuard lockguard(tp->getMutex());
// while(task_queue_.empty()) wait();
while (tp->isEmpty())
tp->waitCond();
// 获取任务 - 100%有任务
task = tp->getTask(); // 任务队列是共享的->将任务从共享,拿到自己的私有空间
} // 自动释放锁
// 处理任务
task(td->_name); // 要求每一个任务都要提供一个仿函数
}
}
// 2.pushTask - 将任务放到任务池里
void pushTask(const T &task)
{
lockGuard lockguard(&_lock); // 加锁
_task_queue.push(task); // 压入任务
pthread_cond_signal(&_cond); // 唤醒线程
} // 自动释放锁
// void join()
// {
// for (auto &iter : _threads)
// {
// iter->join();
// }
// }
~ThreadPool()
{
for (auto &iter : _threads)
{
iter->join();
delete iter;
}
pthread_mutex_destroy(&_lock);
pthread_cond_destroy(&_cond);
}
private:
std::vector _threads;
int _num;
std::queue _task_queue;
pthread_mutex_t _lock;
pthread_cond_t _cond;
};
testMain.cc
#include "threadPool.hpp"
#include "Task.hpp"
#include "log.hpp"
#include
#include
int main()
{
logMessage(NORMAL, "%s %d %c %f\n", "这是一条日志信息", 1234, 'a', 3.14);
srand((unsigned long)time(nullptr) ^ getpid()); // 利用 ^ getpid()让数据更随机
ThreadPool *tp = new ThreadPool();
tp->run();
while (true)
{
// 生产的过程(制作任务的时候,要花时间)
int x = rand() % 1000;
usleep(5000); // 模拟制作任务的时候,花费的时间
int y = rand() % 1000;
Task t(x, y, [](int x, int y)->int{
return x + y;
});
// std::cout << "制作任务完成" << std::endl;
logMessage(DEBUG, "制作任务完成: %d+%d=?", x, y);
// 推送任务到线程池中
tp->pushTask(t);
sleep(1); // 防止刷新过快
}
return 0;
}
增加成员:
// 通过定义一个静态的指针
static ThreadPool* _thread_ptr;
初始化:
// 在类外来对静态成员进行初始化
template
ThreadPool* ThreadPool::_thread_ptr = nullptr;
想要实现单例模式的线程池,首先必须在我们的类当中定义一个静态的线程池对象。之所以称之为单例,是因为其本身的构造、拷贝构造、赋值语句,基本上都是需要被去掉的。
自行实现一个手动构造的自定义函数。
Note:
单例只是将构造变为了私有的,必须要有构造,要不然就无法创建出单例。
于是对于调用的变化:
// 运行一个线程库
ThreadPool::getThreaedPool()->run();
// 推送任务到线程池中
ThreadPool::getThreaedPool()->pushTask(t);
这个时候,对于C++语言上所学习的单例模式的使用,看起来是没有问题的,但是:
#问:如果单例本身也在被多线程申请使用呢?
如果有多个线程去获取、使用这个线程池,我们就需要考虑多线程使用单例(调用getThreaedPool)的过程。
就有可能出现很多线程都进入判断。
就可能导致多个线程new出不同的对象,所以getThreaedPool函数在多线程下并不是线程安全的,所以我们需要加一把锁:
增加成员:
// 通过定义一个静态的互斥量
static pthread_mutex_t _mutex;
初始化:
// 在类外来对静态成员进行初始化
template
pthread_mutex_t ThreadPool::_mutex = PTHREAD_MUTEX_INITIALIZER;
这个时候就可以了,但是并不够好,因为:未来任何一个线程想获取单例,都必须调用getThreadPool接口。但是对于单例模式的线程安全问题只有在第一次的时候,一旦单例被初始化了_thread_ptr一定不为空。于是在后面不免会出现,线程跑过来,先加锁,然后什么都不做,然后再解锁。这不是闲的没事干是什么?导致会存在大量的申请和释放锁的行为,这个是无用且浪费资源,所以需要再调整。
通过添加if判断:可以有效减少未来必定要进行加锁检测的问题,拦截大量的在已经创建好单例的时候,剩余线程请求单例的而直接访问锁的行为。
log.hpp
#pragma once
#include
#include
// 日志是有日志级别的
#define DEBUG 0
#define NORMAL 1 // 正常
#define WARNING 2 // 警告 -- 没出错
#define ERROR 3 // 错误 -- 不影响后续执行(一个功能因为条件等,没有执行)
#define FATAL 4 // 致命 -- 代码无法继续向后执行
const char* gLevelMap[] = {
"DEBUG",
"NORMAL",
"WARNING",
"ERROR",
"FATAL",
};
#define LOGFILE "./threafpool.log"
// 完整的日志功能,至少:日志等级 时间 日志内容 支持用户自定义
void logMessage(int level, const char* format, ...)// level:日志等级; format, ...:用户传参、日志对应的信息等。
{
#ifndef DEBUG_SHOW
if(level == DEBUG) return;
#endif
// va_list ap; // va_list本质上就是char类型的指针
// va_start(ap, format); // 让指针指向栈帧对应的结构
// int x = va_arg(ap, int); // 通过具体的类型,来从通过指针提取特定的值 —— 没有参数就返回NULL
// va_end(ap); // 将指针设置为空(相当于:ap = nullptr)
char stdBuffer[1024]; //标准部分
time_t timestamp = time(nullptr);
//struct tm* localtime = localtime(×tamp); //- 详细时间输出操作
//localtime->tm_year;
snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", gLevelMap[level], timestamp);
char logBuffer[1024]; //自定义部分
va_list args;
va_start(args, format);
// 这个时候就有一个可变参数列表的起始地址
// 向屏幕中直接打印
// vprintf(format, args);
// 向缓冲区logBuffer中打印
vsnprintf(logBuffer, sizeof logBuffer, format, args);
va_end(args);
// 向屏幕
printf("%s%s\n", stdBuffer, logBuffer);
// 向文件
FILE *fp = fopen(LOGFILE, "a");
fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
fclose(fp);
}
lockGuard.hpp
#pragma once
#include
#include
// RAII风格的加锁方式
class lockGuard
{
public:
lockGuard(pthread_mutex_t *mtx):mtx_(mtx)
{
pthread_mutex_lock(mtx_);
}
~lockGuard()
{
pthread_mutex_unlock(mtx_);
}
private:
pthread_mutex_t *mtx_;
};
thread.hpp
#pragma once
#include
#include
#include
// 对线程的封装 - 不是完全必要,但是这样便于后期的统一管理
typedef void*(*fun_t)(void*);
// 整合线程的数据
class ThreadData
{
public:
std::string _name;
void* _args;
};
class Thread
{
public:
Thread(int num, fun_t callback, void* args):_func(callback)
{
char nameBuffer[64];
snprintf(nameBuffer, sizeof(nameBuffer), "Thread-%d", num);
_name = nameBuffer;
_tdata._args = args;
_tdata._name = _name;
}
void start()
{
pthread_create(&_tid, nullptr, _func, (void*)&_tdata);
}
void join()
{
pthread_join(_tid, nullptr);
}
// 未来不再使用线程id了,因为其是一个地址不便于我们查看
std::string name()
{
return _name;
}
~Thread()
{}
private:
std::string _name;
fun_t _func;
ThreadData _tdata;
pthread_t _tid;
};
Task.hpp
#pragma once
#include "log.hpp"
#include
#include
#include
typedef std::function func_t;
class Task
{
public:
Task(){}
Task(int x, int y, func_t func):_x(x), _y(y), _func(func)
{}
void operator()(std::string& name)
{
//std::cout << "线程" << name << "处理完成,结果是" << _x << "+" << _y << "=" << _func(_x, _y) << std::endl;
logMessage(WARNING, "%s处理完成: %d+%d=%d | %s | %d",
name.c_str(), _x, _y, _func(_x, _y), __FILE__, __LINE__); // __FILE__, __LINE__:预处理符。
}
public:
int _x;
int _y;
// int type;
func_t _func;
};
threadPool.hpp
#include "thread.hpp"
#include "lockGuard.hpp"
#include "log.hpp"
#include
#include
#include
#include
const int g_thread_num = 3;
template
class ThreadPool
{
public:
// 返回锁的地址
pthread_mutex_t *getMutex()
{
return &_lock;
}
bool isEmpty()
{
return _task_queue.empty();
}
void waitCond()
{
pthread_cond_wait(&_cond, &_lock);
}
T getTask()
{
T t = _task_queue.front();
_task_queue.pop();
return t;
}
private:
ThreadPool(int thread_num = g_thread_num) : _num(thread_num)
{
for (int i = 1; i <= _num; i++)
{
_threads.push_back(new Thread(i, routine, this));
}
pthread_mutex_init(&_lock, nullptr);
pthread_cond_init(&_cond, nullptr);
}
ThreadPool(const ThreadPool &other) = delete;
const ThreadPool &operator=(const ThreadPool &other) = delete;
public:
// 提供一个函数创建对象
// 想获取对象就只有调用这个函数
static ThreadPool *getThreaedPool(int num = g_thread_num)
{
// 防止出现:大量的申请和释放锁的行为,而导致的无用且浪费资源的行为。
if (nullptr == _thread_ptr)
{
lockGuard lockguard(&mutex);
// 由于_thread_ptr是由static修饰的 - 只有一份
if (nullptr == _thread_ptr)
{
_thread_ptr = new ThreadPool(num);
}
}
return _thread_ptr; // 返回的永远都是一个线程池对象
}
// 1.run - 将线程跑起来
void run()
{
for (auto &iter : _threads)
{
iter->start();
// std::cout << iter->name() << " 启动成功" << std::endl;
logMessage(NORMAL, "%s %s", iter->name().c_str(), "启动成功");
}
}
// 未来所有执行流所执行的方法 - 核心取任务、执行任务的逻辑
static void *routine(void *args) // 因为在类当中,如果是一个成员方法,其会有一个隐藏的参数this指针,所以我们需要使用static进行修饰。
{
ThreadData *td = (ThreadData *)args;
ThreadPool *tp = (ThreadPool *)td->_args;
while (true)
{
T task; // 对应任务的对象
{
// lock
lockGuard lockguard(tp->getMutex());
// while(task_queue_.empty()) wait();
while (tp->isEmpty())
tp->waitCond();
// 获取任务 - 100%有任务
task = tp->getTask(); // 任务队列是共享的->将任务从共享,拿到自己的私有空间
} // 自动释放锁
// 处理任务
task(td->_name); // 要求每一个任务都要提供一个仿函数
}
}
// 2.pushTask - 将任务放到任务池里
void pushTask(const T &task)
{
lockGuard lockguard(&_lock); // 加锁
_task_queue.push(task); // 压入任务
pthread_cond_signal(&_cond); // 唤醒线程
} // 自动释放锁
// void join()
// {
// for (auto &iter : _threads)
// {
// iter->join();
// }
// }
~ThreadPool()
{
for (auto &iter : _threads)
{
iter->join();
delete iter;
}
pthread_mutex_destroy(&_lock);
pthread_cond_destroy(&_cond);
}
private:
std::vector _threads;
int _num;
std::queue _task_queue;
static ThreadPool *_thread_ptr;
static pthread_mutex_t _mutex;
pthread_mutex_t _lock;
pthread_cond_t _cond;
};
// 通过定义一个静态的指针,来在类外来对静态成员进行初始化
template
ThreadPool *ThreadPool::_thread_ptr = nullptr;
template
pthread_mutex_t ThreadPool::_mutex = PTHREAD_MUTEX_INITIALIZER;
testMain.cc
#include "threadPool.hpp"
#include "Task.hpp"
#include "log.hpp"
#include
#include
int main()
{
logMessage(NORMAL, "%s %d %c %f\n", "这是一条日志信息", 1234, 'a', 3.14);
srand((unsigned long)time(nullptr) ^ getpid()); // 利用 ^ getpid()让数据更随机
// 运行一个线程库
ThreadPool::getThreaedPool()->run();
while (true)
{
// 生产的过程(制作任务的时候,要花时间)
int x = rand() % 1000;
usleep(5000); // 模拟制作任务的时候,花费的时间
int y = rand() % 1000;
Task t(x, y, [](int x, int y)->int{
return x + y;
});
// std::cout << "制作任务完成" << std::endl;
logMessage(DEBUG, "制作任务完成: %d+%d=?", x, y);
// 推送任务到线程池中
ThreadPool::getThreaedPool()->pushTask(t);
sleep(1); // 防止刷新过快
}
return 0;
}
#问: STL中的容器是否是线程安全的?
#问: 智能指针是否是线程安全的?
我们通过加锁的方案保证临界区的安全,就是:串行化 + 互斥。申请锁成功进入访问,申请锁失败阻塞挂起(执行流被挂起阻塞)(新的PCB被投入到锁的等待队列当中,并且将PCB的状态由R改变)。
循环的对一个锁进行申请(看这个锁是否好了),就是轮询检测,轮询检测就叫做自旋。自旋锁:本质就是通过不断检测锁状态,来进行资源是否就绪的方案。
#问:什么时候使用自旋锁?
取决于资源就绪的时间问题(时间长采用挂起等待,时间短采用轮询检测),核心就在于:在临界区中执行的时长。
#问:怎么使用自旋锁?
#include
// 释放锁
int pthread_spin_destroy(pthread_spinlock_t *lock);
// 创建锁
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
#include
// 加锁
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
#include
// 解锁
int pthread_spin_unlock(pthread_spinlock_t *lock);
在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。
读者写者模型不同于生产消费模型,我们可以理解为一个正在刊登的黑板报,对于黑板报的内容是只能一个写者来进行书写,但是对于黑板报的内容是可以一堆读者来一起读的,但是并不能在写者未书写完的时候进行读取。
Note:
写独占,读共享,读锁优先级高。
#问:读者写者 vs 生产消费的本质区别?
消费者会取走数据,读者不会取走数据。
没有实际意义,但是增强理解。
Note:
读者优先,写者优先问题:根据引用场景,如:数据被读取的频率非常高,而被修改的频率特别低。
初始化与销毁
#include
// 初始化
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t
*restrict attr);
// 销毁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
#include
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);