问题背景
在一个项目中,有ConfigReceiver
和ConfigApply
两个进程需要通过配置文件进行协作。ConfigReceiver
进程负责接收远程发送过来的配置信息,并写入配置文件config.ini
,而ConfigApply
进程中有一个定时器定时(1s)读取config.ini
,如果发现配置更改,就应用新的配置。(请不要吐槽这种机制,历史原因,咳咳。。)
而config.ini
所在的文件夹中,只有config.ini
一个文件会发生改变。
分析
在定时器的回调里,每次都读取config.ini
,代价太高,所以考虑监控config.ini文件的变化情况,只有当其内容发生变化时,才去实际地读取文件。
Windows下,可以监控文件(夹)改变的API有两个:FindFirstChangeNotification
和ReadDirectoryChangesW
,前者能监控文件夹发生变化,但无法知道具体是哪个文件发生了变化;后者则可以具体到文件。
上述问题中,由于文件夹中可变的文件只有一个,所以很自然地选择使用前者。
解决方案
在ConfigApply
进程的主线程中设置定时器及一个bool值bFileChange
,初始化为true
。然后开启一个线程中,在线程中使用FindFirstChangeNotification
监视config.ini
所在文件夹的状态变化。如果状态发生变化,就将bFileChange
设为true
。
在定时器的回调里,只有bFileChange
为true
时,才读取config.ini
并判断配置是否发生改变,并设置bFileChange
为false
。
代码如下:
ConfigReceiver
进程,main.cpp:
/*
ConfigReceiver进程,使用随机Sleep()函数模拟接收远程配置。
*/
#include <Windows.h>
#include <tchar.h>
#include <time.h>
#include <iostream>
LPCTSTR lpszDir = _T("D:\\");
LPCTSTR lpszFile = _T("D:\\config.ini");
LPCTSTR lpszSection = _T("Switch");
LPCTSTR lpszKey = _T("Open");
int main()
{
srand(_time32(NULL));
SYSTEMTIME st = {0};
while (true)
{
// 随机睡眠
DWORD ms = rand() % 10; // 0-9
Sleep(ms * 1000);
// 更改配置
::WritePrivateProfileString(lpszSection, lpszKey, 0 == ms % 2 ? _T("0") : _T("1"), lpszFile);
::GetLocalTime(&st);
std::cout << st.wMinute << ":" << st.wSecond << "\t更改配置:" << ms % 2 << std::endl;
}
return 0;
}
ConfigApply
进程,main.cpp:
/*
ConfigApply进程。
*/
#include <Windows.h>
#include <tchar.h>
#include <process.h>
#include <iostream>
LPCTSTR lpszDir = _T("D:\\");
LPCTSTR lpszFile = _T("D:\\config.ini");
LPCTSTR lpszSection = _T("Switch");
LPCTSTR lpszKey = _T("Open");
bool g_bFileChange = true;
int g_nOpen = 0;
bool g_bWatch = true;
// 定时器回调函数
VOID CALLBACK TimerProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime);
// 文件夹监控线程函数
unsigned int __stdcall ThreadProc(void* param);
// 应用配置
void Apply();
int main()
{
// 开启定时器和线程
UINT_PTR uTimerID = ::SetTimer(NULL, 0, 1000, &TimerProc);
HANDLE hThread = reinterpret_cast<HANDLE>(_beginthreadex(NULL, 0, &ThreadProc, NULL, 0, NULL));
MSG msg;
while (::GetMessage(&msg, NULL, 0, 0))
{
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
::KillTimer(NULL, uTimerID);
uTimerID = 0;
g_bWatch = false;
DWORD dwWait = ::WaitForSingleObject(hThread, 2000);
if (WAIT_OBJECT_0 != dwWait)
{
::TerminateThread(hThread, -1);
}
::CloseHandle(hThread);
hThread = NULL;
return 0;
}
VOID CALLBACK TimerProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime)
{
if (g_bFileChange)
{
g_bFileChange = false;
int nOpen = ::GetPrivateProfileInt(lpszSection, lpszKey, g_nOpen, lpszFile);
if (nOpen != g_nOpen)
{
g_nOpen = nOpen;
Apply();
}
else
{
SYSTEMTIME st = {0};
::GetLocalTime(&st);
std::cout << st.wMinute << ":" << st.wSecond << "\t文件变化,但配置不变" << std::endl;
}
}
}
unsigned int __stdcall ThreadProc(void* param)
{
// 只监视实际的写入
HANDLE hFind = ::FindFirstChangeNotification(lpszDir, FALSE, FILE_NOTIFY_CHANGE_LAST_WRITE);
if (INVALID_HANDLE_VALUE == hFind)
{
return -1;
}
while (g_bWatch)
{
DWORD dwWait = ::WaitForSingleObject(hFind, 1000);
if (WAIT_OBJECT_0 == dwWait)
{
g_bFileChange = true; // 文件改变
if (!::FindNextChangeNotification(hFind))
{
::FindCloseChangeNotification(hFind);
hFind = NULL;
return -2;
}
}
}
::FindCloseChangeNotification(hFind);
hFind = NULL;
return 0;
}
void Apply()
{
SYSTEMTIME st = {0};
::GetLocalTime(&st);
std::cout << st.wMinute << ":" << st.wSecond << "\t配置改变,应用成功" << std::endl;
}
运行结果如下:
优化
可以看到,在ConfigApply
进程退出时,要首先等待监视线程的退出。虽然最坏的情况下也只需等待1s,但在实际的应用中,还是很明显的。
为了改善这一情况,我刚开始考虑减少监视线程每次wait的时间。但是要将延迟降低到不可察觉的程度,如200ms,又会增加CPU的占用。
基于项目的实际情况,我在ConfigApply
进程退出时,自行修改了一下配置文件以触发wait:
::WritePrivateProfileString(lpszSection, lpszKey, 0 == g_nOpen ? _T("0") : _T("1"), lpszFile); DWORD dwWait = ::WaitForSingleObject(hThread, 2000);
,
则监视线程就可以立刻退出了。而且监视线程中的等待时间也可以设置为无限长:
DWORD dwWait = ::WaitForSingleObject(hFind, INFINITE);
。