C++ С Резьбой Регистратор


Что это?

Это довольно простой регистратор, который использует поток.

Как это работает

Примечание: это довольно страшное объяснение, возможно, было бы легче взглянуть на код.

Класс logger-это синглтон, который содержит функция log(). Журналы () с первого использования создается статический экземпляр логгера. Затем функция возвращает объект logstream (построена со ссылкой на регистратора), который является производным функции std::ostringstream. Этот объект используется для форматирования сообщения. После его уничтожения он посылает форматированный СТД::строка обратно к регистратору, используя push() функция, которая блокирует мьютекс, а затем добавляет СТД::строка частной СТД::очередь к регистратору.

Когда регистратор построен он создает поток выполнения print_routine (функции), т. е. цикл, который блокирует мьютекс и распечатывает все содержимое с std::очереди, а потом спит на определенный промежуток времени. После разрушения он говорит рутинную закончить с помощью установки типа bool печати на false и соединяет нить.

Код

log_enum.ч

#ifndef ANDROMEDA_LOG_ENUM_H
#define ANDROMEDA_LOG_ENUM_H

namespace andromeda {

enum class log_level {
    info,
    warning,
    severe,
    fatal
};

}

#endif

логгер.ч

#ifndef ANDROMEDA_LOGGER_H
#define ANDROMEDA_LOGGER_H

#include <sstream>
#include <mutex>
#include <queue>
#include <chrono>
#include <thread>

#include "log_enum.h"

namespace andromeda {
    class logger;
}

#include "logstream.h"

namespace andromeda {

class logger {
    std::queue<std::string> m_q;
    std::mutex m_q_mu;

    std::mutex m_stdout_mu;
    std::mutex m_stderr_mu;

    std::thread m_print_thread;
    bool m_print = true;
    static void print_routine(logger *instance, std::chrono::duration<double, std::milli> interval);    

    logger();
    ~logger();
public:
    logger(logger const&) = delete;
    void operator=(logger const&) = delete;

    static logstream log(log_level level = log_level::info) {
        static logger m_handler;
        return logstream(m_handler, level);
    }

    void push(std::string fmt_msg);
};

}

#endif

logger.cpp

#include "logger.h"

#include <iostream>

namespace andromeda {

logger::logger()
{
    m_print_thread = std::thread(print_routine, this, std::chrono::milliseconds(16));
}

logger::~logger()
{
    m_print = false;
    m_print_thread.join();
}

void logger::push(std::string fmt_msg)
{
    std::lock_guard<std::mutex> lock(m_q_mu);
    m_q.push(fmt_msg);
}

void logger::print_routine(logger *instance, std::chrono::duration<double, std::milli> interval)
{
    while(instance->m_print || !instance->m_q.empty()) {
        auto t1 = std::chrono::steady_clock::now();
        {
            std::lock_guard<std::mutex> lock(instance->m_q_mu);
            while(!instance->m_q.empty()) {
                std::cout << instance->m_q.front() << std::endl;
                instance->m_q.pop();
            }
        }
        auto t2 = std::chrono::steady_clock::now();
        std::chrono::duration<double, std::milli> time_took = t2 - t1;
        //sleep
        if(time_took < interval && instance->m_print) {
            std::this_thread::sleep_for(interval - time_took);
        }
    }
}

}

logstream.ч

#ifndef ANDROMEDA_LOGSTREAM_H
#define ANDROMEDA_LOGSTREAM_H

#include <sstream>

#include "log_enum.h"

namespace andromeda {

class logger;

class logstream : public std::ostringstream {
    logger& m_logger;
    log_level m_level;

    std::string get_level_string();
    std::string get_time_string();
public:
    logstream(logger& log, log_level);
    ~logstream();
};

}

#endif

logstream.cpp

#include "logstream.h"

#include <ctime>
#include "logger.h"

namespace andromeda {

logstream::logstream(logger& log, log_level level) : m_logger(log), m_level(level)
{}

logstream::~logstream()
{
    //note: not using time yet because it adds 0.015 ms
    //m_logger.push(get_time_string() + get_level_string() + str());
    m_logger.push(get_level_string() + str());

}

std::string logstream::get_level_string()
{
    std::string temp;
    switch(m_level) {
        case log_level::info: temp = "[INFO]"; break;
        case log_level::warning: temp = "[WARNING]"; break;
        case log_level::severe: temp = "[SEVERE]"; break;
        case log_level::fatal: temp = "[FATAL]"; break;
    }
    return temp;    //copy ellision should be guaranteed with a C++17 compiler
}

std::string logstream::get_time_string()
{
    std::time_t t = std::time(nullptr);
#ifdef _WIN32
    std::tm time;
    localtime_s(&time, &t);
#else
    std::tm time = *std::localtime(&t);
#endif
    char t_str[20];
    std::strftime(t_str, sizeof(t_str), "%T", &time);

    return ("[" + std::string(t_str) + "]");
}

}

main.cpp

#include "logger/logger.h"

#include <iostream>

int main(int argc, char **argv) {
    {
        using namespace andromeda;
        auto t1 = std::chrono::steady_clock::now();
        logger::log() << "Hello World";
        auto t2 = std::chrono::steady_clock::now();

        /*
        auto t3 = std::chrono::steady_clock::now();
        std::cout << "Hello World" << std::endl;
        auto t4 = std::chrono::steady_clock::now();
        */

        std::chrono::duration<double, std::milli> d1 = t2 - t1;
        //std::chrono::duration<double, std::milli> d2 = t4 - t3;
        logger::log() << "logger took " << d1.count() << "ms";
        //std::cout << "cout took " << d2.count() << "ms" << std::endl;

        //This line is here to make test whether everything is printed before program exit
        logger::log(log_level::fatal) << "end of program test: " << 33;
    }
    return 0;
}

Ориентир

Я провел тест этого регистратора против СТД::соиь без использования времени.

run 1: logger = 0.02925ms  and cout = 0.007725ms -> log/cout = 3.77
run 2: logger = 0.028469ms and cout = 0.008442ms -> log/cout = 3.37
run 3: logger = 0.027484ms and cout = 0.016155ms -> log/cout = 1.7
run 4: logger = 0.028764ms and cout = 0.007859ms -> log/cout = 3.66
run 5: logger = 0.027457ms and cout = 0.008173ms -> log.cout = 3.36

В среднем логгер был 3.172 раз медленнее, чем в случае std::соиь. Это плохо?

Чего я и добиваюсь

Я стремлюсь, чтобы это было достаточно быстро, нитки-безопасная и кросс-платформенный.

Что я думаю, может быть улучшена

Я думаю, что get_time_string() могут быть улучшены. На данный момент это ухудшает производительность примерно на 50%. Другой вещи-это деталь. Я думаю, что это может быть хорошей идеей, чтобы, возможно, включить источник и идентификатор потока. Одна последняя мелочь, это log_level. У меня не так много опыта, поэтому я не знаю, сколько разных уровнях необходимы для более больших проектов.

Любая обратная связь приветствуется.



Комментарии
3 ответа

Бенчмаркинг

Это лучше иметь больше точек данных, чем меньше в тесте. Поэтому лучше выполнить несколько раз код и занимает в среднем (или просто измерить полное), чем его запустить.

Также (Как упоминалось выше) в первый раз logger::log() называется, статическая переменная инициализируется, создавая новый поток. Так что лучше тест будет что-то вроде:

    logger::log(); // get thread creation out of the way...

auto runs = 500;

auto t1 = std::chrono::high_resolution_clock::now();

for (auto i = 0; i != runs; ++i)
logger::log() << "Hello World";

auto t2 = std::chrono::high_resolution_clock::now();

что для меня дает 0.812507ms. (Первый звонок logger::log() занимает около 1.31655ms, кстати).

Маршрутизация то же самое напрямую std::cout занимает ~500мс!


Одно дополнительное замечание: сравнивая время, затраченное logger в главном потоке времени, затраченного на std::cout это сравнение двух разных вещей. Регистратор звонков создает / копирует / сцепляет некоторые строки и добавляет их в очередь, а cout на самом деле присылают на стандартный вывод.

Поскольку логгер делает ту же работу с cout в другой ветке в любом случае, мы должны рассматривать время, затраченное logger::log() звонки в основном потоке как накладные добавлены cout.

Мы могли бы изолировать, что накладные расходы и профиль его. (Под управлением профилировщика с количество запусков на 500000, и закомментировав строку в logger.cpp: //std::cout << instance->m_q.front() << std::endl; дает приличные указания на то, что занимает больше всего времени).

Помимо проверки накладные на другой поток, или чистого любопытства, нет особого смысла. Накладные расходы 0.8 МС на ~500мс, уже хорошо.


Код - незначительные недостатки


  • get_level_string() и get_time_string() можете оба быть const.

Поскольку мы сосредоточены на производительности...


  • Уровне строк может быть статическим членам logstream класса, что снимает необходимость создавать их каждый раз.

  • Вместо того, чтобы использовать временный буфер, мы можем использовать std::strftime писать напрямую в std::stringкак-то так:

    std::string result(21, '[');
    auto charsWritten = std::strftime(&result[1], result.size() - 1, "%T", &time);
    result[1 + charsWritten] = ']';
    result.resize(1 + charsWritten + 1);
    return result;

  • Конкатенации нескольких строк (например, m_logger.push(get_time_string() + get_level_string() + str()); может быть неэффективной, из-за создания промежуточных объектов String, который может понадобится несколько средств. Этого можно избежать путем создания выходного строкового объекта, .reserve()Инг нужного размера, и с помощью += оператор скопировать каждый в выходную строку.

  • m_q.push(std::move(fmt_msg)); избежать копирования!

  • get_level_string() и get_time_string() можете оба быть const.

Это хоть и все мелкие вещи. Профайлер скажет вам, что на самом деле должен меняться.

2
ответ дан 13 апреля 2018 в 05:04 Источник Поделиться

Резьбонарезной

Я бы посоветовал вам узнать о std::condition_variable.

В основном, вы можете есть что нить ждать условие переменная должна быть включена вместо того, чтобы постоянно опрашивать ваш очереди, чтобы увидеть, если есть данные для чтения. Это может повысить производительность, как вы можете реагировать, как только появились новые данные, а не (потенциально) задержка времени ждать. Кроме того, вы будете использовать меньше процессора таким образом. Рассмотрим следующие:

void logger::push(std::string fmt_msg)
{
std::unique_lock<std::mutex> lg(m_q_mu);
m_q.push(fmt_msg);
m_cv.notify_all(); //where m_cv is a std::condition_variable belonging to logger. Calling notify_all will cause any thread that's sleeping on this condition variable to wake up.
}

В свой способ print_routine, то...

void logger::print_routine(logger* instance)
{
std::unique_lock<std::mutex> lg(m_q_mu)
while(instance->m_print)
{
for(auto& str : instance->m_q)
{
std::cout << q << '\n';
}
instance->m_q.clear();
m_cv.wait(lg, [instance]{return !instance->m_print || !instance->m_q.empty();});
//Upon calling wait, the mutex locked by lg will be unlocked and the thread will sleep until it's notified.
//When that happens, it will re-lock the mutex and run the predicate.
//If it returns true (which will happen if m_print is now false or if there's now something in the queue) then wait will end. Otherwise, the mutex will be unlocked again and the thread will go back to sleep.
//So, once wait ends, either m_print is false or the queue is not empty.
//If m_print is false, the while loop will then see so and exit.
//If m_print is not false, the while loop will continue, which will mean printing out the messages in the queue and then clearing it, then waiting again.
}
}

Наконец, чтобы остановить печать...

logger::~logger()
{
m_print = false;
m_cv.notify_all();
if(m_print_thread.joinable())
{
m_print_thread.join();
}
}


Неиспользуемый код

Я не вижу двух m_stdout_mu и m_stderr_mu мьютексы используются везде.

2
ответ дан 13 апреля 2018 в 09:04 Источник Поделиться

В logstream::get_level_string а не копировать литерал в std::string и затем скопировать ее в качестве возвращаемого значения можно возвращать ссылку на статический общедоступный массив, содержащий строки (которая была создана конструктором).

1
ответ дан 13 апреля 2018 в 11:04 Источник Поделиться