日志
關于日志,首先我們來說一下日志的作用,
問題追蹤:通過日志不僅僅包括我們程序的一些bug,也可以在安裝配置時,通過日志可以發現問題。狀態監控:通過實時分析日志,可以監控系統的運行狀態,做到早發現問題、早處理問題。安全審計:審計主要體現在安全上,通過對日志進行分析,可以發現是否存在非授權的操作。
日志并不是越多越詳細就越好。在分析運行日志,查找問題時,我們經常遇到該出現的日志沒有,無用的日志一大堆,或者有效的日志被大量無意義的日志信息淹沒,查找起來非常困難。那么什么時候輸出日志呢?以下列出了一些常見的需要輸出日志的情況:
1. 系統啟動參數、環境變量系統啟動的參數、配置、環境變量、System.Properties等信息對于軟件的正常運行至關重要,這些信息的輸出有助于安裝配置人員通過日志快速定位問題,所以程序有必要在啟動過程中把使用到的關鍵參數、變量在日志中輸出出來。在輸出時需要注意,不是一股腦的全部輸出,而是將軟件運行涉及到的配置信息輸出出來。比如,如果軟件對jvm的內存參數比較敏感,對最低配置有要求,那么就需要在日志中將-Xms -Xmx -XX:PermSize這幾個參數的值輸出出來。2. 異常捕獲處在捕獲異常處輸出日志,大家在基本都能做到,唯一需要注意的是怎么輸出一個簡單明了的日志信息。這在后面的問題問題中有進一步說明。3. 函數獲得期望之外的結果時一個函數,尤其是供外部系統或遠程調用的函數,通常都會有一個期望的結果,但如果內部系統或輸出參數發生錯誤時,函數將無法返回期望的正確結果,此時就需要記錄日志,日志的基本通常是warn。需要特別說明的是,這里的期望之外的結果不是說沒有返回就不需要記錄日志了,也不是說返回false就需要記錄日志。比如函數:isXXXXX(),無論返回true、false記錄日志都不是必須的,但是如果系統內部無法判斷應該返回true還是false時,就需要記錄日志,并且日志的級別應該至少是warn。4. 關鍵操作關鍵操作的日志一般是INFO級別,如果數量、頻度很高,可以考慮使用DEBUG級別。以下是一些關鍵操作的舉例,實際的關鍵操作肯定不止這么多。5.刪除:刪除一個文件、刪除一組重要數據庫記錄……5.添加:和外系統交互時,收到了一個文件、收到了一個任務……7.處理:開始、結束一條任務……
對于日志我們就說這些,下面我們看一下日志的代碼:
代碼語言:JavaScript代碼運行次數:0運行復制
#pragma once#include <iostream>#include <cstdio>#include <string>#include <cstring>#include <unistd.h>#include <fstream>#include <sstream>#include <filesystem> //c++17#include <memory>#include <time.h>#include "Mutex.hpp"namespace LogModule{ using namespace LockModule; std::string Currtime() { time_t time_stamp = ::time(nullptr); struct tm curr; localtime_r(&time_stamp, &curr); std::string buffer; buffer.resize(100); // 預留足夠空間,可根據實際情況調整大小 std::snprintf(&buffer[0], buffer.size(), "%4d-%02d-%02d %02d:%02d:%02d", curr.tm_year + 1900, curr.tm_mon + 1, curr.tm_mday, curr.tm_hour, curr.tm_min, curr.tm_sec); buffer.resize(std::strlen(&buffer[0])); // 調整大小為實際字符串長度 return buffer; } // 構成:1.構建日志字符串2.刷新落盤(screen,file) // 日志文件的默認路徑和文件名 // 2.日志等級 enum class LogLevel { DEBUG = 1, INFO, // 正常 WARNING, ERROR, FATAL // 致命的 }; std::string Level2String(LogLevel level) { switch (level) { case LogLevel::DEBUG: return "DEBUG"; break; case LogLevel::ERROR: return "ERROR"; break; case LogLevel::FATAL: return "FATAL"; break; case LogLevel::INFO: return "INFO"; break; case LogLevel::WARNING: return "WARNING"; break; default: return ""; } } const std::string defaultlogpath = "./log/"; const std::string defaultlogname = "log.txt"; // 3.刷新策略 class LogStrategy { public: virtual ~LogStrategy() = default; virtual void SyncLog(const std::string &message) = 0; private: }; // 3.1控制臺策略 class ConsoleLogStrategy : public LogStrategy // 繼承一下 { public: ConsoleLogStrategy() { } ~ConsoleLogStrategy() { } void SyncLog(const std::string &message) { LockGuard lockguard(_lock); // 保證刷新策略的安全 std::cout << message << std::endl; } private: Mutex _lock; // 鎖 }; // 3.2 文件級策略 class FileLogStrategy : public LogStrategy { public: FileLogStrategy(const std::string logpath = defaultlogpath, const std::string logname = defaultlogname) : _logpath(logpath), _logname(logname) { LockGuard lockguard(_lock); if (std::filesystem::exists(_logpath)) return; try { std::filesystem::create_directories(_logpath); } catch (const std::filesystem::filesystem_error &e) { std::cerr << e.what() << "n"; } } ~FileLogStrategy() { } void SyncLog(const std::string &message) { std::string log = _logpath + _logname; std::ofstream out(log, std::ios::app); // 日志寫入,一定是追加的 if (!out.is_open()) { return; } out << message << "n"; out.close(); } private: std::string _logpath; std::string _logname; Mutex _lock; }; // 日志類,構建日志字符串,根據策略進行刷新。 class Logger { public: Logger() { // 默認采用ConsoleLogStrategy _strategy = std::make_shared<ConsoleLogStrategy>(); } void EnableConsoleLog() { _strategy = std::make_shared<ConsoleLogStrategy>(); } void EnableFileLog() { _strategy = std::make_shared<FileLogStrategy>(); } ~Logger() { } // 一條完整的信息:[2024-8-09 12:32:22] [DEBUG] class LogMessage { public: LogMessage(LogLevel level, const std::string &filename, int line, Logger &logger) : _currtime(Currtime()), _level(level), _pid(getpid()), _filename(filename), _line(line), _logger(logger) { std::stringstream ssbuffer; ssbuffer << "[" << _currtime << "] " << "[" << Level2String(_level) << "] " << "[" << _pid << "] " << "[" << _filename << "] " << "[" << _line << "] "; _loginfo = ssbuffer.str(); } template <typename T> LogMessage &operator<<(const T &info) { std::stringstream ss; ss << info; _loginfo += ss.str(); return *this; } ~LogMessage() { if (_logger._strategy) { _logger._strategy->SyncLog(_loginfo); } } private: std::string _currtime; // 當前日志時間嗎,需要可讀性,所以不要時間戳 LogLevel _level; // 日志等級 pid_t _pid; // 進程pid std::string _filename; // 源文件名稱 uint32_t _line; // 日治所在的行號,32位的無符號整數 Logger &_logger; // 負責根據不同的策略進行刷新 std::string _loginfo; // 一條完整的日志記錄 }; // 就是要拷貝 LogMessage operator()(LogLevel level, const std::string &filename, int line) { return LogMessage(level, filename, line, *this); } private: std::shared_ptr<LogStrategy> _strategy; // 日志刷新的策略方案 }; Logger logger;#define LOG(Level) logger(Level, __FILE__, __LINE__)#define ENABLE_CONSOLE_LOG() logger.EnableConsoleLog()#define ENABLE_FILE_LOG() logger.EnableFileLog()}
基于環形隊列的生產消費模型
環形隊列采用數組模擬,用模運算來模擬環狀特性


但是我們現在有信號量這個計數器,就很簡單的進行多線程間的同步過程
下面我們看一下代碼:
RingQueue.hpp:
代碼語言:javascript代碼運行次數:0運行復制
#pragma once #include <iostream>#include <vector>#include <string>#include <pthread.h>#include <semaphore.h> template <typename T>class RingQueue{private: void P(sem_t &s)//申請信號量 { sem_wait(&s); } void V(sem_t &s)//釋放信號量 { sem_post(&s); }public: RingQueue(int max_cap) : _ringqueue(max_cap), _max_cap(max_cap), _c_step(0), _p_step(0) { sem_init(&_data_sem, 0, 0); sem_init(&_space_sem, 0, max_cap); pthread_mutex_init(&_c_mutex, nullptr); pthread_mutex_init(&_p_mutex, nullptr); } void Push(const T &in) //生產者 { // 信號量:是一個計數器,是資源的預訂機制。預訂:在外部,可以不判斷資源是否滿足,就可以知道內部資源的情況! P(_space_sem); // 信號量這里,對資源進行使用,申請,為什么不判斷一下條件是否滿足???信號量本身就是判斷條件! pthread_mutex_lock(&_p_mutex); //? _ringqueue[_p_step] = in; _p_step++; _p_step %= _max_cap; pthread_mutex_unlock(&_p_mutex); V(_data_sem); } void Pop(T *out) // 消費 { P(_data_sem); pthread_mutex_lock(&_c_mutex); //? *out = _ringqueue[_c_step]; _c_step++; _c_step %= _max_cap; pthread_mutex_unlock(&_c_mutex); V(_space_sem); } ~RingQueue() { sem_destroy(&_data_sem); sem_destroy(&_space_sem); pthread_mutex_destroy(&_c_mutex); pthread_mutex_destroy(&_p_mutex); }private: std::vector<T> _ringqueue; int _max_cap; int _c_step; int _p_step; sem_t _data_sem; // 消費者關心 sem_t _space_sem; // 生產者關心 pthread_mutex_t _c_mutex; pthread_mutex_t _p_mutex;};
Main.cc
代碼語言:javascript代碼運行次數:0運行復制
#include "RingQueue.hpp"#include "Task.hpp"#include <iostream>#include <pthread.h>#include <unistd.h>#include <ctime> void *Consumer(void*args){ RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(args); while(true) { Task t; // 1. 消費 rq->Pop(&t); // 2. 處理數據 t(); std::cout << "Consumer-> " << t.result() << std::endl; }}void *Productor(void*args){ RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(args); while(true) { sleep(1); // 1. 構造數據 int x = rand() % 10 + 1; //[1, 10] usleep(x*1000); int y = rand() % 10 + 1; Task t(x, y); // 2. 生產 rq->Push(t); std::cout << "Productor -> " << t.debug() << std::endl; }} int main(){ srand(time(nullptr) ^ getpid()); RingQueue<Task> *rq = new RingQueue<Task>(5); // 單單 pthread_t c1, c2, p1, p2, p3; pthread_create(&c1, nullptr, Consumer, rq); pthread_create(&c2, nullptr, Consumer, rq); pthread_create(&p1, nullptr, Productor, rq); pthread_create(&p2, nullptr, Productor, rq); pthread_create(&p3, nullptr, Productor, rq); pthread_join(c1, nullptr); pthread_join(c2, nullptr); pthread_join(p1, nullptr); pthread_join(p2, nullptr); pthread_join(p3, nullptr); return 0;}
Task.hpp
代碼語言:javascript代碼運行次數:0運行復制
#pragma once #include<iostream>#include<functional> // typedef std::function<void()> task_t;// using task_t = std::function<void()>; // void Download()// {// std::cout << "我是一個下載的任務" << std::endl;// } // 要做加法class Task{public: Task() { } Task(int x, int y) : _x(x), _y(y) { } void Excute() { _result = _x + _y; } void operator ()() { Excute(); } std::string debug() { std::string msg = std::to_string(_x) + "+" + std::to_string(_y) + "=?"; return msg; } std::string result() { std::string msg = std::to_string(_x) + "+" + std::to_string(_y) + "=" + std::to_string(_result); return msg; } private: int _x; int _y; int _result;};
線程池(懶漢單例模式)
線程池: 一種線程使用模式。線程過多會帶來調度開銷,進而影響緩存局部性和整體性能。而線程池維護著多個線程,等待著監督管理者分配可并發執行的任務。這避免了在處理短時間任務時創建與銷毀線程的代價。線程池不僅能夠保證內核的充分利用,還能防止過分調度。
這里我們主要還是看一下線程池這個代碼的實現,其他不在這里展示: ThreadPool.hpp:
代碼語言:javascript代碼運行次數:0運行復制
#pragma once #include <iostream>#include <unistd.h>#include <string>#include <vector>#include <queue>#include <functional>#include "Thread.hpp"#include "Log.hpp"#include "LockGuard.hpp" using namespace ThreadMoudle;using namespace log_ns; static const int gdefaultnum = 5; void test(){ while (true) { std::cout << "hello world" << std::endl; sleep(1); }} template <typename T>class ThreadPool{private: void LockQueue() { pthread_mutex_lock(&_mutex); } void UnlockQueue() { pthread_mutex_unlock(&_mutex); } void Wakeup() { pthread_cond_signal(&_cond); } void WakeupAll() { pthread_cond_broadcast(&_cond); } void Sleep() { pthread_cond_wait(&_cond, &_mutex); } bool IsEmpty() { return _task_queue.empty(); } void HandlerTask(const std::string &name) // this { while (true) { // 取任務 LockQueue(); while (IsEmpty() && _isrunning) { _sleep_thread_num++; LOG(INFO, "%s thread sleep begin!n", name.c_str()); Sleep(); LOG(INFO, "%s thread wakeup!n", name.c_str()); _sleep_thread_num--; } // 判定一種情況 if (IsEmpty() && !_isrunning) { UnlockQueue(); LOG(INFO, "%s thread quitn", name.c_str()); break; } // 有任務 T t = _task_queue.front(); _task_queue.pop(); UnlockQueue(); // 處理任務 t(); // 處理任務,此處不用/不能在臨界區中處理 // std::cout << name << ": " << t.result() << std::endl; LOG(DEBUG, "hander task done, task is : %sn", t.result().c_str()); } } void Init() { func_t func = std::bind(&ThreadPool::HandlerTask, this, std::placeholders::_1); for (int i = 0; i < _thread_num; i++) { std::string threadname = "thread-" + std::to_string(i + 1); _threads.emplace_back(threadname, func); LOG(DEBUG, "construct thread %s done, init successn", threadname.c_str()); } } void Start() { _isrunning = true; for (auto &thread : _threads) { LOG(DEBUG, "start thread %s done.n", thread.Name().c_str()); thread.Start(); } } ThreadPool(int thread_num = gdefaultnum) : _thread_num(thread_num), _isrunning(false), _sleep_thread_num(0) { pthread_mutex_init(&_mutex, nullptr); pthread_cond_init(&_cond, nullptr); } ThreadPool(const ThreadPool<T> &) = delete; void operator=(const ThreadPool<T> &) = delete; public: void Stop() { LockQueue(); _isrunning = false; WakeupAll(); UnlockQueue(); LOG(INFO, "Thread Pool Stop Success!n"); } // 如果是多線程獲取單例呢? static ThreadPool<T> *GetInstance() { if (_tp == nullptr) { LockGuard lockguard(&_sig_mutex); if (_tp == nullptr) { LOG(INFO, "create threadpooln"); // thread-1 thread-2 thread-3.... _tp = new ThreadPool(); _tp->Init(); _tp->Start(); } else { LOG(INFO, "get threadpooln"); } } return _tp; } void Equeue(const T &in) { LockQueue(); if (_isrunning) { _task_queue.push(in); if (_sleep_thread_num > 0) Wakeup(); } UnlockQueue(); } ~ThreadPool() { pthread_mutex_destroy(&_mutex); pthread_cond_destroy(&_cond); } private: int _thread_num; std::vector<Thread> _threads; std::queue<T> _task_queue; bool _isrunning; int _sleep_thread_num; pthread_mutex_t _mutex; pthread_cond_t _cond; // 單例模式 // volatile static ThreadPool<T> *_tp; static ThreadPool<T> *_tp; static pthread_mutex_t _sig_mutex;}; template <typename T>ThreadPool<T> *ThreadPool<T>::_tp = nullptr;template <typename T>pthread_mutex_t ThreadPool<T>::_sig_mutex = PTHREAD_MUTEX_INITIALIZER;
線程安全的單例模式
單例模式是一種 “經典的, 常用的, 常考的” 設計模式
大佬和菜雞們兩極分化的越來越嚴重. 為了讓菜雞們不太拖大佬的后腿, 于是大佬們針對一些經典的常見的場景, 給定了一些對應的解決方案, 這個就是設計模式
單例模式的特點
某些類, 只應該具有一個對象(實例), 就稱之為單例. 例如一個男人只能有一個媳婦. 在很多服務器開發場景中, 經常需要讓服務器加載很多的數據 (上百G) 到內存中. 此時往往要用一個單例的類來管理這些數據.
餓漢實現方式和懶漢實現方式吃完飯, 立刻洗碗, 這種就是餓漢方式. 因為下一頓吃的時候可以立刻拿著碗就能吃飯.吃完飯, 先把碗放下, 然后下一頓飯用到這個碗了再洗碗, 就是懶漢方式.
懶漢方式最核心的思想是 “延時加載”. 從而能夠優化服務器的啟動速度.
可重入VS線程安全概念:線程安全:多個線程并發同一段代碼時,不會出現不同的結果。常見對全局變量或者靜態變量進行操作, 并且沒有鎖保護的情況下,會出現該問題。重入:同一個函數被不同的執行流調用,當前一個流程還沒有執行完,就有其他的執行流再次進入,我們稱之為重入。一個函數在重入的情況下,運行結果不會出現任何不同或者任何問題,則該函數被稱為可重入函數,否則,是不可重入函數。 常見的線程不安全的情況不保護共享變量的函數函數狀態隨著被調用,狀態發生變化的函數返回指向靜態變量指針的函數調用線程不安全函數的函數常見的線程安全的情況每個線程對全局變量或者靜態變量只有讀取的權限,而沒有寫入的權限,一般來說這些線程是安全的類或者接口對于線程來說都是原子操作多個線程之間的切換不會導致該接口的執行結果存在二義性 常見不可重入的情況調用了malloc/free函數,因為malloc函數是用全局鏈表來管理堆的調用了標準I/O庫函數,標準I/O庫的很多實現都以不可重入的方式使用全局數據結構可重入函數體內使用了靜態的數據結構 常見可重入的情況不使用全局變量或靜態變量不使用用malloc或者new開辟出的空間不調用不可重入函數不返回靜態或全局數據,所有數據都有函數的調用者提供使用本地數據,或者通過制作全局數據的本地拷貝來保護全局數據 可重入與線程安全聯系函數是可重入的,那就是線程安全的函數是不可重入的,那就不能由多個線程使用,有可能引發線程安全問題如果一個函數中有全局變量,那么這個函數既不是線程安全也不是可重入的。 可重入與線程安全區別可重入函數是線程安全函數的一種線程安全不一定是可重入的,而可重入函數則一定是線程安全的。如果將對臨界資源的訪問加上鎖,則這個函數是線程安全的,但如果這個重入函數若鎖還未釋放則會產生死鎖,因此是不可重入的。 常見鎖概念死鎖
死鎖是指在一組進程中的各個進程均占有不會釋放的資源,但因互相申請被其他進程所站用不會釋放的資源而處于的一種永久等待狀態。
死鎖四個必要條件互斥條件:一個資源每次只能被一個執行流使用請求與保持條件:一個執行流因請求資源而阻塞時,對已獲得的資源保持不放不剝奪條件:一個執行流已獲得的資源,在末使用完之前,不能強行剝奪循環等待條件:若干執行流之間形成一種頭尾相接的循環等待資源的關系 避免死鎖破壞死鎖的四個必要條件加鎖順序一致避免鎖未釋放的場景資源一次性分配STL,智能指針和線程安全
STL中的容器是否是線程安全的? 不是. 原因是, STL 的設計初衷是將性能挖掘到極致, 而一旦涉及到加鎖保證線程安全, 會對性能造成巨大的影響. 而且對于不同的容器, 加鎖方式的不同, 性能可能也不同(例如hash表的鎖表和鎖桶). 因此 STL 默認不是線程安全. 如果需要在多線程環境下使用, 往往需要調用者自行保證線程安全
智能指針是否是線程安全的?
對于 unique_ptr, 由于只是在當前代碼塊范圍內生效, 因此不涉及線程安全問題. 對于 shared_ptr, 多個對象需要共用一個引用計數變量, 所以會存在線程安全問題. 但是標準庫實現的時候考慮到了這 個問題, 基于原子操作(CAS)的方式保證 shared_ptr 能夠高效, 原子的操作引用計數.
其他常見的各種鎖 最后:
十分感謝你可以耐著性子把它讀完和我可以堅持寫到這里,送幾句話,對你,也對我:
1.一個冷知識: 屏蔽力是一個人最頂級的能力,任何消耗你的人和事,多看一眼都是你的不對。
2.你不用變得很外向,內向挺好的,但需要你發言的時候,一定要勇敢。 正所謂:君子可內斂不可懦弱,面不公可起而論之。
3.成年人的世界,只篩選,不教育。
4.自律不是6點起床,7點準時學習,而是不管別人怎么說怎么看,你也會堅持去做,絕不打亂自己的節奏,是一種自我的恒心。
5.你開始炫耀自己,往往都是災難的開始,就像老子在《道德經》里寫到:光而不耀,靜水流深。
最后如果覺得我寫的還不錯,請不要忘記點贊?,收藏?,加關注?哦(??ω??)
愿我們一起加油,奔向更美好的未來,愿我們從懵懵懂懂的一枚菜鳥逐漸成為大佬。加油,為自己點贊!