目录
线程安全的单例模式
饿汉实现方式和懒汉实现方式
饿汉方式实现单例模式
懒汉方式实现单例模式
懒汉方式实现单例模式(线程安全版本)
普通版本的线程池
实现单例模式
线程池单例模式(线程安全版本)代码
STL、智能指针和线程安全
其他常见的各种锁
系统编程中的锁
自旋锁
读者写者问题
读写锁
伪代码
读写锁的加锁、解锁过程
读写锁接口
#问:什么是单例模式?
【C++】-- 特殊类设计_川入的博客-CSDN博客
单例模式相当于,一个类最终只能定义一个对象。单例模式常见的就是:
因为,凡是需要被设置成单例的,往往就是因为其本身不需要被频繁加载到内存。可能在系统里只要有一份就行了 / 可能其本身占的空间太大(将软件运行起来,加载的时候,其会创建一堆的全局变量、全局对象。并且如果我们在代码当中用同一个类,定义了多个对象,如果对象都是一样,就极有可能出现重复。这样是不合理的)。所以,通过单例,实现特定的类只能实现一个对象。
[洗完的例子]
- 吃完饭,立刻洗碗,这种就是饿汉方式。因为下一顿吃的时候可以立刻拿着碗就能吃饭。
- 吃完饭,先把碗放下,然后下一顿饭用到这个碗了再洗碗,就是懒汉方式。
懒汉方式最核心的思想是 "延时加载",从而能够优化服务器的启动速度。
template
class Singleton {static T data;
public:static T* GetInstance() {return &data;}
};
只要通过 Singleton 这个包装类来使用 T 对象, 则一个进程中只有一个 T 对象的实例.
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_SHOWif(level == DEBUG) return;
#endifchar 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; // 对应任务的对象{// locklockGuard 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_SHOWif(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; // 对应任务的对象{// locklockGuard 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中的容器是否是线程安全的?不是。原因是,STL 的设计初衷是将性能挖掘到极致,而一旦涉及到加锁保证线程安全,会对性能造成巨大的影响。而且对于不同的容器,加锁方式的不同,性能可能也不同(例如:hash表的锁表和锁桶)。因此 STL 默认不是线程安全,如果需要在多线程环境下使用,往往需要调用者自行保证线程安全。
#问:智能指针是否是线程安全的?对于 unique_ptr,由于只是在当前代码块范围内生效(和对象是强绑定的,只能和一个对象相关联,在一个线程的上下文当中使用),因此不涉及线程安全问题。(但是也有可能会被修饰为全局,所以不是很全面的说) 对于 shared_ptr,多个对象需要共用一个引用计数变量,所以会存在线程安全问题。但是标准库实现的时候考虑到了这个问题,基于原子操作(CAS)的方式保证 shared_ptr 能够高效,原子的操作引用计数。虽然 shared_ptr 里面的操作是原子的,但是我们用的不是 shared_ptr 本身,而是它所指向的对象,并不能保证所指向的对象是线程安全的。所以还是该加锁加锁……。
我们通过加锁的方案保证临界区的安全,就是:串行化 + 互斥。申请锁成功进入访问,申请锁失败阻塞挂起(执行流被挂起阻塞)(新的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);
上一篇:Fluent中创建监测点
下一篇:Unity性能优化 - 总篇章