【问题标题】:How to make sure a chained logging statement is atomic?如何确保链式日志记录语句是原子的?
【发布时间】:2011-02-10 09:09:09
【问题描述】:

我有一个 operator<< 重载的日志记录类。所以我可以这样做:

oLogger << "Log this" << " and this" << " and " << 10 << endl;
oLogger`<< "Something else" << endl;

记录器执行此操作没有任何问题。但是,我希望在线程之间共享记录器对象。然后,我不希望它打印出这样的内容:

//LogFILE
Log this and this Something else
 and 10

所以,我需要锁定整个operator&lt;&lt;s 链。我猜这可以用 RAII 来完成,我还没有考虑太多。与此同时,是否有任何传统的方式来完成这项工作? (除了用操纵器结束输入?)

【问题讨论】:

  • 您的记录器线程安全吗?如果是这样,您可能只需将字符串放在一边(考虑类似:stackoverflow.com/questions/469696/…)并记录单个 std::string,但一般情况下,请遵循 @Nim 的建议并锁定。

标签: c++ multithreading method-chaining


【解决方案1】:

Nim 的回答略有不同:

创建

class LockedLog {
    static MutEx mutex; // global mutex for logging
    ScopedLock lock; // some scoped locker to hold the mutex
    Logger &oLogger; // reference to the log writer itself
public:
    LockedLog(Logger &oLogger) : oLogger(oLogger), lock(mutex) {}
    template <typename T>
    LockedLog &operator<<(const T &value) { oLogger << value; return *this; }
};

要么这样做:

LockedLog(oLogger) << "Log this" << " and this " << " and " << 10 << endl;

或者把Logger::operator&lt;&lt;改成普通方法,在LockedLog::operator&lt;&lt;中调用这个方法,给Logger加上cast-operator:

operator LockedLog() { return LockedLog(*this); }

这应该为您当前的代码添加锁定。

更新:锁定所有对 operator&lt;&lt; 的调用,可能甚至锁定对其参数的评估(取决于编译器是先评估左参数还是右参数它可以选择)。为了减少这种情况,可以:

class LockedLog {
    static MutEx mutex; // global mutex for logging
    std::stringstream buffer; // temporary formatting buffer;
    Logger &oLogger; // reference to the log writer itself
public:
    LockedLog(Logger &oLogger) : oLogger(oLogger), lock(mutex) {}
    template <typename T> 
    LockedLog &operator<<(const T &value) { buffer << value; return *this; }
    ~LockedLog() { ScopedLock lock(mutex); oLogger << buffer.str() << std::flush; }
};

但是stringstream 增加了另一个开销。

【讨论】:

  • 与 Nim 的回答相同的批评者:您锁定的时间超过了必要的时间。
  • 确实如此。可以通过使用临时缓冲区(字符串流)来减少锁定。不过,这会增加额外的开销,因此取决于有多少线程,它可能会或可能不会得到回报。
  • operator&lt;&lt; 不适用于 endl。它给出以下错误:“LockedLog::operator Logger &operator<<(const T& value) { cout << value; return oLogger; },但我不确定这是否符合预期。
  • @JohannesGerer:是的,它确实有效(除了最后一个带有 stringstream 的变体),因为临时对象仅在语句结束时被销毁。另一种选择是像 ostream 本身一样添加LockedLog &amp;operator&lt;&lt;(ostream&amp; ( *pf )(ostream&amp;)) 重载,其优点是如果您也想从操纵器开始,它也可以工作。
  • @JohannesGerer:我说过它确实有效,除了在带有 stringstream 的变体中,但它也可以在那个变体中工作,只是你必须在那里返回 stringstream。 (哦,stringstream 变体可能也应该刷新,因为你不能在那里刷新 Logger;添加。)
【解决方案2】:

一种方法是使用宏,即

#define LOG(x) \
{\
  <acquire scoped lock> \
  oLogger << x; \
}

然后

LOG("Log this" << " and this" << " and " << 10 << endl);

我也使用您上面提到的操纵器方法完成了它,但是问题是您需要为所有类型实现operator&lt;&lt;(即不能使用现有的标准运算符)

编辑:为了减少持有锁的时间,考虑这样的事情:

#define LOG(x) \
{\
  std::ostringstream str; \
  str << x; \       // the streaming happens in local scope, no need for lock
  oLogger.write(str.str()); \ // ensure the write method acquires a lock
}

【讨论】:

  • +1,原子性只能通过锁来保证,而且,如果没有锁,你甚至可能会导致意想不到的结果(可能是UB?),这取决于记录器的实现(它是可重入的吗?它使用全局状态吗?)
  • LOG("current context: " &lt;&lt; context.dump()) 表示如果dump 执行大量计算,您将在很长一段时间内阻止日志记录。最好将格式化阶段与日志记录分开,只有后者需要锁定。
  • @Matthieu,同意,但是例如将上述宏更改为流式传输到临时 stringstream 然后将结果写入 oLogger 是微不足道的 - 重点是强调它是锁定很重要...
  • 我同意这是微不足道的,但它仍然很重要。至于加锁,不知道有没有针对多客户的无锁队列好的实现。我认为 FF 是通过组合多个 SPSC 无锁队列来做到的。
【解决方案3】:

我发现最好的解决办法是写一个类buffer这样

buffer(oLogger) << "Log this" << " and this" << " and " << 10 << endl;

创建一个临时缓冲区对象,捕获并格式化输出并将其写入析构函数中的oLogger。这可以通过包装stringstream 轻松完成。因为每个线程都有自己的缓冲区,所以格式化是独立的。

为了额外的花哨,buffer::~buffer 可以使用几种不同的机制来防止对oLogger 的线程不安全访问。您假设来自多个线程的operator&lt;&lt; 调用可能是交错的。事实上,情况更糟;它们可以是并发的。你可以得到“LSoogm ethhiinsg else”。确保一次只有一个 buffer 刷新到 oLogger 可以防止这种情况发生。

【讨论】:

    【解决方案4】:

    我可能会在这里使用表达式模板。

    主要思想是在格式化阶段获取锁是非常愚蠢的,特别是因为在格式化期间可能会有函数调用。

    您需要使用两个不同的阶段:

    • 格式化日志
    • 自动发布日志

    这可以通过表达式模板来完成:

    1. 第一次调用 Logger::operator&lt;&lt; 会生成一个 LoggerBuffer,其中嵌入了对 Logger 的引用。
    2. 后续调用在处理所有格式混乱的LoggerBuffer 上执行
    3. 在销毁LoggerBuffer(在语句末尾)时,它会锁定Logger,传递格式化的字符串,然后解锁(除非你有一个无锁队列或其他东西)

    【讨论】:

      【解决方案5】:

      由于我必须国际化日志,我更喜欢这样的东西:

      oLogger << myAutosprintf(_("My wonderful %s ! I have %d apples"), name, nbApple);
      

      翻译更好:) 它会解决你的问题。 _() 是翻译的捷径。

      您可以使用gnu::autosprintfboost.format(感谢 Jan Huec),或者自己编写。

      my2c

      NB:在好评后编辑(太快了,感谢 cmets)。我删除了错误的“第一部分”声明

      【讨论】:

      • 即使它是operator&lt;&lt; 也是线程安全的(假设内部锁定),但不能保证如果两个线程尝试在他们输出的问题中记录消息时不会交错:“记录这个和这个记录这个和这个和和..."
      • 我认为您的第一个语句不正确,第一次调用的结果 operator&lt;&lt; 然后用于第二次调用,依此类推,所以这很重要。您可能会在第一次调用时获得锁,问题是何时释放它(因此是操纵器的要求)
      • @david, Nim:感谢您的发言。我的观点主要是使用构建消息的类...我想知道() 是否可以解决问题...oLogger &lt;&lt; (x &lt;&lt; y &lt;&lt; z &lt;&lt; endl);
      • gnu::autosprintf 看起来很有趣。但它不是类型安全的。 boost.format (boost.org/doc/libs/release/libs/format/index.html) 是。它比较慢,因为它通过stringstream,但另一方面也允许它使用自定义operator&lt;&lt; 格式化对象。
      猜你喜欢
      • 2018-04-10
      • 1970-01-01
      • 1970-01-01
      • 2021-03-19
      • 2018-11-08
      • 2020-11-17
      • 2023-03-19
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多