3

我正在尝试编写一个非常简单的线程安全记录器。理想情况下,我希望它像 一样工作std::cout,您可以只重载<<运算符,然后让所有内容神奇地显示在日志中。我在 Windows 机器上,所以这是我尝试的方法:

// Threadsafe logger
class Logger
{
public:
  Logger()
  {
    InitializeCriticalSection(&s);
  }

  ~Logger()
  {
    DeleteCriticalSection(&s);
  }

  void Log(std::ostream const& os)
  {
    EnterCriticalSection(&s);
    //std::cout << static_cast<std::stringstream const&>(os).str();
    std::cout << os.rdbuf();
    LeaveCriticalSection(&s);
  }

private:
  CRITICAL_SECTION s;
};

请注意,我已经尝试了两种方法来实现该Log()功能。我接受 an 的原因ostream是因为这stringstream似乎<<是在调用运算符之后产生的。当我运行此代码时,该函数的两种变体Log()都以相同的方式失败:

#include <iostream>
#include <sstream>
#include <Windows.h>

int main(int argc, char* argv[])
{
  Logger logger;
  //logger.Log(std::stringstream("Test"));
  logger.Log(std::stringstream("Another ") << "test");
  std::cin.get();
}

使用 Log 函数的两种变体输出第一行(“测试”)可以正常工作并正确显示。第二行输出一个错位的输出:

testher

这显然是test写在上面Another。我对这些流的工作方式缺少什么?我试着flush打电话希望能解决问题,但它什么也没做。

如何在线程安全记录器上尝试与流一起正常工作?

4

4 回答 4

3

至少在我看来,这种解决问题的方法至少使用起来有些笨拙,因为它需要您创建某种辅助ostream对象,将数据流式传输到其中,然后将其传递给您的日志。这似乎与你所说的你真正喜欢的不太吻合。

对于您如何完成线程锁定代码,我也有点不高兴。举个例子,如果你在你的过程中遇到异常std::cout << os.rdbuf();,你可以退出作用域而不离开临界区。

我想我会从关键部分的薄包装开始,添加一个 RAII 样式的类来锁定关键部分(并在超出范围时自动解锁它),然后在实现Log类时使用它们。此外,我会作弊并让Log该类使用模板成员函数一举接受几乎任何类型的输出:

编辑:经过深思熟虑,我决定接受这样一个概念,即每个问题都可以通过另一个间接级别来解决。为此,我添加了一个中间体transaction,它封装了将多个项目的输出链接到 astringstream中,然后将该结果作为线程安全事务写出。

#include <windows.h>
#include <iostream>
#include <sstream>

class crit_sect {
    CRITICAL_SECTION cs;

    void lock() { EnterCriticalSection(&cs); }
    void unlock() { LeaveCriticalSection(&cs); }
    friend class lock;

    crit_sect(crit_sect const &); /* = delete; */
    crit_sect &operator=(crit_sect const &other); /* = delete; */
public:
    crit_sect() { InitializeCriticalSection(&cs); }
    ~crit_sect() { DeleteCriticalSection(&cs); }
};

class lock {
    crit_sect &cs;
public:
    lock(crit_sect &c) : cs(c) { cs.lock(); }
    ~lock() { cs.unlock(); }
};

class transaction {
    std::ostringstream buffer;
public:
    transaction(std::string const &s="") : buffer(s, std::ios::out | std::ios::ate) {}

    template <class T>
    transaction &operator<<(T const &t) {
        buffer << t;
        return *this;
    }

    friend std::ostream &operator<<(std::ostream &os, transaction const &t) {
        return os << t.buffer.str();
    }
};

class Log {
    std::ostream &out;
    crit_sect mutex;
public:
    Log(std::ostream &sink) : out(sink) { }

    template <class T>
    void operator<<(T const &t) {
        lock l(mutex);
        out << t;
    }    
};

int main() {
    Log l(std::cout);

    l << "This is a string\n";

    l << (transaction("Another ") << "Test");
    return 0;
}

由于该log类型不支持链接,因此任何不使用 a 链接输出的尝试transaction都会失败(不会编译)。与原来的相比,用法仍然更简洁——ctor 所需的额外参数ostringstream被隐藏了,并且名称transaction清楚地说明了正在做什么,或者更确切地说,完成了。

于 2012-04-11T17:05:20.960 回答
3

使用可变参数模板:

void Log_impl(std::ostream &os) {} // recursion base case

template<typename T,typename... Us>
void Log_impl(std::ostream &os,T &&t,Us &&... us) {
    os << std::forward<T>(t);
    Log_impl(os,std::forward<Us>(us)...);
}

template<typename... Ts> void Log(Ts &&... ts) {
    std::stringstream ss;
    Log_impl(ss,std::forward<Ts>(ts)...);
    fprintf(stdout,"%s\n",ss.str().c_str()); // thread safe output
}

用法:

Log("Another"," test ",100);

我还没有实际测试过这段代码......

于 2012-04-11T17:35:10.817 回答
2

问题不在于记录器,而在于您对 stringstream 的使用。初始化 std::stringstream 时,流的位置指示符位于流的开头。

现在,当您开始使用 '<<' 写入字符串时,您开始在位置指示器处写入,替换之前的任何内容。

要解决此问题,您可以使用 std::stringstream("Another ", stringstream::in | stringstream::out | std::stringstream::ate) 初始化字符串流

(根据http://www.cplusplus.com/reference/iostream/stringstream/stringstream/

于 2012-04-11T16:46:39.647 回答
0

<< 不会将“test”末尾的空字节发送到您创建的临时字符串流(“Another”)中,这就是您看到“testher”的原因。

于 2012-04-11T16:35:58.083 回答