可变参数增强绑定类型解析

variadic boost bind type resolution

我正在尝试编写一个异步记录器,它接受可变参数,然后使用可变纵梁将这些参数串在一起,然后推送到单个生产者单个消费者队列中。

我卡在了 Log 结构的入队函数部分,如下所示:

template <typename T>
std::string Log::stringer(T const & t){
    return boost::lexical_cast<std::string>(t);
}

template<typename T, typename ... Args>
std::string Log::stringer(T const & t, Args const & ... args){
    return stringer(t) + stringer(args...);
}

 template<typename T, typename ... Args>
 void Log::enqueue(T & t, Args & ... args){
     boost::function<std::string()> f 
        = boost::bind(&Log::stringer<T &, Args & ...>,this,
                      boost::ref(t),
                      boost::forward<Args>(args)...);
 /// the above statement fails to compile though if i use 'auto f' it works ->
 /// but then it is unclear to me what the signature of f really is ?                              

 // at this point i would like to post the functor f onto my asio::io_service, 
 // but not able to cause it's not clear to me what the type of f is.
 // I think it should be of type boost::function<std::string()>

 }

在 main() 中,我调用

  Log t_log;
  t_log.enqueue("hello"," world");

我对你问的功能的建议:

template <typename T, typename... Args> void enqueue(T &t, Args const&... args) {
    this->io_service->post([=]{ 
                auto s = stringer(t, args...);
                //std::fprintf(stderr, "%s\n", s.c_str()); 
            });
}

这适用于 GCC 和 Clang(GCC 4.9 或更高版本,因为捕获的可变参数包存在已知问题)。

但实际上,我会重新考虑手头的设计,并且肯定会开始很多更简单,直到您知道哪些区域值得进一步优化。

问题

这段代码有很多地方我不明白:

  • 为什么参数被非常量引用采用
  • 为什么您随后对它们使用 std::forward<>(您现在已经是价值类别,并且不会改变)
  • 为什么要将字符串化传递给 io_service

    • 队列将引入锁定(有点反驳无锁队列)和
    • 字符串化将忽略其结果...
  • 为什么要在这里使用boost::function?这会导致(另一个)动态分配和间接调度......只是 post f

  • 为什么首先要通过引用绑定参数?如果你打算在不同的线程上处理参数,这会导致未定义的行为。例如。想象来电者在做

    char const msg[] = "my message"; // perhaps some sprintf output
    l.enqueue(cat.c_str(), msg);
    

    c_str()enqueue 返回后过时并且 msg 很快超出范围,或者被其他数据覆盖。

  • 明明有 c++11 支持(因为你使用了 std::forward<> 和属性),为什么还要使用 bind 方法?

  • 你为什么要使用无锁队列(预计会以最大值持续记录 CPU?在那种情况下,记录是你应用程序的核心功能,你可能应该这样想通过更严格一点(很多)(例如写入预分配的交替缓冲区并决定最大积压等)。

    在所有其他情况下,您可能希望在无锁队列上最多有 1 个单线程 运行。这可能已经有点矫枉过正了(不断地旋转一个线程是昂贵的)。相反,如果 n 个周期无事可做,您可以优雅地回退到 yields/synchronization。

  • 您可以只绑定到 shared_ptr。这是一个 lot 比绑定到 .get()

    更安全和方便

    In my sample below I've just removed the need for scoped_ptrs by not allocating everything from the heap (why was that?). (You can use boost::optional<work> if you needed work.)

  • 显式记忆顺序 load/stores 也让我感觉很糟糕。只有在标志中恰好涉及两个线程时,它们的编写方式才有意义,但这对我来说目前还不明显(线程是在周围创建的)。

    在大多数平台上不会有任何区别,鉴于上述情况,显式内存排序的存在作为一种清晰的代码气味脱颖而出

  • 同样的事情也适用于强制内联某些函数的尝试。您可以信任您的编译器,并且您应该避免再次猜测它,直到您知道您的瓶颈是由次优生成的代码

  • 造成的
  • 由于您打算赋予线程线程亲和性,确实使用线程局部变量。在 C++03 (__thread) 中使用 GCC/MSVC 扩展或使用 c++11 thread_local,例如在 pop()

    thread_local std::string s;
    s.reserve(1000);
    s.resize(0);
    

    这极大地减少了分配的数量(代价是使 pop() 不可重入,这不是必需的。

    I later noticed this pop() is limited to a single thread

  • 如果你所做的只是......手动围绕它进行自旋锁,那么拥有那个无锁队列有什么用?

    void push(std::string const &s) {
        while (std::atomic_flag_test_and_set_explicit(&this->lock, std::memory_order_acquire))
            ;
        while (!this->q->push(s))
            ;
        std::atomic_flag_clear_explicit(&this->lock, std::memory_order_release);
    }
    

清理建议

Live On Coliru

#include <boost/iostreams/device/array.hpp>
#include <boost/iostreams/stream.hpp>
#include <boost/atomic.hpp>
#include <boost/lockfree/spsc_queue.hpp>
#include <boost/thread/thread.hpp>

/*
 * safe for use from a single thread only
 */
template <unsigned line_maxchars = 1000>
class Log {
  public:
    Log(std::string const &logFileName, int32_t queueSize)
      : fp(stderr), // std::fopen(logFileName.c_str(),"w")
        _shutdown(false),
        _thread(&Log::pop, this),
        _queue(queueSize)
    { }

    void pop() {
        std::string s;
        s.reserve(line_maxchars);

        struct timeval ts;
        while (!_shutdown) {
            while (_queue.pop(s)) {
                gettimeofday(&ts, NULL);
                std::fprintf(fp, "%li.%06li %s\n", ts.tv_sec, ts.tv_usec, s.c_str());
            }
            std::fflush(fp); // RECONSIDER HERE?
        }

        while (_queue.pop(s)) {
            gettimeofday(&ts, NULL);
            std::fprintf(fp, "%li.%06li %s\n", ts.tv_sec, ts.tv_usec, s.c_str());
        }
    }

    template <typename S, typename T> void stringer(S& stream, T const &t) {
        stream << t;
    }

    template <typename S, typename T, typename... Args>
    void stringer(S& stream, T const &t, Args const &... args) {
        stringer(stream, t);
        stringer(stream, args...);
    }

    template <typename T, typename... Args> void enqueue(T &t, Args const&... args) {
        thread_local char buffer[line_maxchars] = {};
        boost::iostreams::array_sink as(buffer);
        boost::iostreams::stream<boost::iostreams::array_sink> stream(as);

        stringer(stream, t, args...);

        auto output = as.output_sequence();
        push(std::string(output.first, output.second));
    }

    void push(std::string const &s) {
        while (!_queue.push(s));
    }

    ~Log() {
        _shutdown = true;
        _thread.join();

        assert(_queue.empty());
        std::fflush(fp);
        std::fclose(fp);

        fp = NULL;
    }

  private:
    FILE *fp;
    boost::atomic_bool _shutdown;

    boost::thread _thread;
    boost::lockfree::spsc_queue<std::string> _queue;
};

#include <chrono>
#include <iostream>

int main() {
    using namespace std::chrono;
    auto start = high_resolution_clock::now();

    {
        Log<> l("/tmp/junk.log", 1024);

        for (int64_t i = 0; i < 10; ++i) {
            l.enqueue("hello ", i, " world");
        }
    }

    std::cout << duration_cast<microseconds>(high_resolution_clock::now() - start).count() << "μs\n";
}

如您所见,我将代码减少了三分之一。我已经记录了这样一个事实,即它只能在单线程中安全使用。

Asio 不见了。词汇转换消失了。事物有有意义的名字。不再需要调整内存顺序。不再需要线程亲和性摆弄。不再 内联嫉妒 。不再需要繁琐的字符串分配。

您可能从中受益最多的是

  • 通过引用将array_sinks/buffers汇集并存储在队列中
  • 不是每个日志都刷新