目錄
線程安全和重?問題
死鎖和活鎖
死鎖
死鎖四個必要條件
活鎖
STL,智能指針和線程安全
線程安全的單例模式
餓漢模式
懶漢模式
懶漢模式實現單例模式(線程安全版本)
餓漢模式實現單例模式
我們來學習單例模式與線程安全
線程安全和重?問題
線程安全:就是多個線程在訪問共享資源時,能夠正確地執?,不會相互?擾或破壞彼此的執?結 果。?般??,多個線程并發同?段只有局部變量的代碼時,不會出現不同的結果。但是對全局變量 或者靜態變量進?操作,并且沒有鎖保護的情況下,容易出現該問題。
重?:同?個函數被不同的執?流調?,當前?個流程還沒有執?完,就有其他的執?流再次進?, 我們稱之為重?。?個函數在重?的情況下,運?結果不會出現任何不同或者任何問題,則該函數被 稱為可重?函數,否則,是不可重?函數。
學到現在,其實我們已經能理解重?其實可以分為兩種情況 :多線程重?函數 和信號導致?個執?流重復進?函數
所以上面這些都可以不用看的,直接輸出結論:
全局變量屬于臨界資源。
死鎖和活鎖
死鎖
死鎖是指在?組進程中的各個進程均占有不會釋放的資源,但因互相申請被其他進程所站?不會 釋放的資源?處于的?種永久等待狀態。
死鎖是指兩個或多個線程因為相互等待對方釋放資源,導致它們永遠無法繼續執行的狀態。
就是多個線程申請多個未釋放的鎖,然后他們各自卻持有這些鎖(未解鎖),導致都需要等待對方釋放,比如A線程持有鎖a,B線程持有鎖b,那B今天來申請鎖A,A申請鎖b,這樣A,B線程都需要各自等待對方釋放才能申請成功,跟那個shared_ptr的循環引用無法釋放一樣的道理。
死鎖本質上就是多個線程交叉持有對方需要的資源,但又都不釋放,導致所有線程都卡死在等待狀態。
只有一個線程申請鎖時也可能導致死鎖的,自己重復申請自己上次沒有釋放的鎖,怎么解決死鎖問題,只能使用外部外部干預,要干預首先了解死鎖四個必要條件。
死鎖四個必要條件
互斥條件:?個資源每次只能被?個執?流使?,如果多個執行流可以訪問同一個鎖,怎么可能會死鎖呢。破壞這個條件不太可能,違背鎖的意愿。
請求與保持條件:?個執?流因請求資源?阻塞時,對已獲得的資源保持不放。
不剝奪條件:?個執?流已獲得的資源,在末使?完之前,不能強?剝奪,就是一個線程使用鎖進入臨界區訪問具有原子性,還沒訪問完不能解鎖。
循環等待條件:若?執?流之間形成?種頭尾相接的循環等待資源的關系,導致死鎖完全是在鎖上等待導致的。打破這個簡單呀,使用try_lock進行申請鎖就可以了呀,鎖不可用就返回false。
所以如何避免死鎖就是破壞死鎖的四個必要條件中的一個就可以了,破壞循環等待條件問題:資源?次性分配,使?超時機制、加鎖順序?致,也可以避免鎖未釋放的場景。
活鎖
活鎖是指多個線程不斷地 相互讓步,但因為策略問題,導致它們無法取得進展,活鎖和死鎖的區別就是線程不會一直等待一個沒有解鎖的鎖,而是一直不斷的申請,直至申請成功。相當于這些線程一直在申請結果沒有成功,沒有成功還一直在申請。
由于不停的申請鎖會帶來CPU的高占用,所以也是程序禁止的一種情況。
STL,智能指針和線程安全
STL中的容器不一定都是線程安全的,大部分函數都不支持重入所以都是線程不安全的,STL的設計初衷是將性能挖掘到極致,??旦涉及到加鎖保證線程安全,會對性能造成巨?的影 響。?且對于不同的容器,加鎖?式的不同,性能可能也不同(例如hash表的鎖表和鎖桶)。因此STL默認不是線程安全,如果需要在多線程環境下使?,往往需要調?者??保證線程安全。
智能指針是線程安全的,所以我們才愿意將一個線程交給他管理,對于unique_ptr,由于只是在當前代碼塊范圍內?效,因此不涉及線程安全問題。
對于shared_ptr,多個對象需要共??個引?計數變量,所以會存在線程安全問題。但是標準庫實現的時 候考慮到了這個問題,基于原?操作(CAS)的?式保證shared_ptr能夠?效,原?的操作引?計數。
但是智能指針指向的對象不一定是線程安全的,只是智能指針的操作的線程安全的。
線程安全的單例模式
某些類,只應該具有?個對象(實例),就稱之為單例。?例如?個男?只能有?個媳婦。?在很多服務器開發場景中,經常需要讓服務器加載很多的數據(上百G)到內存中,此時往往要??個單例 的類來管理這些數據。
單例模式主要分為餓漢模式和懶漢模式。要實現單例那這兩種模式都是禁止拷貝的,而且初始化函數必須放為私有,不讓外部直接通過構造函數訪問。
餓漢模式
吃完飯, ?刻洗碗, 這種就是餓漢?式.,因為下?頓吃的時候可以?刻拿著碗就能吃飯。也就是餓漢模式里面肯定已經創建出了一個靜態的對象,等你要調用的時候,通過一個static函數返回,保證返回函數,和創建的靜態變量在類中只有一份,防止拷貝。
懶漢模式
吃完飯, 先把碗放下, 然后下?頓飯?到這個碗了再洗碗, 就是懶漢?式,就是需要時才創建對象的機制,屬于延遲創建/加載,同樣也只有一個對象,所以new出來的對象需要裝在一個static指針里面,而這個指針是類里面已經創建好的。
反正需要時才調用getinstance創建,然后返回。存在?個嚴重的問題,線程不安全。?第?次調?GetInstance的時候,如果兩個線程同時調?,可能會創建出兩份T對象的實例,?但是后續再次調?,就沒有問題了,此時inst就不是空了,不能創建對象了。
懶漢模式實現單例模式(線程安全版本)
#pragma once
#include<iostream>
using namespace std;
#include<memory>
#include<unistd.h>
#include<vector>
#include<string>
#include<queue>
#include<functional> //
#include"pthread.hpp"
#include"mutex.hpp"
#include"cond.hpp"
#include"log.hpp"namespace threadpoolmodule
{using namespace lockmodule;using namespace threadmodule;using namespace condmodule;using namespace logmodule;using thread_t = shared_ptr<thread>;//用來做測試的線程方法void defaulttest(){while (true){LOG(loglevel::DEBUG) << "我是一個測試方法";sleep(1);}}const static int defaultnum = 5;template<class T>class threadpool{private:bool isempty(){return _taskq.empty();}void HandleTask(string name){LOG(loglevel::INFO) << "線程進入HandleTask的邏輯\n";while (true){T t;{lockguard lockguard(mut);// 1.拿任務while (isempty() && _isrunning){_wait_num++;_cond.wait(mut);_wait_num--;}if (isempty() && !_isrunning){break;}t = _taskq.front();_taskq.pop();}//2 處理任務t(name); //規定,未來所有的任務處理都是必須提供()直接調用的方法}LOG(loglevel::INFO) << "線程:" << name << "退出";}private:threadpool(): _num(defaultnum), _wait_num(0),_isrunning(false){for (int i = 0; i < _num; i++){//_threads.push_back(make_shared<thread>(bind(&threadpool::HandleTask, this, placeholders::_1)));LOG(loglevel::INFO) << "構鍵線程" << _threads.back()->getname() << "對象 。。。成功";}}public:threadpool<T>& operator=(const threadpool<T>&) = delete; //使用operator=的賦值拷貝不能用了threadpool(const threadpool<T>&) = delete; //賦值拷貝不能用了static threadpool<T>* getinstance(){LOG(loglevel::INFO) << "單例首次被執行,需要加載對象...";if (instance == nullptr){instance = new threadpool<T>();}return instance;}void start(){if (_isrunning){return;}_isrunning = true;for (auto& thread_ptr : _threads){thread_ptr->start();LOG(loglevel::INFO) << "啟動線程" << thread_ptr->getname() << "。。。成功";}}void equeue(T in){lockguard lockguard(mut);if (!_isrunning){return;}_taskq.push(move(in));if ( _wait_num){_cond.notify();}}void stop(){//讓里面的線程都重新自己退出//退出之前需要將所有的任務都處理完,所以需要一次性喚醒所有在等待的線程if (_isrunning){_isrunning = false;if (_wait_num){_cond.notifyall();}}}void join(){for (auto& thread_ptr : _threads){thread_ptr->join();LOG(loglevel::INFO) << "停止線程" << thread_ptr->getname() << "。。。成功";}}~threadpool(){}private:vector<thread_t> _threads; //線程池中的線程初始個數在數組里面int _num; //線程池的容量queue<T> _taskq; //線程池中的任務隊列mutex mut;cond _cond;int _wait_num; //等待隊列線程bool _isrunning;static threadpool<T>* instance;};template<class T>threadpool<T>* threadpool<T>::instance = nullptr; //在外面初始化, 私有的static成員可以在類外初始化
};
instance必須放私有,這樣比較安全。
懶漢模型這種延遲生成,按需生成的技術在操作系統內核用的還是比較多的,像什么寫時拷貝,malloc創建空間,物理地址的映射都是。
“不到萬不得已,絕不提前分配”,這種按需分配的策略在操作系統內核中極為常見,既能提升性能,又能節省資源。
但是外面的getinstance方法還沒有加鎖保護,所以需要多增加一個鎖進行保護。進入函數的時候立即加鎖,加鎖前需要先判斷是否滿足instance是空的,不然白加鎖了,為什么使用雙重判斷呢,外層是為了防止白加鎖,內層是為了防止加鎖失敗或者其他原因導致多個線程進來從而導致instance被賦值兩次。
靜態成員函數不能直接訪問類的非靜態成員變量,所以這個鎖也應該是靜態的才可以被訪問到。
private:vector<thread_t> _threads; //線程池中的線程初始個數在數組里面int _num; //線程池的容量queue<T> _taskq; //線程池中的任務隊列mutex mut;static mutex _lock;cond _cond;int _wait_num; //等待隊列線程bool _isrunning;static threadpool<T>* instance;};template<class T>threadpool<T>* threadpool<T>::instance = nullptr; //在外面初始化, 私有的static成員可以在類外初始化template<class T>mutex threadpool<T>::_lock;
餓漢模式實現單例模式
#pragma once
#include<iostream>
using namespace std;
#include<memory>
#include<unistd.h>
#include<vector>
#include<string>
#include<queue>
#include<functional> //
#include"pthread.hpp"
#include"mutex.hpp"
#include"cond.hpp"
#include"log.hpp"namespace threadpoolmodule
{using namespace lockmodule;using namespace threadmodule;using namespace condmodule;using namespace logmodule;using thread_t = shared_ptr<thread>;//用來做測試的線程方法void defaulttest(){while (true){LOG(loglevel::DEBUG) << "我是一個測試方法";sleep(1);}}const static int defaultnum = 5;template<class T>class threadpool{private:bool isempty(){return _taskq.empty();}void HandleTask(string name){LOG(loglevel::INFO) << "線程進入HandleTask的邏輯\n";while (true){T t;{lockguard lockguard(mut);// 1.拿任務while (isempty() && _isrunning){_wait_num++;_cond.wait(mut);_wait_num--;}if (isempty() && !_isrunning){break;}t = _taskq.front();_taskq.pop();}//2 處理任務t(name); //規定,未來所有的任務處理都是必須提供()直接調用的方法}LOG(loglevel::INFO) << "線程:" << name << "退出";}private:threadpool(): _num(defaultnum), _wait_num(0),_isrunning(false){for (int i = 0; i < _num; i++){//_threads.push_back(make_shared<thread>(bind(&threadpool::HandleTask, this, placeholders::_1)));LOG(loglevel::INFO) << "構鍵線程" << _threads.back()->getname() << "對象 。。。成功";}}public:threadpool<T>& operator=(const threadpool<T>&) = delete; //使用operator=的賦值拷貝不能用了threadpool(const threadpool<T>&) = delete; //賦值拷貝不能用了static threadpool<T>* getinstance(){LOG(loglevel::INFO) << "單例首次被執行,需要加載對象...";return &instance;}void start(){if (_isrunning){return;}_isrunning = true;for (auto& thread_ptr : _threads){thread_ptr->start();LOG(loglevel::INFO) << "啟動線程" << thread_ptr->getname() << "。。。成功";}}void equeue(T in){lockguard lockguard(mut);if (!_isrunning){return;}_taskq.push(move(in));if ( _wait_num){_cond.notify();}}void stop(){//讓里面的線程都重新自己退出//退出之前需要將所有的任務都處理完,所以需要一次性喚醒所有在等待的線程if (_isrunning){_isrunning = false;if (_wait_num){_cond.notifyall();}}}void join(){for (auto& thread_ptr : _threads){thread_ptr->join();LOG(loglevel::INFO) << "停止線程" << thread_ptr->getname() << "。。。成功";}}~threadpool(){}private:vector<thread_t> _threads; //線程池中的線程初始個數在數組里面int _num; //線程池的容量queue<T> _taskq; //線程池中的任務隊列mutex mut;cond _cond;int _wait_num; //等待隊列線程bool _isrunning;static threadpool<T> instance;};template<class T>threadpool<T> threadpool<T>::instance; //在外面初始化, 私有的static成員可以在類外初始化
};
這個實現起來差不多呀,而且完全沒有線程安全問題,因為就一個對象。