spdlog源码解读(二)

本文探讨了在并发操作增多时如何使用SPDLOG进行多线程日志记录,重点介绍了异步机制、预分配队列、后台线程以及不同溢出策略,旨在提高性能并保证数据一致性。
摘要由CSDN通过智能技术生成

不简单改改

从这一章开始,我们得考虑多线程写日志了,当我们的应用程序开始处理更多的并发操作时,日志系统也需要升级以应对多线程环境。在多线程写日志的情境下,我们面临的主要挑战是如何保证日志数据的一致性和完整性,同时又不过度牺牲性能。接下来,我们将一步一步探索如何在多线程环境中有效地实现日志记录。

异步日志

spdlog 的异步机制是设计来提高日志记录的性能和效率的。在同步日志记录中,每次记录日志时,执行线程都会直接写入日志目标(如文件、控制台等),这可能会因为 IO 操作而阻塞执行线程。而异步日志记录通过将日志消息首先发送到一个队列中,然后由另一个专门的线程从队列中取出消息并进行日志记录,从而避免了直接的 IO 操作阻塞执行线程。

关键组件

  • 预分配队列spdlog 使用一个预分配的队列来存储日志消息。这个队列通常是一个固定大小的环形队列,当队列满时,它将根据配置的溢出策略(如阻塞、丢弃最旧的消息或丢弃新消息)来处理新的日志消息。

  • 后台日志线程spdlog 创建一个后台线程,专门负责从队列中取出日志消息并将它们写入到指定的日志目标中。这个分离的线程允许执行线程无需等待 IO 操作即可继续执行,从而提高了应用程序的性能。

  • 溢出策略spdlog 提供了几种不同的队列溢出策略,允许开发者根据应用程序的需求来选择最合适的策略。这些策略包括阻塞写入操作直到队列有空间、丢弃队列中最旧的消息来为新消息腾出空间,或者丢弃尝试添加的新消息。

我们来看代码实现

class SPDLOG_API async_logger final : public std::enable_shared_from_this<async_logger>,
                                      public logger {
    friend class details::thread_pool;

public:
    template <typename It>
    async_logger(std::string logger_name,
                 It begin,
                 It end,
                 std::weak_ptr<details::thread_pool> tp,
                 async_overflow_policy overflow_policy = async_overflow_policy::block)
        : logger(std::move(logger_name), begin, end),
          thread_pool_(std::move(tp)),
          overflow_policy_(overflow_policy) {}

    async_logger(std::string logger_name,
                 sinks_init_list sinks_list,
                 std::weak_ptr<details::thread_pool> tp,
                 async_overflow_policy overflow_policy = async_overflow_policy::block);

    async_logger(std::string logger_name,
                 sink_ptr single_sink,
                 std::weak_ptr<details::thread_pool> tp,
                 async_overflow_policy overflow_policy = async_overflow_policy::block);

    std::shared_ptr<logger> clone(std::string new_name) override;

protected:
    void sink_it_(const details::log_msg &msg) override;
    void flush_() override;
    void backend_sink_it_(const details::log_msg &incoming_log_msg);
    void backend_flush_();

private:
    std::weak_ptr<details::thread_pool> thread_pool_;
    async_overflow_policy overflow_policy_;
};
}  // namespace spdlog

async_logger->logger->thread_pool,通过线程池来循环处理队列中的日志消息

void SPDLOG_INLINE thread_pool::worker_loop_() {
    while (process_next_msg_()) {
    }
}

// process next message in the queue
// return true if this thread should still be active (while no terminate msg
// was received)
bool SPDLOG_INLINE thread_pool::process_next_msg_() {
    async_msg incoming_async_msg;
    q_.dequeue(incoming_async_msg);

    switch (incoming_async_msg.msg_type) {
        case async_msg_type::log: {
            incoming_async_msg.worker_ptr->backend_sink_it_(incoming_async_msg);
            return true;
        }
        case async_msg_type::flush: {
            incoming_async_msg.worker_ptr->backend_flush_();
            return true;
        }

        case async_msg_type::terminate: {
            return false;
        }

        default: {
            assert(false);
        }
    }

    return true;
}

加上线程池

一步一步来,异步很容易想到用独立的线程去干活,那我们先给代码加上线程池

#include "ThreadPool.h" // 开源库线程池,C11实现,100行代码不到,基本功能都有
static ThreadPool log_pool(1); // 创建线程池,里面默认一个线程

给线程池分配任务,来了日志就分配线程去写

log_pool.enqueue([level, format, args...] {
	if (log_file.is_open()) {
		auto log_entry = fmt::format("[{}] [{}] {}\n", currentDateTime(), toString(level), fmt::format(format, args...));
		log_file << log_entry;
	} else {
		std::cerr << "无法打开日志文件!" << std::endl;
	}
});

我们代码变成了这样

#include <iostream>
#include <fstream>
#include <string>
#include <ctime>
#include <fmt/core.h> // 包含 fmt 库的核心功能
#include "ThreadPool.h" // 包含 ThreadPool 类

static std::ofstream log_file("log.txt", std::ios::app); // 静态日志文件对象
static ThreadPool log_pool(1); // 创建一个拥有 1 个工作线程的线程池用于日志

enum LogLevel {
    INFO,
    WARNING,
    ERROR
};

// 日志级别到字符串的转换
const char* toString(LogLevel level) {
    switch(level) {
        case INFO: return "INFO";
        case WARNING: return "WARNING";
        case ERROR: return "ERROR";
        default: return "UNKNOWN";
    }
}

// 获取格式化的当前时间字符串
std::string currentDateTime() {
    std::time_t now = std::time(nullptr);
    std::string dt = std::ctime(&now);
    dt.pop_back(); // 移除换行符
    return dt;
}

// 使用模板和参数包来支持格式化的参数
template <typename... Args>
void log_message(LogLevel level, const std::string& format, Args... args) {
	// 线程池添加任务,每次写日志时用独立线程去写
    log_pool.enqueue([level, format, args...] {
        if (log_file.is_open()) {
            auto log_entry = fmt::format("[{}] [{}] {}\n", currentDateTime(), toString(level), fmt::format(format, args...));
            log_file << log_entry;
        } else {
            std::cerr << "无法打开日志文件!" << std::endl;
        }
    });
}

int main() {
    log_message(INFO, "这是一条信息级别的消息。");
    log_message(ERROR, "错误代码:{}. 错误信息:{}", 404, "未找到");
    // 确保在退出前处理所有日志消息
    // 在真实应用中,这可能会以不同方式处理,例如,在应用关闭时等待所有任务完成。
    std::this_thread::sleep_for(std::chrono::seconds(1)); // 简单的等待日志完成的方式(生产使用不推荐)
    return 0;
}
> [Fri Mar  8 14:00:25 2024] [INFO] 这是一条信息级别的消息。
> [Fri Mar  8 14:00:25 2024] [ERROR] 错误代码:404. 错误信息:未找到

关键变化和注意事项

  1. 日志的线程池:我们初始化了一个拥有一个工作线程的 ThreadPool,专门用于处理日志任务。这将日志 I/O 操作卸载到另一个线程,可能提高主应用程序逻辑的性能。
  2. 异步日志记录log_message 函数现在将日志任务加入到线程池的队列中。每个任务捕获日志级别、格式和参数,然后在工作线程中格式化和写入日志条目。
  3. 等待日志完成:在此简单示例中,我们使用 std::this_thread::sleep_for 来稍等一会,确保程序退出前所有日志消息都被处理。在实际情况中,需要实现更复杂的方式来确保应用退出前所有任务完成,可能涉及跟踪任务或优雅地关闭线程池。
  4. 错误处理和鲁棒性:为了简单起见,某些方面如错误处理(例如,如果无法打开日志文件该怎么办)和溢出策略(例如,限制排队任务的数量或处理队列已满的情况)在这里没有广泛涉及。
  5. spdlog的区别:这两者线程模型还是有点区别的,spdlog是把日志写进消息队列,线程池里面的线程处理消息,而我们的线程池是将处理的函数放进tasks,执行task,目前两者都能实现目标。

溢出策略

我们再来实现下溢出策略,首先我们得知道当前任务有多少个,才能做判断。那么这个判断放哪呢?

template <typename... Args>
void log_message(LogLevel level, const std::string& format, Args... args) {
	// 获取tasks的大小,如果溢出,return?
    log_pool.enqueue([level, format, args...] {
	    // 获取tasks的大小,如果溢出,return?
        if (log_file.is_open()) {
            auto log_entry = fmt::format("[{}] [{}] {}\n", currentDateTime(), toString(level), fmt::format(format, args...));
            log_file << log_entry;
        } else {
            std::cerr << "无法打开日志文件!" << std::endl;
        }
    });
}

放在这两地方首先得解决几个问题,一个是tasks.size()我们目前访问不到,另一个是获取大小得加锁。我们看看spdlog放哪

void SPDLOG_INLINE thread_pool::post_async_msg_(async_msg &&new_msg,
                                                async_overflow_policy overflow_policy) {
    if (overflow_policy == async_overflow_policy::block) {
        q_.enqueue(std::move(new_msg));
    } else if (overflow_policy == async_overflow_policy::overrun_oldest) {
        q_.enqueue_nowait(std::move(new_msg));
    } else {
        assert(overflow_policy == async_overflow_policy::discard_new);
        q_.enqueue_if_have_room(std::move(new_msg));
    }
}

他放在线程池里,入队前,由消息队列来判断要不要入队。那我们也放在任务入队的时候,由 enqueue 决定阻塞策略,这样都在类内部实现了,外面就不需要关心阻塞这件事了。而且还要好处,入队时本身就要上锁,放在这也减少加锁次数。对了,还得设置一个上限,也放线程池里,初始化的时候设置。代码如下:

#ifndef THREAD_POOL_H
#define THREAD_POOL_H

#include <vector>
#include <queue>
#include <memory>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <future>
#include <functional>
#include <stdexcept>

// Async overflow policy - block by default.
enum class async_overflow_policy {
    block,           // Block task can be enqueued
    overrun_oldest,  // Discard oldest task in the queue if full when trying to
                     // add new item.
    discard_new      // Discard new task if the queue is full when trying to add new item.
};

class ThreadPool {
public:
    ThreadPool(size_t threads, size_t maxQueueSize = 1000, async_overflow_policy policy = async_overflow_policy::block);
    template<class F, class... Args>
    auto enqueue(F&& f, Args&&... args)
        -> std::future<typename std::result_of<F(Args...)>::type>;
    ~ThreadPool();
private:
    // need to keep track of threads so we can join them
    std::vector< std::thread > workers;
    // the task queue
    std::queue< std::function<void()> > tasks;

    // synchronization
    std::mutex queue_mutex;
    std::condition_variable condition;
    bool stop;

    // overflow policy
    size_t maxQueueSize;
    async_overflow_policy overflow_policy;
};

// the constructor just launches some amount of workers
inline ThreadPool::ThreadPool(size_t threads, size_t maxQueueSize, async_overflow_policy policy)
    : stop(false), maxQueueSize(maxQueueSize), overflow_policy(policy)
{
    for(size_t i = 0;i<threads;++i)
        workers.emplace_back(
            [this]
            {
                for(;;)
                {
                    std::function<void()> task;

                    {
                        std::unique_lock<std::mutex> lock(this->queue_mutex);
                        this->condition.wait(lock,
                            [this]{ return this->stop || !this->tasks.empty(); });
                        if(this->stop && this->tasks.empty())
                            return;
                        task = std::move(this->tasks.front());
                        this->tasks.pop();
                    }

                    task();
                }
            }
        );
}

// add new work item to the pool
template<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args)
    -> std::future<typename std::result_of<F(Args...)>::type>
{
    using return_type = typename std::result_of<F(Args...)>::type;

    auto task = std::make_shared< std::packaged_task<return_type()> >(
            std::bind(std::forward<F>(f), std::forward<Args>(args)...)
        );

    std::future<return_type> res = task->get_future();
    {
        std::unique_lock<std::mutex> lock(queue_mutex);

        if(stop)
            throw std::runtime_error("enqueue on stopped ThreadPool");

        // 应用不同的溢出策略
        if(tasks.size() >= maxQueueSize) {
            switch (overflow_policy) {
                case async_overflow_policy::block:
                    // 阻塞直到有空间
                    condition.wait(lock, [this]{ return tasks.size() < maxQueueSize; });
                    break;
                case async_overflow_policy::overrun_oldest:
                    // 溢出最旧的任务
                    tasks.pop();
                    break;
                case async_overflow_policy::discard_new:
                    // 丢弃新任务
                    return std::future<return_type>(); // 返回一个空的 future
            }
        }

        tasks.emplace([task](){ (*task)(); });
    }
    condition.notify_one();
    return res;
}

// the destructor joins all threads
inline ThreadPool::~ThreadPool()
{
    {
        std::unique_lock<std::mutex> lock(queue_mutex);
        stop = true;
    }
    condition.notify_all();
    for(std::thread &worker: workers)
        worker.join();
}

#endif

外面将 main.cpp 里面的改成 1 个线程,只允许一个任务,新任务丢弃用来测试

static ThreadPool log_pool(1, 1, async_overflow_policy::discard_new); // 创建一个拥有 1 个工作线程的线程池用于日志

输出情况,只有一条日志,另一条被丢弃了

> [Fri Mar  8 17:24:57 2024] [INFO] 这是一条信息级别的消息。
  • 8
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值