1.線程池實現
全部代碼:whb-helloworld/113
1.喚醒線程
一個是喚醒全部線程,一個是喚醒一個線程。
void WakeUpAllThread(){LockGuard lockguard(_mutex);if (_sleepernum)_cond.Broadcast();LOG(LogLevel::INFO) << "喚醒所有的休眠線程";}void WakeUpOne(){_cond.Signal();LOG(LogLevel::INFO) << "喚醒一個休眠線程";}
2.創建線程
全局變量gnum的值就是要創建的線程的個數,然后把lambda表達式插入進去,Thread里面有function可以接收這個表達式,然后執行routine函數時就會執行這個表達式。emplace_back一個表達式,本質就是把這個表達式復制給這個vector<Thread>的Thread類的元素,就是傳參過去。
ThreadPool(int num = gnum) : _num(num), _isrunning(false), _sleepernum(0){for (int i = 0; i < num; i++){_threads.emplace_back([this](){HandlerTask();});}}
using func_t = std::function<void()>;static void *Routine(void *args) // 屬于類內的成員函數,默認包含this指針!{Thread *self = static_cast<Thread *>(args);self->EnableRunning();if (self->_isdetach)self->Detach();pthread_setname_np(self->_tid, self->_name.c_str());self->_func(); // 回調處理return nullptr;}
?
在C++中,捕獲
this
是一種常見的做法,尤其是在使用 Lambda 表達式時。捕獲this
的主要目的是為了讓 Lambda 表達式能夠訪問類的成員變量和成員函數。
3.開始函數
判斷是否運行,沒有就把狀態變為true,然后范圍for把線程都啟動,并打印日志信息
void Start(){if (_isrunning)return;_isrunning = true;for (auto &thread : _threads){thread.Start();LOG(LogLevel::INFO) << "start new thread success: " << thread.Name();}}
?4.關掉拷貝和賦值
ThreadPool(const ThreadPool<T> &) = delete;ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;
5.單例模式
?先判斷是否創建了實例,沒有就加鎖,第二個判斷也是判斷是否有實例,沒有就創建并執行開始函數,最后返回inc這個指向實例的指針。第一個判斷是為了下次不用一直加鎖,只要保證第一次實例加鎖就行,后面就可以不用加鎖了,提高效率。
static ThreadPool<T> *GetInstance(){if (inc == nullptr){LockGuard lockguard(_lock);LOG(LogLevel::DEBUG) << "獲取單例....";if (inc == nullptr){LOG(LogLevel::DEBUG) << "首次使用單例, 創建之....";inc = new ThreadPool<T>();inc->Start();}}return inc;}
?6.停止函數和回收函數
根據_isrunning判斷狀態,如果是運行狀態,就變為false,然后喚醒所有線程開始執行任務,
join就回收這些線程的信息。
void Stop(){if (!_isrunning)return;_isrunning = false;// 喚醒所有的線層WakeUpAllThread();}void Join(){for (auto &thread : _threads){thread.Join();}}
?
7.處理任務函數
開始是把線程名字放到name里面,進到循環里面,要加鎖保證sleepernum值不會因為并發而改變,循環里面第一個循環只有任務隊列為空且線程是運行才會進到等待隊列進行休眠,sleep值是計算休眠的個數,判斷是線程不運行且任務隊列為空就退出了,沒退出就說明有任務,就取出隊列的任務給t,最后就可以t()執行任務。
void HandlerTask(){char name[128];pthread_getname_np(pthread_self(), name, sizeof(name));while (true){T t;{LockGuard lockguard(_mutex);// 1. a.隊列為空 b. 線程池沒有退出while (_taskq.empty() && _isrunning){_sleepernum++;_cond.Wait(_mutex);_sleepernum--;}// 2. 內部的線程被喚醒if (!_isrunning && _taskq.empty()){LOG(LogLevel::INFO) << name << " 退出了, 線程池退出&&任務隊列為空";break;}// 一定有任務t = _taskq.front(); // 從q中獲取任務,任務已經是線程私有的了!!!_taskq.pop();}t(); // 處理任務,需/要在臨界區內部處理嗎?1 0}}
pthread_getname_np(pthread_self(), name, sizeof(name));
pthread_getname_np
是一個 POSIX 線程庫(pthread)提供的函數,用于獲取線程的名稱。
pthread_self()
是一個函數,返回當前線程的線程ID(pthread_t
類型)。
name
是目標數組,用于存儲線程名稱。
sizeof(name)
是目標數組的大小,確保不會超出數組的存儲范圍。
8.入任務函數
判斷狀態,運行就進去加鎖,插入數據加鎖避免并發插入數據,在判斷線程大小和休眠個數是否相等,一樣就說明都休眠了,要喚醒線程進行處理任務。
bool Enqueue(const T &in){if (_isrunning){LockGuard lockguard(_mutex);_taskq.push(in);if (_threads.size() == _sleepernum)WakeUpOne();return true;}return false;}
9.完整代碼
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <queue>
#include "Log.hpp"
#include "Thread.hpp"
#include "Cond.hpp"
#include "Mutex.hpp"// .hpp header onlynamespace ThreadPoolModule
{using namespace ThreadModlue;using namespace LogModule;using namespace CondModule;using namespace MutexModule;static const int gnum = 5;template <typename T>class ThreadPool{private:void WakeUpAllThread(){LockGuard lockguard(_mutex);if (_sleepernum)_cond.Broadcast();LOG(LogLevel::INFO) << "喚醒所有的休眠線程";}void WakeUpOne(){_cond.Signal();LOG(LogLevel::INFO) << "喚醒一個休眠線程";}ThreadPool(int num = gnum) : _num(num), _isrunning(false), _sleepernum(0){for (int i = 0; i < num; i++){_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();}}ThreadPool(const ThreadPool<T> &) = delete;ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;public:static ThreadPool<T> *GetInstance(){if (inc == nullptr){LockGuard lockguard(_lock);LOG(LogLevel::DEBUG) << "獲取單例....";if (inc == nullptr){LOG(LogLevel::DEBUG) << "首次使用單例, 創建之....";inc = new ThreadPool<T>();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 (true){T t;{LockGuard lockguard(_mutex);// 1. a.隊列為空 b. 線程池沒有退出while (_taskq.empty() && _isrunning){_sleepernum++;_cond.Wait(_mutex);_sleepernum--;}// 2. 內部的線程被喚醒if (!_isrunning && _taskq.empty()){LOG(LogLevel::INFO) << name << " 退出了, 線程池退出&&任務隊列為空";break;}// 一定有任務t = _taskq.front(); // 從q中獲取任務,任務已經是線程私有的了!!!_taskq.pop();}t(); // 處理任務,需/要在臨界區內部處理嗎?1 0}}bool Enqueue(const T &in){if (_isrunning){LockGuard lockguard(_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;// bug??static ThreadPool<T> *inc; // 單例指針static Mutex _lock;};template <typename T>ThreadPool<T> *ThreadPool<T>::inc = nullptr;template <typename T>Mutex ThreadPool<T>::_lock;}
2.單例模式與設計模式
類里面static修飾變量,則變量是屬于類的,而不是屬于某個特定對象,無論創建多少個對象,static變量都只有一份,所有對象共享一份static變量。
static修飾方法,則方法也是屬于類的,不能訪問非靜態成員(變量和方法),因為非靜態成員需要對象才能訪問,所有對象共享一個static方法
而鎖加static,是為了保證唯一鎖,不然創建多個鎖沒有static,則每個鎖都是不一樣的,就等于一個門上有很多鑰匙孔,static就可以保證只有一個鑰匙孔和一把鑰匙。
單例模式
單例模式是屬于一種創建型設計模式,確保一個類在整個應用程序中只有一個實例,提供一個全局訪問點來訪問這個實例這個實例。
餓漢模式
餓漢模式在類被加載時就立即創建實例,而不是在第一次需要實例時才創建。這種方式特定是實例創建早,通長用于資源不多且創建實例開銷較小的情況,因為每次執行都要創建一個實例,如果實例體積大就會占用很多內存,所以適用于資源不多的。
餓漢模式特點
立即實例化:類加載時就創建了單例實例,不管是否有其它地方需要這個實例
線程安全:由于實例在加載時就創建好了,線程安全問題得到天然的解決
浪費資源:單例對象的創建開銷大,且開辟的空間不一定用的上,會有低效率
當兩個線程同時訪問一個類,并且這個類采用立即實例化的方式創建單例時,由于實例化是在類加載時完成的,所以實際上不存在兩個線程同時創建實例的情況。下面是具體的原因和過程:
1. **類加載機制**:在Java中,類加載是由類加載器完成的。類加載器會保證一個類只被加載一次。當第一個線程觸發類的加載時,類加載器會同步這個加載過程,確保其他線程在當前線程完成加載之前不會進入類的加載過程。
2. **靜態初始化**:在類加載的過程中,如果類中包含靜態初始化塊或者靜態變量初始化,這些操作會在類加載時執行。如果單例實例是在靜態初始化塊中創建的,那么這個實例的創建過程是同步的,即在任何線程看到類的加載完成之前,實例已經被創建好了。
3. **線程安全保證**:由于類加載和靜態初始化是同步的,這意味著當第一個線程觸發類的加載并創建實例時,其他線程會被阻塞,直到實例創建完成。因此,不會有多個線程同時創建實例的情況發生。
4. **可見性**:在Java中,靜態變量的初始化具有可見性保證。一旦靜態變量被初始化,它對所有線程都是可見的。這意味著一旦單例實例被創建,所有線程都可以看到它。
總結來說,即使在多線程環境中,由于類加載和靜態初始化的同步機制,以及靜態變量的可見性保證,立即實例化的單例模式可以確保在任何時候都只有一個實例被創建,從而保證了線程安全。這就是為什么即使有兩個線程同時訪問一個采用立即實例化的類,也不會導致線程安全問題的原因。
餓漢模式單例實現
template<typename T>
class Singleton
{static T data;
public:static T* GetInstance(){return &data;}
};
懶漢模式
是一種單例模式的實現方式,就需要在第一次實例時創建對象。特點是推遲實例化,從而避免對不必要的資源消耗。
懶漢單例模式特點
延遲實例化:對象僅在第一次被請求時才進行創建,從而提高程序的效率
資源節省:在不需要實例的情況下,避免了資源的浪費,優化了資源的使用
線程安全問題:在多線程環境下,可能需要額外的同步機制來確保線程安全問題,確保實例的唯一性。
?懶漢單例模式的實現
template<typename T>
class Singleton
{static T* inst;
public:static T* GetInstance(){if (inst == nullptr){inst = new T();}return inst;}
};
上面線程池以懶漢模式實現
3.線程安全和重入問題
常?的線程不安全的情況?不保護共享變量的函數?函數狀態隨著被調?,狀態發?變化的函數?返回指向靜態變量指針的函數?調?線程不安全函數的函數常?不可重?的情況?調?了malloc/free函數,因為malloc函數是?全局鏈表來管理堆的?調?了標準I/O庫函數,標準I/O庫的很多實現都以不可重?的?式使?全局數據結構?可重?函數體內使?了靜態的數據結構常?的線程安全的情況?每個線程對全局變量或者靜態變量只有讀取的權限,?沒有寫?的權限,?般來說這些線程是安全的?類或者接?對于線程來說都是原?操作?多個線程之間的切換不會導致該接?的執?結果存在?義性常?可重?的情況?不使?全局變量或靜態變量?不使? malloc或者new開辟出的空間?不調?不可重?函數?不返回靜態或全局數據,所有數據都有函數的調?者提供?使?本地數據,或者通過制作全局數據的本地拷?來保護全局數據
4.常見鎖的概念


?
?死鎖的四個必要條件
互斥條件:一個資源每次只能被一個執行流使用
請求與保持條件:一個執行流因請求資源而阻塞,對已獲得的資源保持不放
?
5.STL,智能指針和線程安全
?STL的容器不是線程安全的,STL的設計初衷是將性能挖掘到極致,而一旦設計到加鎖保證線程安全,會對性能造成巨大影響,而且對于不同的容器,加鎖的方式不同。這樣STL默認是線程不安全的,需要自己保證線程安全。
智能指針unique_ptr,由于只是在當前代碼塊內生效,不涉及線程安全問題。
對于shared_ptr,多個對象需要共用一個引用計數變量,就會存在線程安全問題。標準庫解決了這個問題,基于原子操作的(CAS)的方式保證了shared_ptr能夠高效,原子的操作引用計數。