在一个很小的应用程序中,我想加入记录日志的功能,已辅助分析判断一些诸如登录被拒绝等事件的原因。由于这个小程序是一个对话框程序,基本都是在内存中运行的,而记录日志则需要频繁的开闭文件,写文件,从“自觉”感觉我有一点担心记录日志会影响运行效率。因此为了不影响UI线程的响应性能,我决定新开一个单独的线程专门做这件事,称其为“日志线程”。
同时,为了降低访问文件的频率,我又在内存中(静态区)放置一个日志缓冲区(Log Buffer),当写日志时,把内容首先追加到日志缓冲区,然后由UI线程定期的通知日志线程去把缓冲区内容输出到文件。也就是采用下面的模型:
【UI线程】 ---> | 日志缓冲区 | <--- 【日志线程】
|
|------------->【日志文件】
我们通过CreateThread开启一个新的线程,当我们需要输出缓冲区时,则通过 PostThreadMessage 方法发送一个我们自定义的消息给线程,告诉线程去写文件。我们的日志线程 Procedure 中有一个消息循环,用于接收UI线程发送的消息。但是一个线程开启以后系统可能还没有为线程创建一个消息队列,所以我们要求系统为我们的线程创建消息队列,这是通过调用 PeekMessage(&msg, NULL, WM_USER, WM_USER, PM_NOREMOVE); 来完成的。
注意最后一个参数表示我们提取消息的目的只是用于“查看”,而不实际“处理”它,因此不要把消息从消息队列中移除。
必须在线程准备好消息队列后发送消息,否则发送消息则可能失败。因此我们在开启线程后,创建一个事件(event)用于等待线程做好准备工作:
在我们的线程过程中,我们在最开始调用PeekMessage,要求系统创建消息队列:
在上面的线程过程中,当收到 WM_WRITELOG 消息时,就会访问缓冲区,把缓冲区内容输出到文件,并清空缓冲区。需要注意的是,Log Buffer同时被UI线程和日志线程使用和访问。当我们的日志线程在输出缓冲区内容期间,很显然我们不希望 UI线程再来访问它(例如向缓冲区输入日志内容),否则缓冲内容就会被破坏,从而导致无法预料的结果,因此 LOGBUFFER 相对于这两个线程来说,属于一种“独占性”资源,因此我们必须把访问“独占性”资源的代码作为关键代码段(Critical Section),系统保证进入Critical Section(拥有对资源访问权)的线程在任一时刻只有一个。而应用程序必须明确的告知系统它需要进入关键代码段(通过调用 EnterCriticalSection)并愿意在此等待直到资源已属于空闲状态,而当它访问完毕以后必须告知系统它已离开(LeaveCriticalSection)(否则其他等待资源的线程就无法被调度)。
我们通过下面的API函数创建Critical Section:
CRITICAL_SECTION m_csLog;
InitializeCriticalSection(&m_csLog); //初始化
//这里是CriticalSection的使用期间
DeleteCriticalSection(&m_csLog); //释放
类似的在UI线程里面,我们要访问 logbuffer 时:
在这里的代码只是为了说明基本问题而进行了一定的简化。这里的对资源的划分应该尽量合理,并且只有再真正需要访问资源时,才进入 Critical Section,而不要在 CriticalSection 中做任何多余的工作。同时,可以资源的划分要尽可能保持独立性,不要把无关的资源放置到同一个 Section 中,以免引发无谓的等待,而影响线程被调度时的效率。
===================================
补充
===================================
(1)当程序退出时,我们希望日志线程可以把还留在缓冲区中的数据输出到文件,然而这看起来有点不容易。因为 我们只有一个 PostThreadMessage 的异步方法,而没有相对应 SendMessage 那种强制当前线程等待对方处理结束的同步方法。
当我先发送 WM_WRITELOG 然后发送 WM_QUIT 消息给日志线程时,我发现日志线程结束了,但是它似乎在结束前没有处理 WM_WRITELOG 消息,当然具体原因还有待检查。由于 WM_QUIT 并不像我们想象的其他消息那样是追加进入队列的,相反,它只是改变了一个 影响 GetMessage 返回值的flag。这里的处理流程对程序员而言是透明的,所以我只能凭自己的自觉猜测,GetMessage 函数首先检测了这个flag,当发现“接收过”WM_QUIT 时就返回了FALSE 。由于多线程处理方式以及 WM_QUIT 的特殊性,导致 WM_WRITELOG 消息可能发送失败了,因为它尝试入列时似乎线程的消息队列已被系统回收了。
由于没有 SendThreadMessage 这样的函数(可能是为了防止线程之间死锁),所以我采用一种看起来比较不是哪么可靠但确实有用的方式,即在发送 WM_WRITELOG 消息后,主动让出CPU时间片,让自己休眠一会200毫秒(我们乐观的估计在此期间日志线程有足够的时间处理完毕该消息),然后再发送WM_QUIT 消息结束日志线程,事实证明这个方法确实有用,但它显得很“盲目”,所以不能说是很好很可靠的方式。
【补充2:对补充的补充】 hoodlum1980 , 2011-9-2
首先重新描述一下补充中提到的问题:
对于进程需要退出时,通过给进程的主线程发送 WM_QUIT 实现。由于日志线程是周期性把缓存中的日志写入磁盘,因此就存在这个问题,进程退出前,应保证日志线程把缓冲区中的剩余内容也写入磁盘。但仅通过在退出时机时,给日志线程 Post 一个 WM_WRITELOG 是不够的!(因为已经开始尝试结束所有线程,因此日志线程未必有机会处理掉这个消息)。
回顾这个问题,可以发现这是一个典型的线程同步问题。
因此,这个问题也可以用典型的线程同步方法结局。当然需要一些新的变动。例如,在 PostQuitMessage 之前,我们同样还是通过 PostThreadMessage 的方式,通知日志线程(我们可以定义另一个消息,例如 WM_WRITELOG2)把缓冲区内容保存到磁盘文件中,这时候我们创建一个信号或事件,然后用 WaitSingleObject 等待它。在日志线程中如果保存完毕,我们就设置信号/事件。
这样就显得比之前的“盲目等待200毫秒”的方法要可靠的多。