一、信號量
關于信號量的介紹在深入Linux內核:IPC資源管理揭秘
這篇文章當中已經做了初步的介紹了,相信大家對于信號量已經有了初步的認知了。
今天,我們就來探討如何實現信號量。
1. 信號量的接口
//初始化信號量
//成功了,返回0,失敗了,返回-1并且設置錯誤碼
//sem初始化的信號量
//pshared設置為0,代表線程間使用
//value信號量的初始值
int sem_init(sem_t* sem, int pshared, unsigned int value);
//銷毀信號量
//成功返回0,失敗返回-1并且設置錯誤碼
int sem_destroy(sem_t* sem);
//減少信號量
//成功返回0,失敗返回-1并且設置錯誤碼
int sem_wait(sem_t* sem);
//增加信號量
//成功返回0,失敗返回-1并且設置錯誤碼
int sem_post(sem_t* sem);
2. 信號量實現的一些細節問題
信號量的接口就了解到這里。我們實現的信號量是基于一個環形隊列實現的(數組)
。
接下來,我們了解實現的一些細節。
隊列的容量是有限的,剛開始時,隊列為空,一定是生產者先運行。此時生產者和消費者訪問同一個位置,生產者還沒生產數據,消費者就開始消費數據,這是不行的,所以,必須等到生產者生產數據之后,消費者才可以消費數據。所以,生產者和消費者之間需要維護互斥與同步的關系。
當隊列為滿時,必須讓消費者先運行。此時生產者,消費者又指向了同一個位置,當消費者拿取數據時,生產者是不能立即生產數據的,要不然消費者還沒有獲取到數據,生產者已經把數據覆蓋了,不就導致數據錯亂了嗎!所以,這個過程不僅需要生產者和消費者互斥的獲取數據,還需要同步。
當隊列不為空,不為滿時,生產者和消費者肯定不是指向同一個位置的,所以,生產者和消費者不就可以并發執行了。
3. 信號量的實現
Sem.hpp
#pragma once
#include<iostream>
#include<vector>
#include<unistd.h>
#include<semaphore.h>class Sem
{
public:Sem(int num):_initnum(num){sem_init(&_sem, 0, _initnum);}void P(){int n = sem_wait(&_sem);}void V(){int n = sem_post(&_sem);}~Sem(){sem_destroy(&_sem);}
private:sem_t _sem;int _initnum;
};
RingQueue.hpp
#include"Sem.hpp"int gcap = 5;
template<typename T>
class RingQueue
{
public:RingQueue(int cap = gcap):_ring_queue(cap),_cap(cap),_space_sem(cap),_data_sem(0),_c_step(0),_p_step(0){}void EnQueue(const T& in){//先申請空間信號量,對資源的一種預定機制_space_sem.P();//生產數據_ring_queue[_p_step++] = in;_p_step %= _cap;_data_sem.V();}void Pop(T* out){//先申請數據信號量_data_sem.P();//消費數據*out = _ring_queue[_c_step++];_c_step %= _cap;_space_sem.V();}~RingQueue(){}
private:std::vector<T> _ring_queue;int _cap;Sem _space_sem;Sem _data_sem;int _c_step;int _p_step;
};
main.cc
#include"RingQueue.hpp"void* consumer(void* args)
{RingQueue<int>* rq = static_cast<RingQueue<int>*>(args);while(true){int data = 0;rq->Pop(&data);std::cout << "消費者消費了一個數據" << data << std::endl;}
}void* productor(void* args)
{RingQueue<int>* rq = static_cast<RingQueue<int>*>(args);int data = 1;while(true){sleep(1);rq->EnQueue(data);std::cout << "生產者生產了一個數據" << data << std::endl;data++;}
}
int main()
{RingQueue<int>* rq = new RingQueue<int>();pthread_t c, p;pthread_create(&c, nullptr, consumer, (void*)rq);pthread_create(&p, nullptr, productor, (void*)rq);pthread_join(c, nullptr);pthread_join(p, nullptr);return 0;
}
Makefile
ringqueue:main.ccg++ -o $@ $^ -std=c++11
.PHONY:clean
clean:rm -f ringqueue
4. 信號量實現過程中的一些疑問
這是一個基于單生產者,單消費者的信號量。
問題1:我們在申請信號量的過程當中并沒有用到鎖,難道就不怕數據不安全嗎?
剛開始時,隊列為空,生產者先申請信號量,生產數據,然后在V操作,喚醒消費者,消費者才能消費數據。這個過程本身就已經完成了生產者和消費者之間的互斥與同步關系
。
當隊列為滿時,生產者申請信號量失敗,就被阻塞住,此時消費者申請信號量,消費數據,然后再喚醒生產者,生產者才能生產數據,所以這個過程本身也完成了生產者與消費者之間的互斥與同步關系
。
而隊列不為空也不為滿時,生產者和消費者可以并發執行
。
問題2:我們怎么沒有在臨界區內部,判斷資源是否就緒呢?
信號量本身就是一把計數器,是對于資源的一種預定機制,對信號量進行P操作的時候,雖然是申請信號量,但本質就是對資源是否就緒進行判斷。有多少資源就可以預定多少資源,絕不會預定出的資源比實際資源多,也就是說有多少資源就可以有多少個生產者線程
。
重新理解信號量。
我們把信號量設置為5,如果信號量設置為1呢?不就是二元信號量,一個線程申請信號量之后就不可能再有第二個線程成功申請信號量,信號量就變為了0,這不就是一把鎖
嗎!控制著線程的開關。
重新理解一下鎖:不就是認為自己的資源只有一份,申請鎖不就類似于二元信號量,信號量P操作,釋放鎖不就是V操作
。
所以,鎖是信號量的一種特殊情況。
二、日志與策略模式
什么是日志呢?
計算機中的日志是記錄系統和軟件運行中發生事件的文件,主要作用是監控運行狀態,記錄異常信息,幫助快速定位問題并支持程序員進行問題修復,它是系統維護,故障排查和安全管理的重要工具。
我們設計的日志格式主要包含以下幾個指標:
時間戳、日志等級、日志內容、文件名、行號,進程線程相關 id 信息。
//獲取時間戳
//tloc設置為nullptr
time_t time(time_t* tloc);
//timep獲取到的時間戳
//result輸出型參數
struct tm* localtime_r(const time_t* timep, struct tm* result);
struct tm
{int tm_sec; /* Seconds (0-60) */int tm_min; /* Minutes (0-59) */int tm_hour; /* Hours (0-23) */int tm_mday; /* Day of the month (1-31) */int tm_mon; /* Month (0-11) */int tm_year; /* Year - 1900 */int tm_wday; /* Day of the week (0-6, Sunday = 0) */int tm_yday; /* Day in the year (0-365, 1 Jan = 0) */int tm_isdst; /* Daylight saving time */
};
Logger.hpp
#pragma once
#include <iostream>
#include <filesystem>
#include <fstream>
#include <string>
#include <sstream>
#include <memory>
#include <unistd.h>
#include "Mutex.hpp"enum class LoggerLevel
{DEBUG,INFO,WARNING,ERROR,FATAL
};std::string LoggerLevelToString(LoggerLevel level)
{switch (level){case LoggerLevel::DEBUG:return "Debug";case LoggerLevel::INFO:return "Info";case LoggerLevel::WARNING:return "Warning";case LoggerLevel::ERROR:return "Error";case LoggerLevel::FATAL:return "Fatal";default:return "Unknown";}
}std::string GetCurrentTime()
{// 獲取時間戳time_t timep = time(nullptr);// 把時間戳轉化為時間格式struct tm currtm;localtime_r(&timep, &currtm);// 轉化為字符串char buffer[64];snprintf(buffer, sizeof(buffer), "%4d-%02d-%02d %02d-%02d-%02d",currtm.tm_year + 1900, currtm.tm_mon + 1, currtm.tm_mday,currtm.tm_hour, currtm.tm_min, currtm.tm_sec);return buffer;
}class LogStrategy
{
public:virtual ~LogStrategy() = default;virtual void SyncLog(const std::string &logmessage) = 0;
};// 顯示器刷新
class ConsoleLogStrategy : public LogStrategy
{
public:~ConsoleLogStrategy(){}virtual void SyncLog(const std::string &logmessage) override{{LockGuard lockguard(&_lock);std::cout << logmessage << std::endl;}}private:Mutex _lock;
};const std::string default_dir_path_name = "log";
const std::string default_filename = "test.log";
// 文件刷新
class FileLogStrategy : public LogStrategy
{
public:FileLogStrategy(const std::string dir_path_name = default_dir_path_name,const std::string filename = default_filename): _dir_path_name(dir_path_name), _filename(filename){if (std::filesystem::exists(_dir_path_name)){return;}try{std::filesystem::create_directories(_dir_path_name);}catch (const std::filesystem::filesystem_error &e){std::cerr << e.what() << "\r\n";}}~FileLogStrategy(){}virtual void SyncLog(const std::string &logmessage) override{{LockGuard lock(&_lock);std::string target = _dir_path_name;target += '/';target += _filename;std::ofstream out(target.c_str(), std::ios::app);if (!out.is_open()){return;}out << logmessage << "\n";out.close();}}private:std::string _dir_path_name;std::string _filename;Mutex _lock;
};class Logger
{
public:Logger(){}void EnableConsoleStrategy(){_strategy = std::make_unique<ConsoleLogStrategy>();}void EnableFileStrategy(){_strategy = std::make_unique<FileLogStrategy>();}class LogMessage{public:LogMessage(LoggerLevel level, std::string filename, int line, Logger& logger): _curr_time(GetCurrentTime()), _level(level), _pid(getpid()), _filename(filename), _line(line), _logger(logger){std::stringstream ss;ss << "[" << _curr_time << "] "<< "[" << LoggerLevelToString(_level) << "] "<< "[" << _pid << "] "<< "[" << _filename << "] "<< "[" << _line << "]"<< " - ";_loginfo = ss.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 _curr_time; // 時間戳LoggerLevel _level; // 日志等級pid_t _pid; // 進程pidstd::string _filename; // 文件名int _line; // 行號std::string _loginfo; // 一條合并完成的,完整的日志信息Logger &_logger; // 提供刷新策略的具體做法};LogMessage operator()(LoggerLevel level, std::string filename, int line){return LogMessage(level, filename, line, *this);}~Logger(){}private:std::unique_ptr<LogStrategy> _strategy;
};Logger logger;#define LOG(level) logger(level, __FILE__, __LINE__)
#define EnableConsoleStrategy() logger.EnableConsoleStrategy()
#define EnableFileStrategy() logger.EnableFileStrategy()
Mutex.hpp
#pragma once
#include<iostream>
#include<mutex>
#include<pthread.h>class Mutex
{
public:Mutex(){pthread_mutex_init(&_lock, nullptr);}void Lock(){pthread_mutex_lock(&_lock);}void Unlock(){pthread_mutex_unlock(&_lock);}~Mutex(){pthread_mutex_destroy(&_lock);}
private:pthread_mutex_t _lock;
};class LockGuard
{
public:LockGuard(Mutex* _mutex):_mutexp(_mutex){_mutexp->Lock();}~LockGuard(){_mutexp->Unlock();}
private:Mutex* _mutexp;
};
main.cc
#include"Logger.hpp"int main()
{EnableConsoleStrategy();LOG(LoggerLevel::ERROR) << "hello linux" << ", 6.66 " << 123;LOG(LoggerLevel::WARNING) << "hello linux" << ", 6.66 " << 123;LOG(LoggerLevel::ERROR) << "hello linux" << ", 6.66 " << 123;LOG(LoggerLevel::ERROR) << "hello linux" << ", 6.66 " << 123;LOG(LoggerLevel::ERROR) << "hello linux" << ", 6.66 " << 123;// std::string test = "hello world, hello log";// std::unique_ptr<LogStrategy> logger_ptr = std::make_unique<ConsoleLogStrategy>();// // logger_ptr->SyncLog(test);// // std::unique_ptr<LogStrategy> logger_ptr = std::make_unique<FileLogStrategy>();// logger_ptr->SyncLog(GetCurrentTime());// sleep(1);// logger_ptr->SyncLog(GetCurrentTime());// sleep(1);// logger_ptr->SyncLog(GetCurrentTime());// sleep(1);// logger_ptr->SyncLog(GetCurrentTime());// sleep(1);// logger_ptr->SyncLog(GetCurrentTime());return 0;
}
Makefile
logger_test:main.ccg++ -o $@ $^ -std=c++17 -lpthread
.PHONY:clean
clean:rm -f logger_test
這里的構思非常的巧妙,本來想要輸出一條完整的日志信息,需要很復雜的操作,現在利用這樣的做法就可以用一行代碼輸出一條完整的日志信息。
下面我們就來看看是怎樣的做法呢?
我們在外部類 Logger 里重載了運算符(),返回了一個 LogMessage 類的臨時對象
。
EnableConsoleStrategy();這個其實就是一個宏,這個宏是對于 Logger 類里面的兩個函數的簡便操作,LOG也是一個宏,是對于()運算符的重載函數的簡便操作
。
所以,當調用了LOG宏之后會返回一個臨時對象,<<運算符重載函數是LogMessage類的一個成員函數,返回的是臨時對象的引用,因為,LOG宏返回一個LogMessage類的臨時對象,這個臨時對象又繼續調用了 << 運算符函數,繼續返回臨時對象的引用,以此類推,直到調用結束
。
臨時對象是具有常性的,它的生命周期在一條語句之后結束,所以可以返回臨時對象的引用。
今天的文章分享到此結束,覺得不錯的伙伴給個一鍵三連吧。