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

How to synchronize access to a global variable with very frequent reads / very rare writes?

本文关键字:非常 访问 全局变量 何同步 同步 读取      更新时间:2023-10-16

我正在为一个服务器应用程序调试日志基础设施。源代码中的每个日志点在其他参数中指定其级别(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. 我的解决方案是否保证当前调试日志级别的更改将在可接受的时间(比如几秒钟)后到达所有线程?理想情况下,我希望级别更改操作是同步的,以便当用户收到级别更改操作的确认时,她可以指望根据新级别发出的后续日志。

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

如果不提供某种栅栏在写和读之间创建一个'happens before'边界,则不要求在一个内核上的一个线程所做的写操作对于在另一个内核上读操作的另一个线程是可见的。

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

在实践中,如果并发应用程序在其他地方使用了锁,并且如果写操作不可见,您的程序将继续或多或少地正常运行,那么很可能在短时间内由于其他fencing操作而使写操作变得可见,并满足您的要求。所以你可能只需要写出来,然后跳过栅栏就可以了。

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

假设你在Windows上,Windows只运行在x86上(目前大多数情况下是正确的,但可能会改变…),并且假设只有一个线程写入变量,你可以不做任何同步。

要"正确",您应该使用某种形式的读写锁。

考虑到您当前的实现,我建议您看一下原子操作。如果这只适用于Windows,看看Interlocked Variable Access

看看Vista和7上新的Slim Reader/Writer锁。它们应该以尽可能少的开销做你想做的事情:

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

在x86和x64上,volatile只会带来很少的直接成本。强制重新获取不相关的变量可能会有一些间接成本(对易失性变量的访问被视为所有其他"地址占用"变量的编译器级内存围栏)。可以把volatile变量看作函数调用,因为编译器在调用过程中会丢失有关内存状态的信息。

在安腾上,挥发性有一些成本,但不是太糟糕。在ARM上,MSVC编译器默认不为volatile提供屏障(也不提供排序)。

重要的一点是,在您的程序中应该至少有一次访问这个日志级别变量,否则它可能会变成一个常量并被优化掉。如果您打算通过除调试器之外的任何机制来设置变量,这可能是一个问题。

定义在作用域中可见的顺序变量,并在适当的时候更新它(当日志级别发生变化时)如果数据是正确对齐的(即默认的),那么你不需要任何特殊的东西,除了声明你当前的日志变量为"volatile"。这将适用于LONG大小(32位序数)。所以你的解决方案是:

extern volatile long g_globalLogLevel;

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