1. 單例模式
單例模式是一種常用的設計模式,它確保一個類只有一個實例,并提供一個全局訪問點來獲取這個實例。這種模式在需要控制資源訪問、管理共享狀態或協調系統行為時非常有用。
單例模式的核心特點:
- 私有構造函數:防止外部通過new關鍵字創建實例;
- 靜態成員函數:提供全局訪問點(靜態成員函數getInstance());
- 禁止拷貝和賦值:通過delete關鍵字禁用拷貝構造函數和賦值運算符。
單例模式的應用場景:
- 日志系統:確保所有日志都寫入同一個日志文件;
- 配置管理:全局共享一份配置信息;
- 數據庫連接池:統一管理數據庫連接;
- 設備管理器:如打印機等硬件設備的管理。
需要注意的是,單例模式雖然方便,但也會帶來一些問題,如增加代碼耦合度、不利于單元測試等,因此在使用時需要權衡利弊。
在 C++ 中,單例模式主要有兩種實現方式:餓漢式和懶漢式,它們的核心區別在于實例創建的時機不同。
1.1 餓漢式實現
餓漢式在程序啟動時(類加載時)就創建唯一實例,不管后續是否會使用到。
class EagerSingleton {
private:// 私有構造函數,防止外部創建實例EagerSingleton() {}// 禁用拷貝構造和賦值運算EagerSingleton(const EagerSingleton&) = delete;EagerSingleton& operator=(const EagerSingleton&) = delete;// 靜態成員變量,程序啟動時就初始化static EagerSingleton instance;public:// 提供全局訪問點static EagerSingleton& getInstance() {return instance;}
};// 在類外初始化靜態成員
EagerSingleton EagerSingleton::instance;
餓漢式特點:
- 線程安全:由于實例在程序啟動時就創建,不存在多線程競爭問題;
- 可能浪費資源:如果單例從未被使用,也會占用內存;
- 實現簡單:不需要考慮線程同步問題。
1.2 懶漢式實現
懶漢式在第一次調用getInstance()方法時才創建實例,實現了 "延遲初始化"。
class LazySingleton {
private:// 私有構造函數LazySingleton() {}// 禁用拷貝構造和賦值運算LazySingleton(const LazySingleton&) = delete;LazySingleton& operator=(const LazySingleton&) = delete;public:// 提供全局訪問點,C++11后局部靜態變量初始化是線程安全的static LazySingleton& getInstance() {// 第一次調用時才初始化實例static LazySingleton instance;return instance;}
};
或者:
class LazySingleton {
private:// 私有構造函數LazySingleton() {}// 私有析構函數~LazySingleton() {instance = nullptr;}// 禁用拷貝構造和賦值運算LazySingleton(const LazySingleton&) = delete;LazySingleton& operator=(const LazySingleton&) = delete;// 使用指針的方式來訪問唯一實例static LazySingleton *instance;public:// 此方式無法保證線程安全,應當加鎖保護static LazySingleton* const getInstance() {// 第一次調用時才初始化實例if(instance == nullptr)instance = new LazySingleton();return instance;}
};LazySingleton* LazySingleton::instance = nullptr;
前者和餓漢式一樣,實例在創建之后就不會消失;后者的實例則可以在被銷毀之后重新申請。
懶漢式特點:
- 延遲初始化:節省資源,只有在真正需要時才創建實例;
- 線程安全(C++11 及以上):標準保證局部靜態變量的初始化是線程安全的;
- 可能影響首次訪問性能:第一次調用時需要完成初始化。
1.3 總結
餓漢式 | 懶漢式 | |
實例創建時機 | 程序啟動時 | 首次使用時 |
線程安全性 | 天然安全 | C++11 后安全 |
資源利用 | 可能浪費 | 更高效 |
實現復雜度 | 簡單 | 稍復雜(需考慮線程安全) |
實際開發中,懶漢式因為其資源利用效率更高而更常用,尤其是在單例可能不會被使用的場景下。而餓漢式適合在程序啟動時就需要初始化的核心組件。
2. 用單例模式設計線程池
2.1 什么是線程池
線程池是一種線程管理機制,它預先創建一定數量的線程,通過復用這些線程來處理多個任務,從而避免頻繁創建和銷毀線程帶來的性能開銷,提高系統效率和資源利用率。
線程池包含一個線程隊列和一個任務隊列:
- 線程隊列:存儲預先創建的空閑線程,等待處理任務。
- 任務隊列:存放待執行的任務,當線程空閑時會從隊列中獲取任務執行。
2.2 如何將任務傳遞給線程
首先,任務隊列一定是定義在線程池內部的,我們要使線程訪問到任務隊列及保護任務隊列的鎖,就要使這些線程能訪問到線程池本身。
所以,我們在線程池內部定義線程的運行函數,即不斷循環訪問任務隊列獲取任務:
void ThreadHandler()
{while (true){Task task;{LockGuard lockguard(_queue_mutex);while (_queue.empty() && _isrunning){_not_empty.wait(_queue_mutex);}if (_queue.empty() && !_isrunning)break;task = _queue.front();_queue.pop();}task();}LOG(LogLevel::DEBUG) << "線程[" << Thread::GetMyName() << "]退出...";
}
但是直接將該函數傳遞給線程是不行的。例如,在構造線程時,直接將該函數以及this傳過去:
ThreadPool(unsigned int thread_num = default_thread_num): _isrunning(true)
{if (thread_num <= 0)throw ThreadPoolException("線程數量過少!");for (int i = 1; i <= thread_num; i++)_threads.emplace_back("thread-" + std::to_string(i), ThreadHandler, this);
}
編譯就會出現如下報錯:
這個編譯錯誤的核心原因是:非靜態成員函數(ThreadHandler)不能直接作為函數名傳遞給線程構造函數。非靜態成員函數的調用必須依賴于一個類的實例(this指針),而直接進行傳參時,編譯器無法自動關聯實例,因此判定為 “無效使用非靜態成員函數”。
讓線程執行非靜態成員函數的正確方式是使用lambda表達式捕捉this并包裝該函數:
[this] { ThreadHandler();
}
然后像這樣傳參:
_threads.emplace_back("thread-" + std::to_string(i), [this]{ ThreadHandler(); });
2.3 懶漢實現1
#pragma once
#include <vector>
#include <queue>
#include "Mutex.hpp"
#include "Thread.hpp"
#include "Cond.hpp"
#include "Log.hpp"using namespace MutexModule;
using namespace ThreadModule;
using namespace CondModule;
using namespace LogModule;namespace ThreadPoolModule
{class ThreadPoolException : public std::runtime_error{public:explicit ThreadPoolException(const std::string &message): std::runtime_error("ThreadPoolException: " + message) {}};static const unsigned int default_thread_num = 6;static const unsigned int default_capacity = 9;template <typename Task>class ThreadPool{private:ThreadPool(unsigned int thread_num = default_thread_num): _isrunning(true){if (thread_num <= 0)throw ThreadPoolException("線程數量過少!");for (int i = 1; i <= thread_num; i++)_threads.emplace_back("thread-" + std::to_string(i), [this]{ ThreadHandler(); });}~ThreadPool(){}ThreadPool(const ThreadPool<Task>&) = delete;ThreadPool<Task>& operator=(const ThreadPool<Task>&) = delete;void ThreadHandler(){while (true){Task task;{LockGuard lockguard(_queue_mutex);while(_queue.empty() && _isrunning){_not_empty.wait(_queue_mutex);}if(_queue.empty() && !_isrunning)break;task = _queue.front();_queue.pop();}task();}LOG(LogLevel::DEBUG) << "線程[" << Thread::GetMyName() << "]退出...";}void Start(){for (auto &thread : _threads)thread.Start();LOG(LogLevel::DEBUG) << "線程池已啟動...";}public:void Stop(){_isrunning = false;_not_empty.broadcast();LOG(LogLevel::DEBUG) << "線程池開始停止...";}void Wait(){if(_isrunning)throw ThreadPoolException("等待前線程池未停止!");for (auto &thread : _threads)thread.Join();LOG(LogLevel::DEBUG) << "等待成功, 線程已全部退出...";}void PushTask(const Task &task){if(!_isrunning)throw ThreadPoolException("線程池已停止, 無法繼續增加任務!");_queue.push(task);_not_empty.signal();}static ThreadPool<Task>& GetInstance(){static ThreadPool<Task> Instance;Instance.Start();return Instance;}private:bool _isrunning;std::vector<Thread> _threads;std::queue<Task> _queue;Mutex _queue_mutex;Cond _not_empty;};
}
2.4 懶漢實現2
#pragma once
#include <vector>
#include <queue>
#include "Mutex.hpp"
#include "Thread.hpp"
#include "Cond.hpp"
#include "Log.hpp"using namespace MutexModule;
using namespace ThreadModule;
using namespace CondModule;
using namespace LogModule;namespace ThreadPoolModule
{class ThreadPoolException : public std::runtime_error{public:explicit ThreadPoolException(const std::string &message): std::runtime_error("ThreadPoolException: " + message) {}};static const unsigned int default_thread_num = 6;static const unsigned int default_capacity = 9;template <typename Task>class ThreadPool{private:ThreadPool(unsigned int thread_num = default_thread_num): _isrunning(true){if (thread_num <= 0)throw ThreadPoolException("線程數量過少!");for (int i = 1; i <= thread_num; i++)_threads.emplace_back("thread-" + std::to_string(i), [this]{ ThreadHandler(); });}~ThreadPool(){_ins = nullptr;}ThreadPool(const ThreadPool<Task>&) = delete;ThreadPool<Task>& operator=(const ThreadPool<Task>&) = delete;void ThreadHandler(){while (true){Task task;{LockGuard lockguard(_queue_mutex);while(_queue.empty() && _isrunning){_not_empty.wait(_queue_mutex);}if(_queue.empty() && !_isrunning)break;task = _queue.front();_queue.pop();}task();}LOG(LogLevel::DEBUG) << "線程[" << Thread::GetMyName() << "]退出...";}void Start(){for (auto &thread : _threads)thread.Start();LOG(LogLevel::DEBUG) << "線程池已啟動...";}public:void Stop(){_isrunning = false;_not_empty.broadcast();LOG(LogLevel::DEBUG) << "線程池開始停止...";}void Wait(){if(_isrunning)throw ThreadPoolException("等待前線程池未停止!");for (auto &thread : _threads)thread.Join();delete _ins;LOG(LogLevel::DEBUG) << "等待成功, 線程已全部退出...";}void PushTask(const Task &task){if(!_isrunning)throw ThreadPoolException("線程池已停止, 無法繼續增加任務!");_queue.push(task);_not_empty.signal();}static ThreadPool<Task> *const GetInstance(){if (_ins == nullptr){LockGuard lockguard(_mutex);if (_ins == nullptr){try{_ins = new ThreadPool<Task>();}catch (const std::exception &e){std::cerr << e.what() << '\n';}_ins->Start();}}return _ins;}private:bool _isrunning;std::vector<Thread> _threads;std::queue<Task> _queue;static ThreadPool<Task> *_ins;Mutex _queue_mutex;static Mutex _mutex;Cond _not_empty;};template <typename Task>ThreadPool<Task> *ThreadPool<Task>::_ins = nullptr;template <typename Task>Mutex ThreadPool<Task>::_mutex;
}
相比較于第一種,第二種可以在調用Stop以及Wait函數之后重新啟動。