如何使用非常频繁的读取/非常罕见的写入来同步对全局变量的访问?

时间:2011-09-30 18:50:42

标签: c++ c windows multithreading thread-safety

我正在为服务器应用程序开发调试日志记录基础结构。源代码中的每个日志记录点都指定其级别(CRITICAL,ERROR等)以及其他参数。 所以在源代码中,日志点看起来像:

DBG_LOG_HIGH( … )

这是一个扩展为

的宏
if ( CURRENT_DEBUG_LOG_LEVEL >= DEBUG_LOG_LEVEL_HIGH ) {
   // prepare and emit log record
}

其中DEBUG_LOG_LEVEL_HIGH是预定义的常量(假设为2),而CURRENT_DEBUG_LOG_LEVEL是一个表达式,用于计算用户设置的当前调试日志记录级别。 最简单的方法是将CURRENT_DEBUG_LOG_LEVEL定义为:

extern int g_current_debug_log_level;
#define CURRENT_DEBUG_LOG_LEVEL (g_current_debug_log_level)

我想允许用户在应用程序执行期间更改当前的调试日志记录级别,并且可以让更改生效几秒钟。该应用程序是多线程的,对g_current_debug_log_level的更改可以很容易地序列化(例如通过CRITICAL_SECTION),但为了不影响性能表达式( CURRENT_DEBUG_LOG_LEVEL >= DEBUG_LOG_LEVEL_HIGH )应该尽快执行,所以我会喜欢避免在那里使用任何线程同步机制。

所以我的问题是:

  1. g_current_debug_log_level读取中是否缺少同步导致读取的值不正确?虽然它不应该影响应用程序的正确性,因为用户可能将当前的调试日志记录级别设置为不正确的值,但它可能会影响应用程序性能,因为它可能导致它在不可控制的时间段内发出非常大量的调试日志。

  2. 我的解决方案是否可以保证当前调试日志记录级别的更改将在可接受的时间后到达所有线程(比方说几秒钟)?理想情况下,我希望级别更改操作是同步的,这样当用户收到关于级别更改操作的确认时,她可以指望后续日志按照新级别发出。

  3. 我也非常感谢满足上述要求的替代实现的任何建议(对级别比较和同步级别更改的最小性能影响,延迟不超过几秒)。

6 个答案:

答案 0 :(得分:4)

没有什么要求在一个核心上的一个线程上进行的写入将对另一个核心读取的另一个线程变得可见,而不提供某种类型的围栏以在写入和读取之间创建“先发生”边缘

因此,为了严格正确,您需要在写入日志级别之后以及每次读取之前插入适当的内存栅栏/屏障指令。围栏操作并不便宜,但它们比完全吹制的互斥锁便宜。

在实践中,给定一个在其他地方使用锁定的并发应用程序,以及如果写入不可见,程序将继续或多或少地正常运行的事实,很可能写入将偶然显示由于其他围栏操作在短时间内完成并符合您的要求。所以你可以通过编写和跳过围栏来逃脱。

但是使用适当的防护来强制执行边缘之前的确实是正确的答案。 FWIW,C ++ 11提供了一个显式内存模型,它定义了语义并在语言级别公开了这些类型的fencing操作。但据我所知,还没有编译器实现新的内存模型。因此,对于C / C ++,您需要使用库中的锁或显式屏蔽。

答案 1 :(得分:1)

假设您使用的是Windows,而Windows仅在x86上运行(现在大部分都是真的,但可能会改变......),并假设只有一个线程会写入变量,您可以在不进行任何同步的情况下离开任何。

要“正确”,你应该使用某种形式的读写器锁。

答案 2 :(得分:0)

鉴于您当前的实现,我建议您看一下原子操作。如果这仅适用于Windows,请查看Interlocked Variable Access

答案 3 :(得分:0)

查看Vista和7上可用的新Slim Reader / Writer锁。他们应尽可能少地执行您想要的操作:

http://msdn.microsoft.com/en-us/library/windows/desktop/aa904937(v=vs.85).aspx

答案 4 :(得分:0)

在x86和x64上,volatile只会产生很少的直接成本。可能存在一些与强制重新获取不相关变量相关的间接成本(对volatile变量的访问被视为所有其他“地址采用”变量的编译器级内存屏障)。可以将volatile变量看作函数调用,因为编译器将丢失有关调用内存状态的信息。

在Itanium上,volatile有一些成本,但也不算太糟糕。在ARM上,MSVC编译器默认不为volatile提供障碍(而不是提供排序)。

一个重要的事情是程序中应该至少有一个访问此日志级别变量的权限,否则它可能会变为常量并进行优化。如果您打算通过除调试器之外的其他机制设置变量,则可能会出现问题。

答案 5 :(得分:0)

定义作用域中可见的序数变量并根据需要进行更新(当日志级别更改时) 如果数据正确对齐(即默认值),那么除了将当前日志变量声明为“volatile”之外,您不需要任何特殊内容。这适用于LONG尺寸(32位序数)。 所以你的解决方案是:

extern volatile long g_globalLogLevel;

无需外部同步(即RWlock / CriticalSection / Spin等)