C++ 基于多設計模式下的同步異步?志系統-2項目實現

?志系統框架設計

1.?志等級模塊:對輸出?志的等級進?劃分,以便于控制?志的輸出,并提供等級枚舉轉字符串功能。
? OFF:關閉
? DEBUG:調試,調試時的關鍵信息輸出。
? INFO:提?,普通的提?型?志信息。
? WARN:警告,不影響運?,但是需要注意?下的?志。
? ERROR:錯誤,程序運?出現錯誤的?志
? FATAL:致命,?般是代碼異常導致程序?法繼續推進運?的?志

2.?志消息模塊:中間存儲?志輸出所需的各項要素信息
? 時間:描述本條?志的輸出時間。
? 線程ID:描述本條?志是哪個線程輸出的。
? ?志等級:描述本條?志的等級。
? ?志數據:本條?志的有效載荷數據。
? ?志?件名:描述本條?志在哪個源碼?件中輸出的。
? ?志?號:描述本條?志在源碼?件的哪??輸出的。

3.?志消息格式化模塊:設置?志輸出格式,并提供對?志消息進?格式化功能。
? 系統的默認?志輸出格式:%d{%H:%M:%S}%T[%t]%T[%p]%T[%c]%T%f:%l%T%m%n
? -> 13:26:32 [2343223321] [FATAL] [root] main.c:76 套接字創建失敗\n
? %d{%H:%M:%S}:表??期時間,花括號中的內容表??期時間的格式。
? %T:表?制表符縮進。
? %t:表?線程ID
? %p:表??志級別
? %c:表??志器名稱,不同的開發組可以創建??的?志器進??志輸出,?組之間互不影響。
? %f:表??志輸出時的源代碼?件名。
? %l:表??志輸出時的源代碼?號。
? %m:表?給與的?志有效載荷數據
? %n:表?換?
? 設計思想:設計不同的?類,不同的?類從?志消息中取出不同的數據進?處理。

4.?志消息落地模塊:決定了?志的落地?向,可以是標準輸出,也可以是?志?件,

也可以滾動?件輸出....
? 標準輸出:表?將?志進?標準輸出的打印。
? ?志?件輸出:表?將?志寫?指定的?件末尾。
? 滾動?件輸出:當前以?件??進?控制,當?個?志?件??達到指定??,則切換下?個?件進?輸出
? 后期,也可以擴展遠程?志輸出,創建客?端,將?志消息發送給遠程的?志分析服務器。
? 設計思想:設計不同的?類,不同的?類控制不同的?志落地?向。

5.?志器模塊:
? 此模塊是對以上?個模塊的整合模塊
,??通過?志器進??志的輸出,有效降低??的使?難度。
? 包含有:?志消息落地模塊對象,?志消息格式化模塊對象,?志輸出等級

6.?志器管理模塊:
? 為了降低項?開發的?志耦合,不同的項?組可以有??的?志器來控制輸出格式以及落地?向,因此本項?是?個多?志器的?志系統。
? 管理模塊就是對創建的所有?志器進?統?管理。并提供?個默認?志器提供標準輸出的?志輸出。

7.異步線程模塊:
? 實現對?志的異步輸出功能,??只需要將輸出?志任務放?任務池,異步線程負責?志的落地輸出功能,以此提供更加?效的?阻塞?志輸出。

一.實用類設計

logs/util.hpp

Date類

static size_t getTime()獲取當前時間(靜態函數)

File類

1.判斷文件是否存在

struct stat st;

  • stat 是一個結構體(定義在 <sys/stat.h> 頭文件中)

  • 它會被用來存儲目標文件或目錄的各種信息

    • 比如文件大小、權限、類型(是否是目錄)、最后訪問時間等

int stat(const char *pathname, struct stat *statbuf)?是一個系統調用函數

????????獲取路徑 pathname 所指文件的信息,并存儲在 st 變量中

返回值含義
0成功,說明文件/目錄存在并可訪問 ?
0失敗,說明文件/目錄不存在或無權限訪問 ?

2.提取文件路徑

3.遞歸創建多級目錄

找路徑分割符,沒找到說明已經到最底層的目錄了,直接創建目標目錄。

找到了判斷是否存在該目錄,沒有就該目錄創建。

#include<iostream>
#include<ctime>
#include <sys/stat.h>
namespace mylog
{
namespace util
{//獲取時間class Date{public:static size_t getTime(){return (size_t)time(nullptr);}};class File{public://1.判斷文件是否存在static bool exists(const std::string &pathname){struct stat st;//stat(...) 的返回值為:== 0:說明文件存在 != 0:說明文件不存在或無權限訪問return stat(pathname.c_str(),&st)==0;}//2.獲取這個文件所處的路徑static std::string path(const std::string &pathname){//./dir1/dir2/a.txtsize_t pos=pathname.find_last_of("/\\");//查找"/" "\"(windows下路徑分割符)if(pos==std::string::npos) return ".";return pathname.substr(0,pos+1);}//3.在指定路徑下創建目錄static void createDiretory(const std::string &pathname){//./dir1/dir2/dir3 ../dir3//pos 找 / 的位置 idx查找的起始位置size_t pos=0,idx=0;while(pos<pathname.size()){pos=pathname.find_first_of("/\\",idx);//沒找到 到目標路徑下了 直接創建if(pos==std::string::npos){//創建目錄mkdir(pathname.c_str(),0777);break;}//找到了 判斷父目錄是否存在 dir1/else{idx=pos+1; //pos指向/ +1跳過///不存在就創建 pos+1帶上/if(exists(pathname.substr(0,pos+1))==false)mkdir(pathname.substr(0,pos+1).c_str(),0777);}}}};
}
}

二.日志等級類

logs/level.hpp

對輸出?志的等級進?劃分,以便于控制?志的輸出,并提供等級枚舉轉字符串功能。

#pragma once
namespace mylog
{class LogLevel{public:// 日志等級類,用于表示不同級別的日志輸出控制enum class value{UNKNOW = 0, // 未知等級DEBUG,      // 調試信息INFO,       // 正常運行的信息WARN,       // 警告ERROR,      // 錯誤OFF         // 關閉日志輸出};// 將日志等級枚舉值轉換為對應的字符串(便于打印輸出)static const char*toString(LogLevel::value level){switch (level){case LogLevel::value::DEBUG: return "DEBUG";case LogLevel::value::INFO: return "INFO";case LogLevel::value::WARN: return "WARN";case LogLevel::value::ERROR: return "ERROR";case LogLevel::value::OFF: return "OFF";}return "UNKNOW";}};}

三.日志消息類

message.hpp

字段名類型含義說明
_ctimesize_t日志創建的時間戳(秒),用于記錄日志生成的時刻
_levelLogLevel::value日志級別,例如 DEBUG/INFO/WARN/ERROR/OFF,用于日志過濾
_linesize_t日志語句所在的代碼行號(一般宏傳入 __LINE__
_tidstd::thread::id當前線程的 ID,支持多線程日志追蹤
_filestd::string文件名(一般傳入 __FILE__),幫助定位日志位置
_loggerstd::string日志器名稱(如 "root"、"async_logger"),區分多個 logger
_payloadstd::string實際日志內容(要輸出的文字)
在什么時間,那個組的日志器 哪個線程 哪個文件 具體在哪一行,什么等級的日志內容
#include<iostream>
#include<thread>
#include<string>
#include"level.hpp"
#include"util.hpp"namespace mylog
{struct LogMesg{size_t _ctime;//日志產生的時間戳LogLevel::value _level;//日志等級size_t _line;//行號std::thread::id _tid;//線程idstd::string _file;//文件名std::string _logger;//日志器名std::string _payload;//有效消息數據LogMesg(LogLevel::value level,size_t line,std::string file,std::string logger,std::string msg): _ctime(util::Date::getTime()),_level(level),_line(line),_tid(std::this_thread::get_id()),_file(file),_logger(logger),_payload(msg){}};}

四.?志輸出格式化類

format.hpp

按照用戶給的格式/默認格式,把LogMsg里面的信息格式化放到對應的流中。

1. FormatItem(抽象基類)

  • 抽象接口,定義日志格式子項的統一接口。

  • 子類會重寫 format(),輸出指定字段內容。

2. 各種子類(繼承自 FormatItem)

不同子類重寫format函數,從LogMsg中取出對應的字段的內容輸出到對應的out流中。

類名輸出內容LogMsg 來源字段格式
MsgFormatItem日志正文內容_payload%m
LevelFormatItem日志等級_level(轉為字符串)%p
TimeFormatItem時間戳,支持自定義格式_ctime%d{fmt}
FileFormatItem源文件名_file%f
LineFormatItem行號_line%l
ThreadFormatItem線程ID_tid%t
LoggerFormatItem日志器名稱_logger%c
TabFormatItem制表符 \t%T
NLineFormatItem換行符 \n%n
OtherFormatItem原始字符串構造傳入 _str% 開頭字符

1.消息正文字段

取出消息字段直接輸出到out流中

2.等級 調用LogLevel類中的靜態函數 把枚舉類value類型轉換為char*

?

3.時間 可以傳入字符串fmt初始化該子類,表示需要打印的時間格式。

localtime():非線程安全

time_t raw = time(nullptr);
struct tm* t = localtime(&raw);
  • 它返回的是一個 指向靜態內存區域的指針

  • 這塊靜態內存通常是函數內部的一個全局變量或 static 變量,在整個進程中只有一份共享的副本

  • 每次調用 localtime(),這個內部的 struct tm 都會被重寫

localtime_r():線程安全

time_t raw = time(nullptr);
struct tm t;
localtime_r(&raw, &t);

傳入用戶自己定義的 struct tm 變量,不會發生數據覆蓋問題。

?其它子類...

// 派生格式化子項子類 從msg中找到對應消息放入out流中// 1.有效消息class MsgFormatItem : public FormatItem{public:void format(std::ostream &out, const LogMsg &msg) override{out << msg._payload;}};// 2.等級class LevelFormatItem : public FormatItem{public:void format(std::ostream &out, const LogMsg &msg){out << LogLevel::toString(msg._level);}};// 3.時間 按照傳入的參數fmt格式化時間戳 默認為%H:%M:%Sclass TimeFormatItem : public FormatItem{public:TimeFormatItem(const std::string &fmt = "%H:%M:%S"): _time_fmt(fmt) {}void format(std::ostream &out, const LogMsg &msg) override{struct tm t;//(對比localtime返回的是一個內部共享的靜態指針,localtime_r線程安全)// 來把時間戳 time_t 轉換成本地時間 保存在t中localtime_r(&msg._ctime, &t);char tmp[32] = {0};// 把本地時間按照指定格式 格式化到tmp中strftime(tmp, sizeof(tmp) - 1, _time_fmt.c_str(), &t);out << tmp;}private:std::string _time_fmt; // 時間戳格式};// 4.文件名class FileFormatItem : public FormatItem{public:void format(std::ostream &out, const LogMsg &msg){out << msg._file;}};// 5.行號class LineFormatItem : public FormatItem{public:void format(std::ostream &out, const LogMsg &msg){out << msg._line;}};// 5.線程idclass ThreadFormatItem : public FormatItem{public:void format(std::ostream &out, const LogMsg &msg){out << msg._tid;}};// 6.日志器名class LoggerFormatItem : public FormatItem{public:void format(std::ostream &out, const LogMsg &msg){out << msg._logger;}};// 7.Tabclass TabFormatItem : public FormatItem{public:void format(std::ostream &out, const LogMsg &msg){out << "\t";}};// 8.換行class NLineFormatItem : public FormatItem{public:void format(std::ostream &out, const LogMsg &msg){out << "\n";}};// 9.其它 asda[] 直接放入到out中class OtherFormatItem : public FormatItem{public:OtherFormatItem(const std::string &str): _str(str) {}void format(std::ostream &out, const LogMsg &msg){out << _str;}private:std::string _str;};

3. Formatter 類(格式化核心)

這個類完成的功能就是,根據用戶指定的格式,格式化消息輸出到指定的流中。

比如說用戶傳入的格式是"dasd{}[%%[%d{%H:%M:%S}[%t][%c][%f:%l][%p]%T%m%n]"

先對格式化字符串的字符進行分類:
1.dasd{}[ 屬于原始字符

2.%% 表示轉義% 屬于原始字符

3.%d d屬于格式化字符

4.格式化字符后面的{%H:%M:%S} 屬于格式化字符的子格式(“{}”也屬于)?

1.原始字符 就保持不動 (為了統一處理 原始字符串也是調用FormatItem子類輸出到流中)

2.格式化字符調用對應的FormatItem子類從LogMsg中取出對應的字段輸出到流中。

字段的輸出順序就是用戶傳入的格式從左向右的順序,我們可以用一個vecotr<FormatItem::ptr>按順序保存需要調用的子類,再遍歷vecotr數組完成格式化。

1.構造函數

用戶傳入 格式化規則字符串 初始化_pattern

并完成字符串解析 assert()強斷言

2.createrItem()根據不同的格式化字符?創建不同的子類對象

key格式化字符 val其子格式({ }以及里面的字符串)? 或者key=="" val代表原始字符[ ]adc

3.bool parsePattern()?對格式化字符串進行解析,將需要調用的子類保存到vector數組中

流程:

輸入字符串:  [%d{%H:%M:%S}][%p]%T%m%nHello【第一階段】解析成:("", "[")("d", "%H:%M:%S")("", "][")("p", "")("T", "")("m", "")("n", "")("", "Hello")【第二階段】根據 key 構建對應的 FormatItem 子類

  • fmt_order: 暫存格式化字符(key)與其子格式(val)的列表

  • key: 當前格式化符號(如 d

  • val: 當前格式化符的子格式(如 %H:%M:%S),或原始非格式化文本

  • pos: 當前掃描位置

1.非 % 字符 → 原始字符原樣收集?("", "[") key=="" val+=[

2.%% → 視為轉義的 %當作原始字符原樣收集?("", "%")

3.%x → %后面是格式化字符?

????????1.如果val中保存有原始字符 就先放入數組vector中,并clear()為后面保存格式化字符的子格式(如 %H:%M:%S)作準備。

????????2.給key賦值 保存當前的轉義字符是什么

????????原始字符后面對應的子類

4.判斷格式化字符是否有 {} 子格式 eg.%d{%H:%M:%S} → key = d,val = {%H:%M:%S}

如果找到最后都沒找到與之匹配的 } 說明子規則{}匹配錯誤 返回false

注意此時{ }也被保存到了val中,但這并不影響后面格式的輸出,后面調用對應子類,fmt=val,向流中輸出時也會帶上{ }。所以前面說格式化字符后面的“{}”也屬于子格式,不當作原始字符處理。

5.保存解析結果?解析完一組 %x{子格式} 或 %x 后,放入 fmt_order

6.字符串解析完 生成格式化項對象?

每組 key/val 通過 createrItem() 構建出具體的 FormatItem 派生類實例,如:

  • %d{}TimeFormatItem

  • %pLevelFormatItem

  • %mMsgFormatItem

  • "["OtherFormatItem

示例分析:[%d{%H:%M:%S}][%p]%T%m%nHello

Step1:fmt_order 內容

[("", "["),("d", "%H:%M:%S"),("", "]["),("p", ""),("T", ""),("m", ""),("n", ""),("", "Hello")
]

Step2:生成 _items 內容

[OtherFormatItem("["),TimeFormatItem("%H:%M:%S"),OtherFormatItem("]["),LevelFormatItem(),TabFormatItem(),MsgFormatItem(),NLineFormatItem(),OtherFormatItem("Hello")
]

4.format()

逐個遍歷 _items(每個 item 是 FormatItem 的子類,如 TimeFormatItem、MsgFormatItem 等),每個 item 都負責從 LogMsg 提取對應的信息并寫入 out。

五.?志落地(LogSink)類設計(簡單??模式)

sink.hpp

把日志“落地”(寫入)的位置抽象出來,使得用戶可以靈活指定日志寫到哪里(控制臺?文件?滾動文件?)。同時使用簡單工廠模式簡化使用方式,提升靈活性與擴展性。

1.LogSink 抽象類設計

  • 定義日志落地的統一接口:只需要實現 log() 方法即可。

  • 所有具體的日志落地方式都繼承自它,符合面向接口編程原則。

  • 使用 shared_ptr 管理對象生命周期,便于在異步或多線程中使用。

2.三種落地方式的實現

1. 控制臺輸出:StdoutSink

2. 固定文件輸出:FileSink

1.初始化傳入目錄路徑+文件名? ? ? ?

createDirectory(path()) path()取出目錄路徑再進行遞歸創建 ? createDirectory確保目錄路徑存在。open()再在對應路徑下創建指定文件名(如果不存在)并打開。

3. 按大小滾動輸出:RollBySizeSink

  • 文件過大自動滾動,新建文件。

  • 使用時間戳 + 自增后綴保證文件名不重復。

怎么判斷需新建文件?

用_cur_fsize記錄當前文件的大小,如果超過限制的最大文件大小就新建,注意更新_cur_fsize=0,以及關閉原文件寫。

怎么確保新建的文件名不重復?

文件名=base文件名+時間戳(精確到秒)+自增數(新建一個文件++)

這樣即使一秒創建兩個文件,也不用擔心會重復。

  //落地方向: 滾動文件(以大小進行滾動)class RollBySizeSink: public LogSink{public:RollBySizeSink(const std::string &basename,const size_t max_size): _basename(basename),_max_fsize(max_size),_cur_fsize(0),_name_count(0){//獲取文件所處的路徑+文件名std::string pathname=createNewFile();// 1.遞歸創建文件所在目錄util::File::createDirectory(util::File::path(pathname));// 2.創建并打開文件_ofs.open(pathname, std::ios::binary | std::ios::app);assert(_ofs.is_open()); // 保證打開}//將日志消息寫到滾動文件中void log(const char* data,size_t len){//超出大小 新建文件if(_cur_fsize>=_max_fsize){//一定要先關閉原文件 防止資源泄漏_ofs.close();_cur_fsize=0;_ofs.open(createNewFile(),std::ios::binary|std::ios::app);assert(_ofs.is_open()); }_ofs.write(data,len);assert(_ofs.good());_cur_fsize+=len;}private://獲取新文件名std::string createNewFile(){//獲取以時間生成的文件名time_t t=util::Date::now();struct tm lt;localtime_r(&t,&lt);std::stringstream ss;ss<<_basename;ss<<lt.tm_year+1900;ss<<lt.tm_mon+1 ;ss<<lt.tm_mday;ss<<lt.tm_hour;ss<<lt.tm_min;ss<<lt.tm_sec;ss<<'-';ss<<_name_count++;ss<<".log";return ss.str();}private://文件名=基礎文件名+擴展文件名(以時間生成) std::string _basename;//./logs/base-20250421203801 準確到秒size_t _name_count;//防止一秒內生成的文件名重復std::ofstream _ofs;size_t _max_fsize;//文件最大大小size_t _cur_fsize;//當前文件大小 };

4. 按時間滾動輸出:RollByTimeSink

  • 枚舉類 TimeGap 表示間隔(秒、分、小時、天)。

  • 日志會按時間粒度自動切分,比如每分鐘一個文件。

  • 比大小滾動更適合做按時歸檔(日志分析、ELK 系統對接等)。

怎么判斷需要新建文件?

我們是根據時間段進行劃分文件的,比如說我們以 1 分鐘進行劃分,時間段的大小就是60秒,time(NULL)/60 算出來當前時間戳屬于第幾個時間段。初始化時先保存當前時間戳屬于第幾個時間段,每次寫入時再判斷時間段是不是變化了?變化了就,新建并更新當前保存的時間段。

定義一個枚舉類來表示 一個時間段的大小

  //時間間隔 枚舉類enum class TimeGap{GAP_SECOND=1,GAP_MINUTE=60,GAP_HOUR=3600,GAP_DAY=3600*24};//落地方向: 滾動文件(以時間為間隔進行滾動)class RollByTimeSink: public LogSink{public:RollByTimeSink(const std::string &basename,const TimeGap gap_type): _basename(basename),_gap_type((size_t)gap_type){//獲取文件所處的路徑+文件名std::string pathname=createNewFile();// 1.遞歸創建文件所在目錄util::File::createDirectory(util::File::path(pathname));_cur_gap=(time(NULL)/_gap_type);//獲取當前是第幾個時間段// 2.創建并打開文件_ofs.open(pathname, std::ios::binary | std::ios::app);assert(_ofs.is_open()); // 保證打開}//將日志消息寫到滾動文件中void log(const char* data,size_t len){//出現新的時間段size_t new_gap=((time(NULL)/_gap_type));if(_cur_gap!=new_gap){//一定要先關閉原文件 防止資源泄漏_ofs.close();_cur_gap=new_gap;//更新當前時間段_ofs.open(createNewFile(),std::ios::binary|std::ios::app);assert(_ofs.is_open()); }_ofs.write(data,len);assert(_ofs.good());}private://獲取新文件名std::string createNewFile(){//獲取以時間生成的文件名time_t t=util::Date::now();struct tm lt;localtime_r(&t,&lt);std::stringstream ss;ss<<_basename;ss<<lt.tm_year+1900;ss<<lt.tm_mon+1 ;ss<<lt.tm_mday;ss<<lt.tm_hour;ss<<lt.tm_min;ss<<lt.tm_sec;ss<<".log";return ss.str();}private:std::string _basename;std::ofstream _ofs;size_t _gap_type;//時間段大小size_t _cur_gap;//當前是第幾個時間段};

3.簡單工廠類 SinkFactory

  • 利用函數模板和完美轉發創建任意 LogSink 子類對象。

  • 解耦日志使用者與具體實現,符合開放封閉原則

類中定義一個靜態的模板函數,不要寫成模板類,因為可變參數是給create函數的,不是給類的。

六.?志器類(Logger)設計(建造者模式)

logger.hpp

?志器主要是?來和前端交互, 當我們需要使??志系統打印log的時候, 只需要創建Logger對象,調?該對象debug、info、warn、error、fatal等?法輸出??想打印的?志即可,?持解析可變參數列表和輸出格式, 即可以做到像使?printf函數?樣打印?志。
當前?志系統?持同步?志 & 異步?志兩種模式,兩個不同的?志器唯?不同的地?在于他們在?志的落地?式上有所不同:
同步?志器:直接對?志消息進?輸出。
異步?志器:將?志消息放?緩沖區,由異步線程進?輸出。
因此?志器類在設計的時候先設計出?個Logger基類,在Logger基類的基礎上,繼承出SyncLogger同步?志器和AsyncLogger異步?志器
且因為?志器模塊是對前邊多個模塊的整合,想要創建?個?志器,需要設置?志器名稱,設置?志輸出等級,設置?志器類型,設置?志輸出格式,設置落地?向,且落地?向有可能存在多個,整個?志器的創建過程較為復雜,為了保持良好的代碼?格,編寫出優雅的代碼,因此?志器的創建這?采?了建造者模式來進?創建。

1.Logger類

Logger 類主要負責記錄日志消息并將其輸出到指定的目標(如文件、控制臺)。其構造函數接收日志名稱、日志級別、格式化器以及落地方向(LogSink):

每次我們調用Logger里面函數進行日志輸出時,要判斷當前傳入的日志是否>=限制的日志等級,只有>=才能進行日志輸出。

因此我們保證對日志等級_limit_level的訪問操作必須是原子性的,不能在訪問的過程中被其它線程進行修改。

怎么保證對該變量的操作是原子性的?

std::atomic 可以應用于不同的基本類型,如整數、指針、布爾值等。它的作用是提供一種方式來保證對這些類型的訪問是 線程安全的,不需要顯式的互斥鎖。

1.構造函數

2.日志記錄方法

Logger 類中定義了多個日志記錄方法:debuginfowarnerrorfatal,它們接收文件名、行號、格式化字符串和可變參數。所有這些方法都遵循相似的邏輯:

  1. 檢查日志級別:首先判斷當前日志級別是否符合輸出條件,如果不符合則直接返回,不進行日志記錄。

  2. 格式化日志消息:使用 vasprintf 將可變參數格式化成日志消息字符串。

  3. 調用 serialize 方法serialize 方法將格式化后的日志消息封裝成 LogMsg 對象,然后通過指定的格式化器對消息進行格式化,并最終輸出到日志目標。

eg.debug等級日志輸出

3.具體向哪里輸出 log() 由繼承的子類日志器(同步 異步)完成

1.SyncLogger 同步日志器類

根據傳入的參數初始化Logger日志器

1.先保證落地方向存在

2.遍歷落地方向 一個一個打印日志進行輸出

2.AsyncLogger異步日志器類

繼承 Logger,重寫了 log() 方法,實現了異步寫入。

1.構造

  • 創建了異步線程對象 _looper,傳入一個回調 realLog()

  • 當異步線程從緩沖區中取出日志后,會自動調用 realLog(buf) 寫入文件。

2.log 只向緩沖區中寫入數據

  • 主線程只寫入緩沖區(非阻塞、線程安全);

  • 具體向哪里?I/O 寫入交由 AsyncLooper 在線程中處理。

3.realLog?

異步線程中處理緩沖區數據的具體邏輯,將內存緩沖區中的日志數據寫入到所有配置的落地目標中

2.LoggerBuilder 類(建造者模式)

使用建造者模式來構造日志器 不讓用戶一個個構造成員變量再構造日志器

1.抽象一個日志器建造者類 (完成日志器對象所需零部件的構建&&日志器的構建)

? ? ? ? 1.設置日志器類型(異步 同步)

? ? ? ? 2.將不同的日志器的創建放到同一個日志器構建者類中完成

2.派生出具體的構造者類 ?局部日志器的構造者&全局的日志器構造者

構建對應成員遍歷的build__函數

在LoggerBuiler進行初始化時完成對日志器類型 日志限制等級的默認構造 異步線程緩沖區的策略(緩沖區大小是否固定,默認固定)

具體創建Logger日志器并返回的build函數,由其子類完成。

1.LocalLoggerBuiler 局部(本地)日志器類

日志器名稱必須有,格式化操作 落地方向可以給默認值

使用方法:

2.GlobalLoggerBuilder全局日志器類

全局日志器其實就是用單例對象管理的局部日志器。單例對象延長了日志器的生命周期,通過獲取單例對象查找里面對應的日志器,進行操作。

關鍵詞含義
局部日志器是指通過 LoggerBuilder(尤其是 LocalLoggerBuilder)手動創建、管理的日志器實例
全局日志器是指通過 GlobalLoggerBuilder 創建,并自動注冊到單例 LoggerManager 中的日志器
單例對象LoggerManager 是懶漢模式的全局單例,統一管理所有日志器,提供注冊/查找接口
本質所有日志器(無論本地創建或全局注冊)最終其實都是 Logger 實例,只是有沒有放入 LoggerManager_loggers 容器里
LoggerManager日志器管理類 (懶漢模式)
項目說明
類型單例類(懶漢式,局部靜態變量)
主要作用統一管理所有日志器,包括 root 日志器和自定義日志器
核心功能創建默認 root 日志器、添加日志器、查詢日志器、獲取日志器
線程安全性采用 std::mutex 加鎖保護 _loggers 容器

構造函數

  • LoggerManager 構造時,創建了一個 root 日志器。

  • 使用 LocalLoggerBuilder,避免遞歸調用(GlobalLoggerBuilder里面又會構造LoggerManager,導致遞歸調用)。

  • 直接 insert 到 _loggers,保證程序最初始至少有一個可用日志器。

static LoggerManager& getInstance()

  • 采用 C++11 之后線程安全的局部靜態變量初始化機制

  • 懶漢模式(第一次用到時再初始化)

  • 線程安全,不會因為多線程導致多次創建


addLogger(Logger::ptr& logger)

  • 加鎖保護 _loggers

  • 將 logger 插入 _loggers 映射表中

  • 注意:因為在持鎖狀態下又調用了 hasLogger(),原來存在死鎖風險,所以注釋掉了 hasLogger()調用,改為直接 insert


hasLogger(const std::string& name)

  • 單獨加鎖判斷 _loggers 中是否存在某名字

  • 注意:如果在 addLogger 內部調用,需要避免加鎖兩次問題(最好解耦鎖邏輯)


getLogger(const std::string& name)

  • 加鎖安全地查詢并返回 logger

  • 如果找不到,返回空指針 Logger::ptr()


rootLogger()

  • 返回默認的 root 日志器

  • root 是程序啟動時創建的,名字為 "root"

GlobalLoggerBuilder

項目說明
類型日志器構建器(Builder模式)
主要作用幫助用戶構建自定義日志器,并自動注冊到 LoggerManager
特點build()后不僅返回日志器,還自動 addLogger
線程安全性依賴 LoggerManager 內部加鎖

Logger::ptr build() override

  • 校驗日志器名字不為空

  • 如果沒有設置 formatter,默認使用一個新建 formatter

  • 如果沒有設置 sinks,默認加一個 StdoutSink

  • 根據同步/異步選擇創建 SyncLogger 或 AsyncLogger

  • 構建完成后,注冊到 LoggerManager 單例中

  • 返回 logger 指針,方便外部繼續操作

七.異步工作器設計

1. 為什么要異步輸出日志消息?

問題:

  • 同步輸出(例如 send()write())一旦對端或磁盤緩沖區滿了會阻塞主線程。

  • 頻繁系統調用開銷大,影響主線程性能。

解決:

  • 業務線程僅負責將日志寫入內存緩沖區(生產者角色)。

  • 另有專屬異步線程負責將日志落地(寫文件、send到網絡等),主線程立刻返回,不阻塞。

  • 避免主線程陷入IO,提升系統吞吐量與響應能力。

通常一個日志器對應一個異步處理線程,再多反而浪費系統資源(尤其CPU與上下文切換成本)。

2. 緩沖區存儲結構用什么?

隊列,因為先進入的消息要先處理.

3. 每次寫入/讀取都申請釋放內存效率太低?

?問題:

  • new / delete 太頻繁,容易導致內存碎片與系統開銷。

?解決:

  • 提前申請一整塊連續內存,作為環形緩沖區或雙緩沖區的底層存儲空間。

  • _read_index_write_index 控制寫入/讀取位置,復用空間而不頻繁分配

4.業務線程寫數據相當于生產者 異步處理線程取數據相當于消費者,寫數據取數據每次進入緩沖區都需要加鎖,太過于頻繁怎么辦? 先分析一下,生產者會有多個線程 而消費者一般一個日志器對應一個,所以主要是生產者和生產者 生產者和消費者沖突.

這樣 我們采用雙緩沖區的方案,生產者 消費者各一個緩沖區,每當消費者把消費者緩沖區的數據消費完 且生產者緩沖區內有數據 就交換兩個緩沖區。就減少了生產者和消費者的鎖沖突。

5.我們在緩存區存儲的是一個個日志消息結構體 LogMsg嗎?不這樣頻繁創建和析構LogMsg會,降低效率,我們在緩沖區存入的是格式化的日志字符串,這樣不用new delete LogMsg對象,而且異步線程一次性把緩沖區的多條日志消息落地減少write次數。

傳統方式:LogMsg 結構體

直接格式化字符串

每條日志需要創建 LogMsg 對象

每條日志直接格式化為字符串

內存頻繁 new/delete 造成碎片

寫入緩沖區是連續內存操作

異步線程還需重新 format 后輸出

異步線程直接寫入文件,無需處理

每條日志都需一次 write()

可一次性 write 多條,提高吞吐量

buffer.hpp

Buffer 類

目的: 在內存中維護一塊連續的日志寫入緩沖區,支持動態擴容、雙緩沖交換、快速讀寫操作,并為異步日志器提供數據中轉

+-------------------------------+
|....已讀....|....待讀....|....可寫....|
0          _reader      _writer      _buffer.size()

生產者從_writer_idx位置寫入到內存中

消費者從_reader_idx位置讀取并寫入到文件中

當_reader_idx==_writer_idx時 說明已經把緩沖區的數據都寫入文件,之后就交換緩沖區繼續處理

1.構造函數

  • 默認創建一個 1MB 的緩沖區

  • 使用 std::vector<char> 管理內存,避免裸指針和手動 new/delete

2.push()? 生產者寫入內存

  • 調用 ensureEnoughSize() 確保空間夠用(如不夠則擴容)。

  • std::copy 進行內存拷貝(性能高于 memcpy 在泛型容器中)。

  • 更新 _writer_idx 寫指針。

buffer只考慮擴容,緩沖區大小是否固定由上層進行控制,空間不夠上層就會阻塞,不夠還不阻塞說明就需要擴容。

  • 設定閾值 10MB

    • 小于時采用倍增擴容:性能高、增長快。

    • 大于時改為線性擴容:防止內存爆炸。

  • 總會額外加上 len,確保本次寫入不會失敗。

3.writeAbleSize() 獲取還有多少空間給生產者寫入

  • 返回當前緩沖區還剩多少空間可以寫。

  • 在異步日志中用于判斷是否“生產者需要阻塞等待”。

4.readAbleSize() 獲取還剩多少數據給消費者處理

  • 返回還未消費的數據長度。

  • 被消費者線程用于“一次性取出所有待寫日志數據”。

5.begin() 獲取數據處理的起始地址給消費者

6.moveReader(size_t len)

消費者從緩沖區中讀了多少數據,可讀指針就向后面偏移多少。但確保不能超過可寫指針的位置

7.moveWrite(size_t len)

同理生產者向緩沖區寫了多少數據 可寫指針就向后面偏移多少,不能超出緩沖區大小。

8.reset() 重置緩沖區

  • 表示消費完數據后,清空整個緩沖區,準備下次復用。

  • 重要特性:不重新分配內存,只是重置兩個指針極大減少內存抖動

9.swap()?

消費者處理完數據 并且生產者緩沖區中有數據才進行交換緩沖區

9.empty()

Buffer 是一個高性能、支持自動擴容的環形日志緩沖區,結合 read/write 指針操作和雙緩沖技術,能極大降低內存申請與鎖粒度,是異步日志系統中極其重要的性能核心模塊。

looper.hpp

AsyncLooper類

1.構造函數

傳入處理日志消息的回調函數cb 以及緩沖區的策略模式

并設置線程的入口函數啟動線程

  • 創建時立即啟動工作線程,由 threadEntry() 開始處理緩沖區數據。

  • 線程通過回調函數處理日志內容,完全解耦主邏輯和落地邏輯。

ASYNC_SAFE 安全策略 緩沖區大小固定,空間不夠時生產者會wait阻塞直到可寫入

ASYNC_UNSAFE 非安全策略 緩沖區可擴容 ,空間不夠時會擴容寫入不阻塞

2.stop():安全終止線程

_thread.join等待異步線程處理完數據再退出,。沒有它,異步線程可能中途被殺,數據丟失,資源泄漏。

  • 必須喚醒消費者線程(可能正 wait()阻塞),否則線程可能掛死。

  • 退出條件為 _stop == true && _pro_buf.empty(),確保剩余數據處理完。

3.push():生產者寫入緩沖區

  • 加鎖保護 _pro_buf,確保線程安全。

  • 如果是安全模式(ASYNC_SAFE),寫不下就阻塞等待消費者釋放空間,直到可以寫入。

  • 寫入完成后 notify_one() 喚醒消費線程處理。

4.threadEntry(): 消費者線程主循環

步驟動作
1??等待 _pro_buf 有數據,或者收到 _stop 信號
2??如果滿足退出條件(且沒有殘留數據)→ break
3??否則交換緩沖區:_pro_buf_con_buf,如果是安全策略 喚醒可能阻塞住的生產者
4??解鎖后執行 _callBack(_con_buf)?把內存數據寫入文件
5??最后 reset() 清空消費緩沖區

異步線程的退出時機設計

第一次編寫時,當我選擇向顯示器打印日志,按理來說while()循環會打印1000條fatal等級的日志。但為什么只打印了460條就終止了呢?

因為我一開始寫的時候,異步處理線程中收到終止信號就直接break,打破循環,此時處理完消費者緩沖區的數據就直接退出了,但此時生產者緩沖區的數據并沒有swap處理完,進而導致了數據沒有處理完全。

我用 join() 保證主線程等待異步線程結束再退出,但日志還是只打了一半,最后發現是線程收到 stop() 后立刻退出,后面只處理完了消費者緩沖區的數據,沒處理完生產者緩沖區的數據,所以把while循環的退出條件再加上消費者緩沖區為空才解決。

八.日志系統的全局接口和宏封裝

九.性能測試

#include "../logs/mylog.h"
#include <chrono>namespace mylog
{//1.線程名稱 2.線程個數 3.日志條數 4.一條日志大小void bench(const std::string &logger_name,size_t thr_count,size_t msg_count,size_t msg_len){//1.獲取日志器mylog::Logger::ptr logger=mylog::getLogger(logger_name);if(logger.get()==nullptr)return;std::cout<<"測試日志:"<<msg_count<<" 條,總大小:"<<(msg_count*msg_len)/1024<<"KB\n";//2.組織指定長度的日志消息std::string msg(msg_len-1,'A');// \n占一個字節//3.創建指定數量的線程std::vector<std::thread> threads;std::vector<double> cost_arry(thr_count); //每個線程的寫日志的時間size_t msg_per_thr=msg_count/thr_count; //每個線程平均要寫的日志條數for(int i=0;i<thr_count;i++){//i按值捕獲 不引用(每個線程保存自己的i)threads.emplace_back([&,i](){//4.線程函數內部開始計時auto start=std::chrono::high_resolution_clock::now();//5.開始循環寫日志for(int j=0;j<msg_per_thr;j++)logger->fatal("%s",msg.c_str());//6.結束計時auto end=std::chrono::high_resolution_clock::now();std::chrono::duration<double> cost=end-start;cost_arry[i]=cost.count();//.count得到時間長度(單位秒)std::cout<<"\t線程"<<i<<":\t輸出數量"<<msg_per_thr<<"耗時:"<< cost_arry[i]<<"s\n";});}//等待所有線程退出for(int i=0;i<thr_count;i++){threads[i].join();}//7.計算總時間 (因為線程并行 所有總時間為最長的線程運行時間)double max_cost=0;for(int i=0;i<thr_count;i++)max_cost=max_cost>cost_arry[i]?max_cost:cost_arry[i];//每秒輸出日志數=總條數/總時間size_t msg_per_sec=msg_count/max_cost;//每秒輸出日志大小=總大小/(總時間*1024 ) 單位KBsize_t size_per_sec=(msg_count*msg_len)/(max_cost*1024);//8.進行輸出打印std::cout<<"\t總耗時"<<max_cost<<"s"<<std::endl;std::cout<<"\t每秒輸出日志數量"<<msg_per_sec<<" 條"<<std::endl;std::cout<<"\t每秒輸出日志大小"<<size_per_sec<<" KB"<<std::endl;}//同步void sync_bench(){std::unique_ptr<mylog::LoggerBuilder> builder(new mylog::GlobalLoggerBuilder());builder->buildLoggerName("sync_logger");builder->buildFormatter("%m%n");builder->buildLoggerType(mylog::LoggerType::LOGGER_SYNC);builder->buildSink<mylog::FileSink>("./logfile/sync.log");builder->build();bench("sync_logger",16,200000,1024*16);}//異步void async_bench(){std::unique_ptr<mylog::LoggerBuilder> builder(new mylog::GlobalLoggerBuilder());builder->buildLoggerName("async_logger");builder->buildFormatter("%m%n");builder->buildEnaleUnSafeAsync();builder->buildLoggerType(mylog::LoggerType::LOGGER_ASYNC);builder->buildSink<mylog::FileSink>("./logfile/async.log");builder->build();bench("async_logger",8,200000,1024*10);}}
int main()
{mylog::async_bench();return 0;
}

同步寫入磁盤的過程

在開始前我們先了解一下同步模式下,日志數據寫入磁盤的全過程。

1.程序格式化日志內容(用戶態)

先把日志內容組織好,變成一塊連續的內存數據

2.調用 write() 系統調用

這時候,程序要做一件重要的事:

  • 從用戶態切換到內核態(陷入系統內核)

  • 調用內核的 sys_write 系統調用

3.數據寫入內核緩沖區(Page Cache)

注意:向內核緩沖區寫完就返回了,繼續執行。后面是Linux后臺異步寫回線程 完成阻塞并刷新到磁盤的過程。日志線程不會卡在等待flush磁盤上!(只有你顯式調用fsync(),線程才會因為刷新磁盤而阻塞

  • 內核接收到 write 請求,不是直接寫磁盤!

  • 它首先把數據寫到Page Cache,也就是內核管理的一塊內存緩存區

4.Page Cache 決定什么時候真正寫磁盤

內核什么時候把 Page Cache 里的內容同步到磁盤呢?

  • 緩沖區滿了(比如寫入太多數據)

  • 過了一定時間(定時flush)(比如默認5秒一次)

  • 用戶調用 fsync() 強制刷盤

  • 系統負載很低,后臺自動同步

?真正觸發刷盤時,內核才會:

  • 把緩存中的數據,提交給磁盤驅動

  • 磁盤控制器接收數據,最終物理寫入磁盤

細節解釋
write() 返回了,是不是代表數據已經寫到磁盤?不是!只是到了內核Page Cache里,真正落盤可能還要等一段時間
write() 過程慢不慢?通常快(因為只是內存拷貝),除非Page Cache滿了或I/O很忙
真正慢的是哪一步?Page Cache flush到磁盤時才真正慢,但通常不是同步日志線程在等待
調用fsync()會怎樣?強制刷新Page Cache到磁盤,非常慢(阻塞)

單線程同步vs多線程同步

在同步模式下,我們一般會選擇單線程,因為多線程會出現鎖沖突導致效率下降。

但在我的2核4G服務器測試中,發現多線程反而比單線程更快

1.單線程同步

2.多線程同步

接下來我們進行原因分析,為什么同步模式下多線程有鎖沖突還是比單線程快?

簡單來說:多線程充分利用CPU提高的效率大,且鎖沖突降低的效率低

1. 單條日志很小

  • 每條日志體積只有幾十到一百字節。

  • write()寫入過程極短,鎖持有時間非常短。

  • 所以即使多線程競爭鎖,每次持鎖時間很快釋放,鎖沖突不明顯


2. 總日志數據量小

  • 總寫入數據量只有幾十MB到100MB左右。

  • 內核Page Cache能完全hold住所有數據。

  • 向磁盤真正flush的次數很少(內核異步回寫)(這個過程也需要加鎖)

  • 沒有真正暴露磁盤I/O延遲系統調用 write() 只拷貝到內存,很快返回。


3. 多線程數量適中

  • 只開了2~4個線程,并未遠遠超出CPU核心數(2核)。

  • 多線程合理分攤到不同CPU核上執行,CPU利用率提升

  • 并行執行帶來的加速效果,大于鎖競爭導致的損失。

鎖沖突分類:

類型解釋特點
鎖持有時間長型沖突(Lock Holding Contention)一個線程拿著鎖很久,其他線程只能苦等比如一次write操作太慢,鎖持有時間過長
鎖等待排隊型沖突(Lock Waiting Contention)很多線程搶鎖,排隊搶占,雖然每次持鎖很短比如多線程短寫日志,鎖很快釋放,但搶鎖的人太多
反過來我們也可以從這三點入手,1.增加單條日志大小 2.增加日志總量 3.增加線程數量
方法目的
① 增加單條日志大小加重單次write開銷
② 增加日志總量提高Page Cache壓力、增加flush次數
③ 增加線程數量提高鎖競爭和CPU切換開銷

總結:起到兩個方面的作用

1.增加鎖沖突

? ? ? ? 1.增加鎖持有時間 1.增加單條日志大小 write()寫入內核緩沖區速度下降。2.日志總量增加,增加write()寫入緩沖區阻塞的概率,以及增加緩沖區數據向磁盤刷新的次數。

? ? ? ? 2.增加線程排隊時間,增加線程數量 線程搶鎖排隊,等待時間變長,整體吞吐下降。

2.增加CPU切換開銷 (降低CPU利用率)

? ? ? ? 增加線程,因為CPU輪詢機制,每個線程都會被調用且運行一段時間換下一個。致CPU在不同線程之間頻繁切換,浪費大量CPU時間,總耗時增加,吞吐下降。

1.單線程

2.多線程

項目單線程同步日志多線程同步日志(16線程)
總日志條數200,000條200,000條
每條大小16KB16KB
總數據量3.2GB3.2GB
總耗時24.5038秒25.042秒
每秒輸出條數8162條/s7986條/s
每秒輸出日志大小130592 KB/s127785 KB/s
有的線程17秒就輸出完了,有的線程24秒多才完成。 為什么同步多線程測試中,不同線程完成時間差很多?
  • 多線程同步日志,大家寫日志都要搶一把鎖(通常是std::mutex保護的)。

  • std::mutex在Linux底層是非公平鎖(搶到就用,不保證排隊順序)。

  • 結果就是:

    • 某些線程運氣好,連續搶到鎖,瘋狂輸出

    • 某些線程運氣差,總在鎖外苦等,一直排隊

原因現象影響
鎖搶占不公平有的線程連續拿鎖,有的線程苦等導致完成時間天差地別
CPU調度不均某些線程搶到CPU多,跑得快執行速率不同
Page Cache刷盤堵塞后期線程write變慢后期線程完成時間普遍更長

異步寫入磁盤的過程

1. 【主線程】格式化日志內容

2.【主線程】push日志到異步緩沖區

  • log()函數內部做的事情:

    1. 加鎖(保護緩沖區,通常是std::mutex

    2. 把日志數據拷貝到生產緩沖區(內存區域)

    3. 解鎖

    4. 條件變量 notify_one 通知異步線程:有新日志來了

push動作只涉及:

  • 加鎖保護

  • 內存拷貝(拷貝到內部緩沖區)

  • 通知后臺線程

  • 沒有系統調用(沒有write()

push很快完成,主線程立刻繼續跑業務,不受I/O影響。

3. 【異步線程】被喚醒

4. 【異步線程】交換緩沖區

5. 【異步線程】處理消費緩沖區數據

????????這一步才真正發生了系統調用(write)

6. 【內核】處理write動作

7. 【異步線程】處理完成,繼續睡眠等待下一波日志

所以說異步日志,就是讓異步線程完成耗費時間多的write(),但為了讓異步線程獲取到數據,還得再建一個緩沖區,多一步拷貝到緩沖區的內容。對比同步,異步主線程相當于把write()換成了一次push拷貝(以及其它的細節開銷 比如說緩沖區交換時會加鎖 喚醒線程的系統調用notify等)。

對比同步模式,可以理解為:

  • 同步日志主線程需要:

    • 格式化 + write()(系統調用,可能慢)

  • 異步日志主線程需要:

    • 格式化 + push拷貝 + notify異步線程(全在用戶態完成,極快)

? 異步日志相當于把主線程的 write() 開銷換成了一次輕量級的 push拷貝
? 再加上一些很輕的鎖和notify開銷。

單線程同步vs單線程異步

如果需要調用的write()次數很少,那么單線程異步 同步差距不明顯,但需要頻繁調用write()才能處理完數據,還是異步更快一點 。

單線程異步vs多線程異步

異步模式下 單線程和多線程對比,和同步模式一樣,異步模式下多線程也會出現鎖競爭,但不用自己調用write() push寫入buffer緩沖區不夠會自動擴容不會阻塞住,push寫入速度很快,導致鎖競爭并不大 只有在push寫入時加鎖,速度很快。

多線程異步最主要的優勢在于:對日志消息格式化的過程多線程是并行的,雖然push串行有細微鎖開銷,但總體的效率還是比單線程快的。單線程push寫入少穩定 多線程短時間push大量數據。

利用多核CPU,加速日志格式化

  • 格式化(如:時間戳、線程ID、日志級別、文本拼接)本身是有一定開銷的。

  • 單線程異步時,所有格式化工作由一個線程做,受限于單核CPU速度。

  • 多線程異步時,不同線程可以在不同核上并行進行格式化

格式化速率大大提高,總體日志生產能力上升。

1.單線程異步

2.多線程異步 8

測試線程數總日志條數總大小總耗時每秒輸出條數每秒輸出大小
第一次1線程(單線程異步)200,000條2GB2.62613秒76,157條/s761,578 KB/s
第二次8線程(多線程異步)200,000條(每線程25,000條)2GB2.35712秒84,849條/s848,491 KB/s

總結:

條件推薦日志模式原因
每秒日志量小(≤幾千條)同步單線程系統開銷最小,結構最簡單
每秒日志量中等(幾萬條)異步單線程主線程減少阻塞,異步線程批量處理
每秒日志量大(十萬條以上)異步多線程并行格式化 + 快速push + 批量write,極限提升吞吐

總結:

模塊功能
Logger類日志器,統一管理日志級別、格式化器、輸出目的地
Formatter類日志消息格式化(支持自定義格式)
Sink類日志落地(支持stdout/file等多種輸出)
Builder模式統一構建日志器(配置LoggerName、LoggerType、Formatter、Sink等)
LoggerManager(單例)全局日志器管理中心,負責創建、查找日志器實例
異步模塊(AsyncLogger)實現緩沖區管理、異步push和write,減少主線程I/O阻塞
同步模塊(SyncLogger)簡單直接的日志同步落地,適合小量數據低延遲需求

難點:

異步模式下push和write之間的速率平衡問題

由于push本身非常快(只是內存拷貝),
而異步線程的write動作相對慢(需要系統調用,將數據從用戶態寫入內存緩沖區),
如果主線程push頻率太高,異步線程write跟不上,就會導致緩沖區積壓,最終push阻塞(安全模式),影響主線程業務流程。

針對這個問題,我做了幾層優化設計:

1. 雙緩沖區結構?

減少消費者和生產者的鎖沖突,提高異步線程write()處理速率。

  • 主線程push到生產緩沖區;

  • 異步線程消費交換后的緩沖區;

  • 交換期間加鎖,數據處理期間無鎖,減少鎖沖突時間

2. 條件變量+批處理機制

push完成數據立刻用條件變量notify通過異步線程處理異步線程一次性批量write,減少系統調用次數,提升磁盤寫入效率。

  • 主線程push時,用std::condition_variable::notify_one()喚醒異步線程;

  • 異步線程wait時只在緩沖區有數據或stop信號時醒來;

  • 一次消費整個緩沖區內所有日志,批量write,減少系統調用次數,提升磁盤寫入效率。

3. 支持安全異步與非安全異步模式

生產者push太快就選安全模式 阻塞push,等有空間時再push

  • 在業務量爆發時,可以選擇:

    • 安全異步模式(生產緩沖區滿了就阻塞push,保護內存)

    • 非安全異步模式(無限擴容緩沖區,保證主線程push不卡頓,犧牲內存)

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/79146.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/79146.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/79146.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

提示詞工程(GOT)把思維鏈推理過程圖結構化

Graph of Thoughts&#xff08;GOT&#xff09;&#xff1f; 思維圖&#xff08;Graph of Thoughts&#xff09;是一種結構化的表示方法&#xff0c;用于描述和組織模型的推理過程。它將信息和思維過程以圖的形式表達&#xff0c;其中節點代表想法或信息&#xff0c;邊代表它們…

登錄github失敗---解決方案

登錄github失敗—解決方案 1.使用 Microsoft Edge 瀏覽器 2.https://www.itdog.cn/dns/ 查詢 github.global.ssl.fastly.net github.com 兩個 域名的 IP 3.修改DNS 為 8.8.8.8 8.8.4.4 4.修改windows hosts 文件 5. 使用 Microsoft Edge 瀏覽器 打開github.com

Spring AOP概念及其實現

一、什么是AOP 全稱Aspect Oriented Programming&#xff0c;即面向切面編程&#xff0c;AOP是Spring框架的第二大核心&#xff0c;第一大為IOC。什么是面向切面編程&#xff1f;切面就是指某一類特定的問題&#xff0c;所以AOP也可以稱為面向特定方法編程。例如對異常的統一處…

強化學習_Paper_2017_Curiosity-driven Exploration by Self-supervised Prediction

paper Link: ICM: Curiosity-driven Exploration by Self-supervised Prediction GITHUB Link: 官方: noreward-rl 1- 主要貢獻 對好奇心進行定義與建模 好奇心定義&#xff1a;next state的prediction error作為該state novelty 如果智能體真的“懂”一個state&#xff0c;那…

spring中的@Configuration注解詳解

一、概述與核心作用 Configuration是Spring框架中用于定義配置類的核心注解&#xff0c;旨在替代傳統的XML配置方式&#xff0c;通過Java代碼實現Bean的聲明、依賴管理及環境配置。其核心作用包括&#xff1a; 標識配置類&#xff1a;標記一個類為Spring的配置類&#xff0c;…

7.計算機網絡相關術語

7. 計算機網絡相關術語 ACK (Acknowledgement) 確認 ADSL (Asymmetric Digital Subscriber Line) 非對稱數字用戶線 AP (Access Point) 接入點 AP (Application) 應用程序 API (Application Programming Interface) 應用編程接口 APNIC (Asia Pacific Network Informatio…

Hadoop 集群基礎指令指南

目錄 &#x1f9e9; 一、Hadoop 基礎服務管理指令 ?? 啟動 Hadoop ?? 關閉 Hadoop &#x1f9fe; 查看進程是否正常運行 &#x1f4c1; 二、HDFS 常用文件系統指令 &#x1f6e0;? 三、MapReduce 作業運行指令 &#x1f4cb; 四、集群狀態監控指令 &#x1f4a1; …

【MySQL數據庫】事務

目錄 1&#xff0c;事務的詳細介紹 2&#xff0c;事務的屬性 3&#xff0c;事務常見的操作方式 1&#xff0c;事務的詳細介紹 在MySQL數據庫中&#xff0c;事務是指一組SQL語句作為一個指令去執行相應的操作&#xff0c;這些操作要么全部成功提交&#xff0c;對數據庫產生影…

一、OrcaSlicer源碼編譯

一、下載 1、OrcaSlicer 2.3.0版本的源碼 git clone https://github.com/SoftFever/OrcaSlicer.git -b v2.3.0 二、編譯 1、在OrcaSlicer目錄運行cmd窗口&#xff0c;輸入build_release.bat 2、如果出錯了&#xff0c;可以多運行幾次build_release.bat 3、在OrcaSlicer\b…

港口危貨儲存單位主要安全管理人員考試精選題目

港口危貨儲存單位主要安全管理人員考試精選題目 1、危險貨物儲存場所的電氣設備應符合&#xff08; &#xff09;要求。 A. 防火 B. 防爆 C. 防塵 D. 防潮 答案&#xff1a;B 解析&#xff1a;港口危貨儲存單位存在易燃易爆等危險貨物&#xff0c;電氣設備若不防爆&…

格雷希爾用于工業氣體充裝站的CZ系列氣罐充裝轉換連接器,其日常維護有哪些

格雷希爾氣瓶充裝連接器&#xff0c;長期用于壓縮氣體的快速充裝和壓縮氣瓶的氣密性檢測&#xff0c;需要進行定期的維護&#xff0c;為每一次的充裝提供更好的連接。下列建議的幾點維護準則適用于格雷希爾所有充注接頭&#xff0c;請非專業人士不要隨意拆卸連接器。 格雷希爾氣…

Java 多線程進階:什么是線程安全?

在多線程編程中&#xff0c;“線程安全”是一個非常重要但又常被誤解的概念。尤其對于剛接觸多線程的人來說&#xff0c;不理解線程安全的本質&#xff0c;容易寫出“偶爾出錯”的代碼——這類 bug 往往隱蔽且難以復現。 本文將用盡可能通俗的語言&#xff0c;從三個角度解釋線…

MSO-Player:基于vlc的Unity直播流播放器,支持主流RTSP、RTMP、HTTP等常見格式

MSO-Player 基于libVLC的Unity視頻播放解決方案 支持2D視頻和360度全景視頻播放的Unity插件 &#x1f4d1; 目錄 &#x1f3a5; MSO-Player &#x1f4cb; 功能概述&#x1f680; 快速入門&#x1f4da; 關鍵組件&#x1f4dd; 使用案例&#x1f50c; 依賴項&#x1f4cb; 注意…

navicat中導出數據表結構并在word更改為三線表(適用于navicat導不出doc)

SELECTCOLUMN_NAME 列名,COLUMN_TYPE 數據類型,DATA_TYPE 字段類型,IS_NULLABLE 是否為空,COLUMN_DEFAULT 默認值,COLUMN_COMMENT 備注 FROMINFORMATION_SCHEMA.COLUMNS WHEREtable_schema db_animal&#xff08;數據庫名&#xff09; AND table_name activity&#xff08;…

docker學習筆記6-安裝wordpress

一、創建自定義網絡、查看網絡 docker netword create blog docker network ls 二、 啟動mysql容器 啟動命令&#xff1a; docker run -d -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD123456 \ -e MYSQL_DATABASEwordpress \ -v mysql-data:/var/lib/mysql \ -v /app/myconf:/etc…

03_Mybatis-Plus LambadaQueryWrapper 表達式爆空指針異常

&#x1f31f; 03_MyBatis-Plus LambdaQueryWrapper 爆出空指針異常的坑點分析 ? 場景描述 來看一段常見的 MyBatis-Plus 查詢寫法&#xff0c;是否存在問題&#xff1f; Page<VideoInfoVo> videoInfoVosPage videoMapper.selectPage(page, new LambdaQueryWrapper&…

WEB安全--社會工程--SET釣魚網站

1、選擇要釣魚的網站 2、打開kali中的set 3、啟動后依次選擇&#xff1a; 4、輸入釣魚主機的地址&#xff08;kali&#xff09;和要偽裝的網站域名&#xff1a; 5、投放釣魚網頁&#xff08;服務器域名:80&#xff09; 6、獲取賬號密碼

Ethan獨立開發產品日報 | 2025-04-29

1. mrge 代碼審查的光標 mrge 是一個由人工智能驅動的代碼審查平臺&#xff0c;能夠自動審核拉取請求&#xff08;PR&#xff09;&#xff0c;為人工審查員提供超級能力。它是像 cal.com 和 n8n 這樣快速發展的團隊的首選工具。 關鍵詞&#xff1a;mrge, 代碼審查, AI驅動, …

ubuntu22.04 qemu arm64 環境搭建

目錄 創建 安裝 Qemu 啟動 # 進入qemu虛擬機后執行 qemu編譯器安裝 創建 qemu-img create ubuntu22.04_arm64.img 40G 安裝 qemu-system-aarch64 -m 4096 -cpu cortex-a57 -smp 4 -M virt -bios QEMU_EFI.fd -nographic -drive ifnone,fileubuntu-22.04.5-live-server-a…

安全生產知識競賽宣傳口號160句

1. 安全生產是責任&#xff0c;每個人都有責任 2. 安全生產是保障&#xff0c;讓我們遠離危險 3. 安全生產是團結&#xff0c;共同守護每一天 4. 注重安全&#xff0c;守護明天 5. 安全生產無小事&#xff0c;關乎千家萬戶 6. 安全第一&#xff0c;人人有責 7. 安全生產無差別&…