18. 添加异步日志——2.实现异步

在上一节中我们实现了同步日志,并输出到stdout。这节我们来实现异步。

在上一节中,添加了AppendFile类,这是对文件操作的一个底层的类,我们需要一个更加上层的,使用更便捷的接口来操控磁盘文件。比如说每输出了1000条日志消息,就需要冲刷AppendFile类中的缓冲区buffer_;还有日志文件大小若达到了预定的大小,就要新开一个日志文件等等。所以我们新创一个类,更加方便我们使用。

1.LogFile类

class LogFile
{
public:
	LogFile(const std::string& file_name, int flushEveryN);
	//添加日志消息到file_的缓冲区
	void Append(const char* str, int len);
    //冲刷file_的缓冲区
	void Flush(){ file_.Flush(); }
private:
	//日志文件名
	const std::string filename_;
	const int flushEveryN_;	//每写flushEveryN_条日志消息就强制冲刷file_的缓冲区内容到磁盘中
	int count_;	//计算写了多少条日志消息(即是调用Append()的次数)
	std::unique_ptr<AppendFile> file_;
};

LogFile类中有个AppendFile的智能指针成员变量file_,LogFile类通过操控file_就可以更加方便实现我们上述的需求。

该构造函数中创建一个AppendFile类对象的智能指针赋给成员变量file_。

在LogFile::Append()中调用AppendFile::Append()函数,并记录Append()的使用次数,(即也是记录添加了多少条日志消息)。达到次数后,就进行fflush()冲刷,即把日志消息写到磁盘文件中。

LogFile::LogFile(const std::string& file_name, int flushEveryN)
	:filename_(filename_)
	,flushEveryN_(flushEveryN)
	,count_(0)
{
	//file_.reset(new AppendFile(filename_));
	file_ = std::make_unique<AppendFile>(filename_);
}
//添加日志消息到file_的缓冲区
void LogFile::Append(const char* logline, int len)
{
	file_->Append(logline, int len);
	if (++count_ >= flushEveryN_) {
		count_ = 0;
		file_->Flush();
	}
}

这里可能又有疑惑了:为什么不把这些操作直接放到AppendFile类中呢,为什么就非要创建多一个类呢

这是为了方便我们的使用,AppendFile类封装了对磁盘文件的操作,LogFile类就只需要操控AppendFile的智能指针file_就行,这样就能很大便利我们去编写代码。

封装起来方便我们编写代码,这也是为了让类的责任分工明确。

或者你也可以去试试把这两个类写成一个类看看是什么效果。

接着来看看重头戏,实现异步的类。

2.AsyncLogger类

//异步日志,单独开一个线程来负责输出日志
class AsyncLogger
{
public:
	// 参数1为日志文件名,参数2默认每隔3秒冲刷缓冲区到磁盘文件中
	AsyncLogger(const std::string fileName, int flushInterval = 3);
    ~AsyncLogger();

    void Append(const char* logline, int len);

    //开始异步日志线程
    void start()
    {
        is_running_ = true;
        thread_ = std::thread([this]() {ThreadFunc(); });
    }

    void stop();

public:
    void ThreadFunc();  //后端日志线程函数

    //固定大小的缓冲区
    using Buffer = FixedBuffer<KLargeBuffer>;
    using BufferPtr = std::unique_ptr<Buffer>;
    using BufferVector = std::vector<BufferPtr>;

    const int flushInterval_;   //需要冲刷的时间间隔
    bool is_running_;
    std::string fileName_;      //日志文件名

    std::thread thread_;    //log后端线程

    std::mutex mutex_;   //用于保护下面的四个成员变量
    std::condition_variable cond_;

    BufferPtr currentBuffer_;   //当前的缓冲区指针
    BufferPtr nextBuffer_;      //预备可用的缓冲区指针
    BufferVector buffers_;      //待写入文件的已填完的缓冲vector
};

这里的异步主要是用空间换时间。该类里面的成员变量很多。

muduo中的日志使用的是双缓冲技术,这里讲解的也主要是参考陈硕书籍《Linux多线程服务端编程》。基本思路是先准备好两块buffer:currentBuffer_nextBuffer_前端负责往currentBuffer_中填写日志消息后端负责将nextBuffer_的数据写入文件中(磁盘)

当currentBuffer_写满后,交换currentBuffer_和nextBuffer_,(交换之后,那么前端就是往nextBuffer_中填写日志消息了),让后端将currentBuffer_的数据写入文件,而前端就往nextBuffer_中填入新的日志消息,如此往复。

先来看看发送方的代码,即是前端代码。

void AsyncLogger::Append(const char* logline, int len)
{
	std::unique_lock<std::mutex> lock(mutex_);
	if (currentBuffer_->Available() > len) {
		//当前缓冲区有足够的空间,就直接往缓冲区中添加
		currentBuffer_->Append(logline, len);
	}
	else {
		// 把已满的缓冲区放入预备给后端log线程的容器中
		buffers_.emplace_back(std::move(currentBuffer_));

		if (nextBuffer_) {			 // 若下一个可用缓冲区不为空,则通过move给cur
			currentBuffer_ = std::move(nextBuffer_);
		}
		else {	//否则就新建一个
			currentBuffer_.reset(new Buffer);
		}

		currentBuffer_->Append(logline, len);
		cond_.notify_one();			// 这时提醒后端log线程可以读取缓冲区了
	}
}

该函数中主要有3种情况,逻辑也清晰,要说的也在注释中了哈。

cond_.notify_one()就会唤醒后端log线程,把日志消息真正写入到磁盘文件中了。

来看看后端log线程函数。

//后端日志线程函数  代码有省略,只写了比较关键的部分
void AsyncLogger::ThreadFunc()
{
    //========================步骤1 ============================
	//打开磁盘日志文件,即是创建LogFile对象
	LogFile output(fileName_);

	//准备好后端备用的缓冲区1、2
	auto newBuffer1 = std::make_unique<Buffer>();
	auto newBuffer2 = std::make_unique<Buffer>();
	//备好读取的缓冲区vector
	BufferVector buffersToWrite;
	buffersToWrite.reserve(16);	//预留16个元素空间

	while (is_running_) {
		{
            //============================= 步骤2 =========================
			std::unique_lock<std::mutex> lock(mutex_);
			if (buffers_.empty()) {		//注意这里是用 if ,因为够这么多秒时间后也会唤醒的
				cond_.wait_for(lock, std::chrono::seconds(flushInterval_));
			}

			buffers_.emplace_back(std::move(currentBuffer_));  //把currentBuffer_放入到容器中
			currentBuffer_ = std::move(newBuffer1);    //将后端log线程备好的缓冲区1交给cur
			buffersToWrite.swap(buffers_);          //把buffers跟后端log线程空的容器交换

			 // 如果下一个可用缓冲区nextBuffer_为空,就把后端log线程的缓冲区2交给next
			if (!nextBuffer_) {
				nextBuffer_ = std::move(newBuffer2);
			}
		}	//这里出了锁的作用域

          //=======================步骤3 =======================
		 // 这里代码省略,若缓冲区过多,说明前端产生log的速度远大于后端消费的速度,这里只是简单的将它们丢弃
		// 将缓冲区内容写入文件
		for (size_t i = 0; i < buffersToWrite.size(); ++i){
			output.Append(buffersToWrite[i]->GetData(), buffersToWrite[i]->GetLength());
		}
        
        //======================== 步骤4 =====================
		// 将过多的缓冲区丢弃
		if (buffersToWrite.size() > 2){
			buffersToWrite.resize(2);
		}

		// 恢复后端备用缓冲区
		if (!newBuffer1){
			newBuffer1 = buffersToWrite.back();
			buffersToWrite.pop_back();
			newBuffer1->Reset();    //将缓冲区的数据指针归零
		}
        //newBuffer1的操作和1的操作也是一样的,所以在这里就省略,不写了

		// 丢弃无用的缓冲区
		buffersToWrite.clear();
		output.Flush();
	}
	output.Flush();
}

步骤1:

先打开保存日志的磁盘文件,准备好后端备用的空闲缓冲区1、2,用来交换已写好日志消息的缓冲区。也备好备好读取的缓冲区vector,用来交换已写入文件的填完完毕的缓冲vector。

这里都是为了在临界区内交换而不用阻塞的。

步骤2:到了while(1)循环里面了。也到了需要加锁的临界区。

首先等待条件是否触发等待触发的条件有两个:其一是超时(超过默认的3s),其二是前端写满了一个或多个buffer。注意这里使用的是if(),没有使用while()循环的。

当条件满足时,将当前缓冲(currentBuffer_)移入buffers_,并立刻将空闲的newBuffer1移为当前缓冲。要注意的是,这整段代码是位于临界区内的,所以不会有任何race condition。

接下来将已填写完毕的缓冲vector(buffers_)与备好的缓冲区buffersToWrite交换(和第12节中的函数doPendingFunctors()中也是要进行交换后,才好执行任务回调函数的),后面的代码就可以在临界区外安全地访问buffersToWrite,将其中的日志数据写入到磁盘文件中

接着用newBuffer2替换nextBuffer_,这样前端就始终有一个预备buffer 可以调配。

nextBuffer_可以减少前端临界区分配内存的概率,缩短前端临界区长度。(因为前端Append()函数中有一种情况会currentBuffer_.reset(new Buffer);)

步骤3:这时已经出了临界区

  若缓冲区过多,说明前端产生log的速度远大于后端消费的速度,这里只是简单的将它们丢弃(调用erase()函数)。之后把日志消息写入到磁盘(调用LogFile::Append函数),

步骤4:

 调用vector的resize()函数重置该缓冲区vector大小,并该缓冲区vector内的buffer重新填充给newBuffer1newBuffer2,这样下一次执行的时候就还有两个空闲buffer可用于替换前端的当前缓冲预备缓冲

3.如何使用AsyncLogger类

在AsyncLogger类中我们使用的是LogFile类,不使用AppendFile类。AppendFile类是供LogFile对象使用的,我们基本不会直接去操作AppendFile类。

记住是通过LogFile类去调用AppendFile类

那关键的异步日志的AsyncLogger类已实现。那该怎么使用该类呢。

对外界可见,可以直接使用的是Logger类,那怎么把这两个类关联起来呢。

AsyncLogger类中是使用AsyncLogger::start()开始后端线程前端收集日志消息是AsyncLogger::Append()函数。我们就要从这两个函数下手。

在Logger类中添加

public:
    static std::string LogFileName() { return logFileName_; }
    using OutputFunc=void (*)(const char* msg, int len);    //输出回调函数
    using FlushFunc=void (*)();    //冲刷回调函数

private:
    //日志文件名字
    static std::string logFileName_;

 前一节我们输出到stdout是通过Logger类的析构函数来输出的。

//这是上一节的实现
Logger::~Logger()
{
    //其他的省略
	DefaultOutput(buf.Data(), buf.Length());    //是调用了fwrite()输出到stdout的
}

那我们可以修改析构函数中的DefaultOutput()函数就行,我们可以调用AsyncLogger::Append()函数来让消息日志写到磁盘中。AsyncLogger::Append()就是收集消息日志,写到缓冲中,等到缓存满了,条件符合了,就会notify_one(),唤醒后端log线程,把日志消息真正写到磁盘中去

再来看看AsyncLogger类对象在哪生成呢。

static std::unique_ptr<AsyncLogger> asyncLogger;
static std::once_flag g_once_flag;

std::string Logger::logFileName_ = "../li22.log";
void OnceInit()
{
	asyncLogger = std::make_unique<AsyncLogger>(Logger::LogFileName());
	asyncLogger->start();    //开启后端线程
}
void AsyncOutput(const char* logline, int len)
{
	std::call_once(g_once_flag, OnceInit);    //保证只调用OnceInit函数一次
	asyncLogger->Append(logline, len);    //把日志消息写到缓冲区中
}

// 全局变量:输出函数
Logger::OutputFunc g_output = AsyncOutput;
Logger::~Logger()
{
    //省略其他的..............
	g_output(buf.Data(), buf.Length());
}

logFileName_ 就是我们的日志文件名。

那我们从该析构函数Logger::~Logger()开始,g_output()是全局输出函数,这里设置成了AsyncOutput()函数,AsyncOutput()函数使用了std::call_once,这是c++11新增的,可以保证不管是开多少个线程,该函数(OnceInit())只会执行一次。这就可以保证了后端log线程只有一个,这就不会有抢占写日志到磁盘的情况发生。asyncLogger的后端log线程就开始了,那就接着调用AsyncLogger::Append()函数就可以了。下面是其主要的流程图。

异步日志的主体就完成了,还有一些功能,小细节没有实现,下一节再讲解了。

这个双缓冲的设计真不错。

这一节我们就可以把日志输出到磁盘文件了。

完整源代码:https://github.com/liwook/CPPServer/tree/main/code/18_log2

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
目 录 第1章 Node.js简介 1 1.1 Node.js是什么 2 1.2 Node.js能做什么 3 1.3 异步式I/O与事件驱动 4 1.4 Node.js的性能 5 1.4.1 Node.js架构简介 5 1.4.2 Node.js与PHP+Nginx 6 1.5 JavaScript简史 6 1.5.1 Netscape与LiveScript 7 1.5.2 Java与Javascript 7 1.5.3 微软的加入——JScript 8 1.5.4 标准化——ECMAScript 8 1.5.5 浏览器兼容性问题 9 1.5.6 引擎效率革命和JavaScript的未来 9 1.6 CommonJS 10 1.6.1 服务端JavaScript的重生 10 1.6.2 CommonJS规范与实现 11 1.7 参考资料 12 第2章 安装和配置Node.js 13 2.1 安装前的准备 14 2.2 快速安装 14 2.2.1 Microsoft Windows系统上安装Node.js 14 2.2.2 Linux发行版上安装Node.js 16 2.2.3 Mac OS X上安装Node.js 16 2.3 编译源代码 17 2.3.1 在POSIX系统中编译 17 2.3.2 在Windows系统中编译 18 2.4 安装Node包管理器 18 2.5 安装多版本管理器 19 2.6 参考资料 21 第3章 Node.js快速入门 23 3.1 开始用 Node.js编程 24 3.1.1 Hello World 24 3.1.2 Node.js命令行工具 25 3.1.3 建立HTTP服务器 26 3.2 异步式I/O与事件式编程 29 3.2.1 阻塞与线程 29 3.2.2 回调函数 31 3.2.3 事件 33 3.3 模块和包 34 3.3.1 什么是模块 35 3.3.2 创建及加载模块 35 3.3.3 创建包 38 3.3.4 Node.js包管理器 41 3.4 调试 45 3.4.1 命令行调试 45 3.4.2 远程调试 47 3.4.3 使用Eclipse调试Node.js 48 3.4.4 使用node-inspector调试Node.js 54 3.5 参考资料 55 第4章 Node.js核心模块 57 4.1 全局对象 58 4.1.1 全局对象与全局变量 58 4.1.2 process 58 4.1.3 console 60 4.2 常用工具util 61 4.2.1 util.inherits 61 4.2.2 util.inspect 62 4.3 事件驱动events 63 4.3.1 事件发射器 64 4.3.2 error事件 65 4.3.3 继承EventEmitter 65 4.4 文件系统fs 65 4.4.1 fs.readFile 66 4.4.2 fs.readFileSync 67 4.4.3 fs.open 67 4.4.4 fs.read 68 4.5 HTTP服务器与客户端 70 4.5.1 HTTP服务器 70 4.5.2 HTTP客户端 74 4.6 参考资料 77 第5章 使用Node.js进行Web开发 79 5.1 准备工作 80 5.1.1 使用http模块 82 5.1.2 Express框架 83 5.2 快速开始 84 5.2.1 安装Express 84 5.2.2 建立工程 85 5.2.3 启动服务器 86 5.2.4 工程的结构 87 5.3 路由控制 89 5.3.1 工作原理 89 5.3.2 创建路由规则 92 5.3.3 路径匹配 93 5.3.4 REST风格的路由规则 94 5.3.5 控制权转移 95 5.4 模板引擎 97 5.4.1 什么是模板引擎 97 5.4.2 使用模板引擎 98 5.4.3 页面布局 99 5.4.4 片段视图 100 5.4.5 视图助手 100 5.5 建立微博网站 102 5.5.1 功能分析 102 5.5.2 路由规划 102 5.5.3 界面设计 103 5.5.4 使用Bootstrap 104 5.6 用户注册和登录 107 5.6.1 访问数据库 107 5.6.2 会话支持 110 5.6.3 注册和登入 111 5.6.4 页面权限控制 120 5.7 发表微博 123 5.7.1 微博模型 123 5.7.2 发表微博 125 5.7.3 用户页面 126 5.7.4 首页 127 5.7.5 下一步 129 5.8 参考资料 129 第6章 Node.js进阶话题 131 6.1 模块加载机制 132 6.1.1 模块的类型 132 6.1.2 按路径加载模块 132 6.1.3 通过查找node_modules目录加载模块 133 6.1.4 加载缓存 134 6.1.5 加载顺序 134 6.2 控制流 135 6.2.1 循环的陷阱 135 6.2.2 解决控制流难题 137 6.3 Node.js应用部署 138 6.3.1 日志功能 138 6.3.2 使用cluster模块 140 6.3.3 启动脚本 142 6.3.4 共享80端口 143 6.4 Node.js不是银弹 144 6.5 参考资料 146 附录A JavaScript的高级特性 147 附录B Node.js编程规范 167
下是一个使用 log4j.xml 配置异步日志的示例: ```xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"> <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" debug="false"> <appender name="ASYNC" class="org.apache.log4j.AsyncAppender"> <param name="BufferSize" value="1024" /> <appender-ref ref="CONSOLE" /> <appender-ref ref="FILE" /> </appender> <appender name="CONSOLE" class="org.apache.log4j.ConsoleAppender"> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss,SSS} [%t] %-5p %c{1}:%L - %m%n" /> </layout> </appender> <appender name="FILE" class="org.apache.log4j.RollingFileAppender"> <param name="File" value="logs/app.log" /> <param name="MaxFileSize" value="10MB" /> <param name="MaxBackupIndex" value="10" /> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss,SSS} [%t] %-5p %c{1}:%L - %m%n" /> </layout> </appender> <root> <level value="INFO" /> <appender-ref ref="ASYNC" /> </root> </log4j:configuration> ``` 在这个配置文件中,我们定义了三个 Appender:一个 ConsoleAppender 和一个 RollingFileAppender,以及一个 AsyncAppender,将其它两个 Appender 作为子 Appender 异步输出日志AsyncAppender 的 BufferSize 属性定义了缓冲区大小,一旦达到这个大小,就会异步输出缓冲区中的日志。 在 root 节点中,我们将日志级别设为 INFO,同时将 AsyncAppender 指定为使用的 Appender。这样,在应用程序中记录的所有日志都会被异步输出到 Console 和 RollingFile 中。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值