项目源网站tinywebserver。这个模块是日志模块,开了另一个线程,往fd里写日志。
blockqueue 双端队列
这个双端队列用于存续需要写入的数据,使用信号量和锁保证线程安全,为啥用双端队列而不用队列呢?我也不知道,log模块里也只调用了push_back的接口。
代码
/*
* @Author : mark
* @Date : 2020-06-16
* @copyleft Apache 2.0
*/
#ifndef BLOCKQUEUE_H
#define BLOCKQUEUE_H
#include <mutex>
#include <deque>
#include <condition_variable>
#include <sys/time.h>
// 线程安全的阻塞双端队列
template <class T>
class BlockDeque
{
public:
explicit BlockDeque(size_t MaxCapacity = 1000);
~BlockDeque();
void clear();
bool empty();
bool full();
void Close();
size_t size();
size_t capacity();
T front();
T back();
void push_back(const T &item);
void push_front(const T &item);
// 都是弹出队首元素
bool pop(T &item); // 返回值表示是否成功弹出元素,item就是弹出的元素
bool pop(T &item, int timeout); // 如果队列有元素,返回,没有元素,等待timeout秒,还是没有元素返回false
void flush();
private:
std::deque<T> deq_; // 双端队列
size_t capacity_;
std::mutex mtx_;
bool isClose_;
std::condition_variable condConsumer_;
std::condition_variable condProducer_;
};
template <class T>
BlockDeque<T>::BlockDeque(size_t MaxCapacity) : capacity_(MaxCapacity)
{
assert(MaxCapacity > 0);
isClose_ = false;
}
template <class T>
BlockDeque<T>::~BlockDeque()
{
Close();
};
template <class T>
void BlockDeque<T>::Close()
{
{
std::lock_guard<std::mutex> locker(mtx_);
deq_.clear();
isClose_ = true;
}
condProducer_.notify_all();
condConsumer_.notify_all();
};
template <class T>
void BlockDeque<T>::flush()
{
condConsumer_.notify_one();
};
template <class T>
void BlockDeque<T>::clear()
{
std::lock_guard<std::mutex> locker(mtx_);
deq_.clear();
}
template <class T>
T BlockDeque<T>::front()
{
std::lock_guard<std::mutex> locker(mtx_);
return deq_.front();
}
template <class T>
T BlockDeque<T>::back()
{
std::lock_guard<std::mutex> locker(mtx_);
return deq_.back();
}
template <class T>
size_t BlockDeque<T>::size()
{
std::lock_guard<std::mutex> locker(mtx_);
return deq_.size();
}
template <class T>
size_t BlockDeque<T>::capacity()
{
std::lock_guard<std::mutex> locker(mtx_);
return capacity_;
}
template <class T>
void BlockDeque<T>::push_back(const T &item)
{
std::unique_lock<std::mutex> locker(mtx_);
while (deq_.size() >= capacity_)
{
condProducer_.wait(locker);
}
deq_.push_back(item);
condConsumer_.notify_one();
}
template <class T>
void BlockDeque<T>::push_front(const T &item)
{
std::unique_lock<std::mutex> locker(mtx_);
while (deq_.size() >= capacity_)
{
condProducer_.wait(locker);
}
deq_.push_front(item);
condConsumer_.notify_one();
}
template <class T>
bool BlockDeque<T>::empty()
{
std::lock_guard<std::mutex> locker(mtx_);
return deq_.empty();
}
template <class T>
bool BlockDeque<T>::full()
{
std::lock_guard<std::mutex> locker(mtx_);
return deq_.size() >= capacity_;
}
template <class T>
bool BlockDeque<T>::pop(T &item)
{
std::unique_lock<std::mutex> locker(mtx_);
while (deq_.empty())
{
condConsumer_.wait(locker);
if (isClose_)
{
return false;
}
}
item = deq_.front();
deq_.pop_front();
condProducer_.notify_one();
return true;
}
template <class T>
bool BlockDeque<T>::pop(T &item, int timeout)
{
std::unique_lock<std::mutex> locker(mtx_);
while (deq_.empty())
{
if (condConsumer_.wait_for(locker, std::chrono::seconds(timeout)) == std::cv_status::timeout)
{
return false;
}
if (isClose_)
{
return false;
}
}
item = deq_.front();
deq_.pop_front();
condProducer_.notify_one();
return true;
}
#endif // BLOCKQUEUE_H
组成
- deque 标准库里的双端队列
- mutex 锁 保证只有一个线程访问这个deq_
- condProducer_ 信号量 表示生产者,当需要调用push的时候
- deque没满,直接push,唤醒一个消费者
- deque满了,等待
- condConsumer_信号量 表示消费者,当需要pop的时候
- deque为空,等待
- deque不为空,直接pop,唤醒一个生产者
关闭循环队列
template <class T>
void BlockDeque<T>::Close()
{
{
std::lock_guard<std::mutex> locker(mtx_);
deq_.clear();
isClose_ = true;
}
condProducer_.notify_all();
condConsumer_.notify_all();
};
清空deque,唤醒所有的等待进程。
log 日志模块
代码
头文件
/*
* @Author : mark
* @Date : 2020-06-16
* @copyleft Apache 2.0
*/
#ifndef LOG_H
#define LOG_H
#include <mutex>
#include <string>
#include <thread>
#include <sys/time.h>
#include <string.h>
#include <stdarg.h> // vastart va_end
#include <assert.h>
#include <sys/stat.h> //mkdir
#include "blockqueue.h"
#include "../buffer/buffer.h"
class Log
{
public:
void init(int level, const char *path = "./log",
const char *suffix = ".log",
int maxQueueCapacity = 1024);
// 单例模式
static Log *Instance();
static void FlushLogThread();
void write(int level, const char *format, ...);
void flush();
int GetLevel();
void SetLevel(int level);
bool IsOpen() { return isOpen_; }
private:
Log();
void AppendLogLevelTitle_(int level);
virtual ~Log();
void AsyncWrite_();
private:
static const int LOG_PATH_LEN = 256;
static const int LOG_NAME_LEN = 256;
static const int MAX_LINES = 50000;
const char *path_;
const char *suffix_;
int MAX_LINES_;
int lineCount_;
int toDay_;
bool isOpen_;
Buffer buff_;
int level_;
bool isAsync_;
FILE *fp_;
std::unique_ptr<BlockDeque<std::string>> deque_;
std::unique_ptr<std::thread> writeThread_;
std::mutex mtx_;
};
// do {}while(0) 括起来,确保宏展开后的内容被视为一个语句块,避免多次展开
#define LOG_BASE(level, format, ...) \
do \
{ \
Log *log = Log::Instance(); \
if (log->IsOpen() && log->GetLevel() <= level) \
{ \
log->write(level, format, ##__VA_ARGS__); \
log->flush(); \
} \
} while (0);
#define LOG_DEBUG(format, ...) \
do \
{ \
LOG_BASE(0, format, ##__VA_ARGS__) \
} while (0);
#define LOG_INFO(format, ...) \
do \
{ \
LOG_BASE(1, format, ##__VA_ARGS__) \
} while (0);
#define LOG_WARN(format, ...) \
do \
{ \
LOG_BASE(2, format, ##__VA_ARGS__) \
} while (0);
#define LOG_ERROR(format, ...) \
do \
{ \
LOG_BASE(3, format, ##__VA_ARGS__) \
} while (0);
#endif // LOG_H
源文件
/*
* @Author : mark
* @Date : 2020-06-16
* @copyleft Apache 2.0
*/
#include "log.h"
using namespace std;
Log::Log() {
lineCount_ = 0;
isAsync_ = false;
writeThread_ = nullptr;
deque_ = nullptr;
toDay_ = 0;
fp_ = nullptr;
}
Log::~Log() {
if(writeThread_ && writeThread_->joinable()) {
while(!deque_->empty()) {
deque_->flush();
};
deque_->Close();
writeThread_->join();
}
if(fp_) {
lock_guard<mutex> locker(mtx_);
flush();
fclose(fp_);
}
}
int Log::GetLevel() {
lock_guard<mutex> locker(mtx_);
return level_;
}
void Log::SetLevel(int level) {
lock_guard<mutex> locker(mtx_);
level_ = level;
}
void Log::init(int level = 1, const char* path, const char* suffix,
int maxQueueSize) {
isOpen_ = true;
level_ = level;
if(maxQueueSize > 0) {
isAsync_ = true;
if(!deque_) {
unique_ptr<BlockDeque<std::string>> newDeque(new BlockDeque<std::string>);
deque_ = move(newDeque);
std::unique_ptr<std::thread> NewThread(new thread(FlushLogThread));
writeThread_ = move(NewThread);
}
} else {
isAsync_ = false;
}
lineCount_ = 0;
time_t timer = time(nullptr);
struct tm *sysTime = localtime(&timer);
struct tm t = *sysTime;
path_ = path;
suffix_ = suffix;
char fileName[LOG_NAME_LEN] = {0};
snprintf(fileName, LOG_NAME_LEN - 1, "%s/%04d_%02d_%02d%s",
path_, t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, suffix_);
toDay_ = t.tm_mday;
{
lock_guard<mutex> locker(mtx_);
buff_.RetrieveAll();
if(fp_) {
flush();
fclose(fp_);
}
fp_ = fopen(fileName, "a");
if(fp_ == nullptr) {
mkdir(path_, 0777);
fp_ = fopen(fileName, "a");
}
assert(fp_ != nullptr);
}
}
void Log::write(int level, const char *format, ...) {
struct timeval now = {0, 0};
gettimeofday(&now, nullptr);
time_t tSec = now.tv_sec;
struct tm *sysTime = localtime(&tSec);
struct tm t = *sysTime;
va_list vaList;
/* 日志日期 日志行数 */
if (toDay_ != t.tm_mday || (lineCount_ && (lineCount_ % MAX_LINES == 0)))
{
unique_lock<mutex> locker(mtx_);
locker.unlock();
char newFile[LOG_NAME_LEN];
char tail[36] = {0};
snprintf(tail, 36, "%04d_%02d_%02d", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday);
if (toDay_ != t.tm_mday)
{
snprintf(newFile, LOG_NAME_LEN - 72, "%s/%s%s", path_, tail, suffix_);
toDay_ = t.tm_mday;
lineCount_ = 0;
}
else {
snprintf(newFile, LOG_NAME_LEN - 72, "%s/%s-%d%s", path_, tail, (lineCount_ / MAX_LINES), suffix_);
}
locker.lock();
flush();
fclose(fp_);
fp_ = fopen(newFile, "a");
assert(fp_ != nullptr);
}
{
unique_lock<mutex> locker(mtx_);
lineCount_++;
int n = snprintf(buff_.BeginWrite(), 128, "%d-%02d-%02d %02d:%02d:%02d.%06ld ",
t.tm_year + 1900, t.tm_mon + 1, t.tm_mday,
t.tm_hour, t.tm_min, t.tm_sec, now.tv_usec);
buff_.HasWritten(n);
AppendLogLevelTitle_(level);
va_start(vaList, format);
int m = vsnprintf(buff_.BeginWrite(), buff_.WritableBytes(), format, vaList);
va_end(vaList);
buff_.HasWritten(m);
buff_.Append("\n\0", 2);
if(isAsync_ && deque_ && !deque_->full()) {
deque_->push_back(buff_.RetrieveAllToStr());
} else {
fputs(buff_.Peek(), fp_);
}
buff_.RetrieveAll();
}
}
void Log::AppendLogLevelTitle_(int level) {
switch(level) {
case 0:
buff_.Append("[debug]: ", 9);
break;
case 1:
buff_.Append("[info] : ", 9);
break;
case 2:
buff_.Append("[warn] : ", 9);
break;
case 3:
buff_.Append("[error]: ", 9);
break;
default:
buff_.Append("[info] : ", 9);
break;
}
}
void Log::flush() {
if(isAsync_) {
deque_->flush();
}
fflush(fp_);
}
void Log::AsyncWrite_() {
// 写日志线程
// 循环从队列中取出日志,写到文件中
string str = "";
while(deque_->pop(str)) {
lock_guard<mutex> locker(mtx_);
fputs(str.c_str(), fp_);
}
}
Log* Log::Instance() {
static Log inst;
return &inst;
}
void Log::FlushLogThread() {
Log::Instance()->AsyncWrite_();
}
使用了c++的单例模式,需要刚开始的时候对log进行初始化。
成员
- std::unique_ptr<BlockDequestd::string> 指向BlockDeque的指针,用于存储需要写入的数据
- std::unique_ptrstd::thread 指向写入数据的线程,初始化的时候调用写线程,BlockDeque一有数据,就往文件里写
void Log::AsyncWrite_() {
// 写日志线程
// 循环从队列中取出日志,写到文件中
string str = "";
while(deque_->pop(str)) {
lock_guard<mutex> locker(mtx_);
fputs(str.c_str(), fp_);
}
}
- LOG_INFO LOG_WARN LOG_ERROR是日志的宏,本质上就是调用单例模式,往队列里添加数据,加了do{}while(0)是防止循环展开之后的bug问题
测试代码
#include <gtest/gtest.h>
#include <iostream>
#include <string>
#include <ctime>
#include <fstream>
#include "log.h"
#include "blockqueue.h"
namespace WebServerTest
{
using namespace std;
class LogTest : public ::testing::Test
{
protected:
void SetUp() override
{
Log::Instance()->init(1, "./log", ".log", 1024);
}
void TearDown() override
{
// Log::Instance()->flush();
}
};
TEST_F(LogTest, LogInfo)
{
// 因为是另一个线程在写入程序,可能出现还没有开始写,就读去文件,那个时候啥也读不到
LOG_INFO("This is a log info message");
LOG_INFO("This is a log info message with %d", 10);
LOG_INFO("This is a log info message with %s", "string");
sleep(1);
Log::Instance()->flush();
time_t timer = time(nullptr);
struct tm *sysTime = localtime(&timer);
struct tm t = *sysTime;
char fileName[256] = {0};
snprintf(fileName, 256 - 1, "%s/%04d_%02d_%02d%s",
"./log", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, ".log");
ifstream ifs(fileName);
string last_one, curr_line, last_two, last_three;
getline(ifs, last_three);
getline(ifs, last_two);
getline(ifs, last_one);
while (getline(ifs, curr_line))
{
last_three = move(last_two);
last_two = move(last_one);
last_one = move(curr_line);
}
ifs.close();
// cout << last_one << endl;
// cout << last_two << endl;
// cout << last_three << endl;
EXPECT_NE(last_one.find("This is a log info message with string"), std::string::npos);
EXPECT_NE(last_two.find("This is a log info message with 10"), std::string::npos);
EXPECT_NE(last_three.find("This is a log info message"), std::string::npos);
}
TEST_F(LogTest, LogWarn)
{
LOG_WARN("This is a log warn message");
sleep(1);
Log::Instance()->flush();
time_t timer = time(nullptr);
struct tm *sysTime = localtime(&timer);
struct tm t = *sysTime;
char fileName[256] = {0};
snprintf(fileName, 256 - 1, "%s/%04d_%02d_%02d%s",
"./log", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, ".log");
ifstream ifs(fileName);
string curr_line, last_one;
getline(ifs, last_one);
while (getline(ifs, curr_line))
{
last_one = move(curr_line);
}
ifs.close();
EXPECT_NE(last_one.find("This is a log warn message"), std::string::npos);
EXPECT_NE(last_one.find("warn"), std::string::npos);
}
TEST_F(LogTest, LogError)
{
LOG_ERROR("This is a log error message");
sleep(1);
Log::Instance()->flush();
time_t timer = time(nullptr);
struct tm *sysTime = localtime(&timer);
struct tm t = *sysTime;
char fileName[256] = {0};
snprintf(fileName, 256 - 1, "%s/%04d_%02d_%02d%s",
"./log", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, ".log");
ifstream ifs(fileName);
string curr_line, last_one;
getline(ifs, last_one);
while (getline(ifs, curr_line))
{
last_one = move(curr_line);
}
ifs.close();
EXPECT_NE(last_one.find("This is a log error message"), std::string::npos);
EXPECT_NE(last_one.find("error"), std::string::npos);
}
};
这里有一个bug,就是你初始化了之后,往BlockQueue里写数据,操作系统可能并不一定会执行你写入文件的那个线程,导致LOG_INFO之后,文件里还是没有数据。
解决方法是让测试的线程sleep,操作系统就会调用那个线程。