我正在研究的一些C ++库具有一个简单的跟踪机制,可以激活它以生成日志文件,显示调用了哪些函数以及传递了哪些参数。它基本上归结为一个TRACE
宏溢出到整个库的源头,宏扩展到这样的东西:
typedef void(*TraceProc)( const char *msg );
/* Sets 'callback' to point to the trace procedure which actually prints the given
* message to some output channel, or to a null trace procedure which is a no-op when
* case the given source file/line position was disabled by the client.
*
* This function also registers the callback pointer in an internal data structure
* and resets it to zero in case the filtering configuration changed since the last
* invocation of updateTraceCallback.
*/
void updateTraceCallback( TraceProc *callback, const char *file, unsinged int lineno );
#define TRACE(msg) \
{ \
static TraceProc traceCallback = 0; \
if ( !traceCallback ) \
updateTraceCallback( &traceCallback, __FILE__, __LINE__ ); \
traceCallback( msg ); \
}
这个想法是人们可以在他们的代码中说TRACE("foo hit")
,然后也可以
调用一个调试打印功能,否则它将是一个无操作。他们可以使用其他一些API(此处未显示)来配置只应打印位置(源文件/行号)中的TRACE
使用。此配置可在运行时更改。
这个问题是现在应该在多线程代码库中使用这个想法。因此,TRACE
展开的代码需要在面对同时运行代码的多个执行线程时正常工作。现在代码库中有大约20,000个不同的跟踪点,它们经常被击中,因此它们应该非常有效
使这种方法线程安全的最有效方法是什么?我需要一个适用于Windows(XP和更新版本)和Linux的解决方案。我害怕过度锁定只是为了检查过滤器配置是否发生了变化(99%的时间点到了跟踪点,配置没有改变)。我也愿意对宏进行更大的更改。因此,如果宏只是将事件发送到不同线程中的事件循环(假设访问事件循环是线程安全的)并且所有处理都发生在同一个线程中,那么它也可以接受,而不是讨论互斥体与临界区性能。线程,所以它使用事件循环进行同步。
更新:我可以简化这个问题:
如果我有一个线程读取指针,另一个可能写入变量的线程(但99%的时间没有),我怎么能避免读取线程需要一直锁定?
答案 0 :(得分:2)
我仍然不完全理解这个问题,所以请纠正我没有得到的任何问题。
(我正在抛弃反斜杠。)
#define TRACE(msg)
{
static TraceProc traceCallback = NULL;
TraceProc localTraceCallback;
localTraceCallback = traceCallback;
if (!localTraceCallback)
{
updateTraceBallback(&localTraceCallback, __FILE__, __LINE__);
// If two threads are running this at the same time
// one of them will update traceCallback and get it overwritten
// by the other. This isn't a big deal.
traceCallback = localTraceCallback;
}
// Now there's no way localTraceCallback can be null.
// An issue here is if in the middle of this executing
// traceCallback gets null'ed. But you haven't specified any
// restrictions about this either, so I'm assuming it isn't a problem.
localTraceCallback(msg);
}
答案 1 :(得分:2)
您可以实现配置文件版本变量。程序启动时,它设置为0.宏可以保存一个静态int,它是它看到的最后一个配置版本。然后,最后看到的和当前配置版本之间的简单原子比较将告诉您是否需要执行完全锁定并重新调用updateTraceCallback();
。
这样,99%的时间你只会添加一个额外的原子操作,或内存屏障或类似的东西,这是非常便宜的。 1%的时间,只需完整的mutex
,它不会以任何明显的方式影响您的表现,如果它只有1%的时间。
修改强>
一些.h文件:
extern long trace_version;
一些.cpp文件:
long trace_version = 0;
宏:
#define TRACE(msg)
{
static long __lastSeenVersion = -1;
static TraceProc traceCallback = 0;
if ( !traceCallback || __lastSeenVersion != trace_version )
updateTraceCallback( &traceCallback, &__lastSeenVersion, __FILE__, __LINE__ );
traceCallback( msg );
}
增加版本和更新的功能:
static long oldVersionRefcount = 0;
static long curVersionRefCount = 0;
void updateTraceCallback( TraceProc *callback, long &version, const char *file, unsinged int lineno ) {
if ( version != trace_version ) {
if ( InterlockedDecrement( oldVersionRefcount ) == 0 ) {
//....free resources.....
//...no mutex needed, since no one is using this,,,
}
//....aquire mutex and do stuff....
InterlockedIncrement( curVersionRefCount );
*version = trace_version;
//...release mutex...
}
}
void setNewTraceCallback( TraceProc *callback ) {
//...aquire mutex...
trace_version++; // No locks, mutexes or anything, this is atomic by itself.
while ( oldVersionRefcount != 0 ) { //..sleep? }
InterlockedExchange( &oldVersionRefcount, curVersionRefCount );
curVersionRefCount = 0;
//.... and so on...
//...release mutex...
当然,这是非常简化的,因为如果您需要升级版本和oldVersionRefCount > 0
,那么您就遇到了麻烦;如何解决这个问题取决于你,因为它真的取决于你的问题。我的猜测是,在这些情况下,您可以等到引用计数为零,因为引用计数递增的时间应该是运行宏所需的时间。
答案 2 :(得分:1)
您的评论说“如果过滤配置在运行时发生变化,则将其重置为零”,但我是否正确地将其视为“当过滤配置发生变化时将其重置为零”?
如果不确切知道updateTraceCallback如何实现其数据结构,或者它所引用的其他数据以决定何时重置回调(或者实际上是首先设置它们),则无法判断什么是安全的。类似的问题适用于知道traceCallback的作用 - 例如,如果它访问共享输出目的地。
鉴于这些限制,唯一不需要重新编写其他代码的安全建议是在整个批次(或最好是Windows上的关键部分)中粘贴互斥锁。
答案 3 :(得分:1)
我害怕过度锁定只是为了检查过滤器配置是否发生了变化(99%的时间点到了跟踪点,配置没有改变)。我也愿意对宏进行更大的更改。因此,如果宏只是将事件发送到不同线程中的事件循环(假设访问事件循环是线程安全的),而不是讨论互斥体与临界区性能,那么它也是可以接受的。
您认为如何在没有锁的情况下实现线程之间的线程安全消息传递?
无论如何,这是一个可行的设计:
必须更改保存过滤器的数据结构,以便从堆中动态分配,因为我们将创建多个过滤器实例。此外,它还需要添加引用计数。你需要一个类似于:
的typedeftypedef struct Filter
{
unsigned int refCount;
// all the other filter data
} Filter;
在某处宣布了单身“当前过滤器”。
static Filter* currentFilter;
并使用一些默认设置进行初始化。
在您的TRACE宏中:
#define TRACE(char* msg)
{
static Filter* filter = NULL;
static TraceProc traceCallback = NULL;
if (filterOutOfDate(filter))
{
getNewCallback(__FILE__, __LINE__, &traceCallback, &filter);
}
traceCallback(msg);
}
filterOutOfDate()
只是将过滤器与currentFilter进行比较,看它是否相同。仅仅比较地址就足够了。它没有锁定。
getNewCallback()
应用当前过滤器来获取新的跟踪功能,并使用当前过滤器的地址更新传入的过滤器。它的实现必须使用互斥锁进行保护。此外,它会减少原始过滤器的refCount,并递增新过滤器的refCount。这是我们知道何时可以释放旧过滤器。
void getNewCallback(const char* file, int line, TraceProc* newCallback, Filter** filter)
{
// MUTEX lock
newCallback = // whatever you need to do
currentFilter->refCount++;
if (*filter != NULL)
{
*filter->refCount--;
if (*filter->refCount == 0)
{
// free filter and associated resources
}
}
*filter = currentFilter;
// MUTEX unlock
}
如果要更改过滤器,可以执行类似
的操作changeFilter()
{
Filter* newFilter = // build a new filter
newFilter->refCount = 0;
// MUTEX lock (same mutex as above)
currentFilter = newFilter;
// MUTEX unlock
}
答案 4 :(得分:1)
如果我有一个线程读取指针,另一个线程可能写入变量(但99%的时间没有),我怎样才能避免读取线程需要一直锁定? / p>
从您的代码中,可以使用updateTraceCallback()
内的互斥锁,因为它很少被调用(每个位置一次)。在获取互斥锁之后,检查traceCallback
是否已经初始化:如果是,那么其他线程就是为您完成的,没有什么可做的。
如果updateTraceCallback()
由于全局互斥锁上的冲突而导致严重的性能问题,那么您可以简单地创建一个互斥锁数组并使用traceCallback
指针的散列值作为互斥数组的索引。这会在许多互斥锁上传播锁定并最大限度地减少碰撞次数。
答案 5 :(得分:0)
#define TRACE(msg) \
{ \
static TraceProc traceCallback = \
updateTraceBallback( &traceCallback, __FILE__, __LINE__ ); \
traceCallback( msg ); \
}