Windows下使用FindFirstChangeNotification监控文件夹变化

问题背景

在一个项目中,有ConfigReceiverConfigApply两个进程需要通过配置文件进行协作。ConfigReceiver进程负责接收远程发送过来的配置信息,并写入配置文件config.ini,而ConfigApply进程中有一个定时器定时(1s)读取config.ini,如果发现配置更改,就应用新的配置。(请不要吐槽这种机制,历史原因,咳咳。。)
config.ini所在的文件夹中,只有config.ini一个文件会发生改变。

分析

在定时器的回调里,每次都读取config.ini,代价太高,所以考虑监控config.ini文件的变化情况,只有当其内容发生变化时,才去实际地读取文件。
Windows下,可以监控文件(夹)改变的API有两个:FindFirstChangeNotificationReadDirectoryChangesW,前者能监控文件夹发生变化,但无法知道具体是哪个文件发生了变化;后者则可以具体到文件。
上述问题中,由于文件夹中可变的文件只有一个,所以很自然地选择使用前者。

解决方案

ConfigApply进程的主线程中设置定时器及一个bool值bFileChange,初始化为true。然后开启一个线程中,在线程中使用FindFirstChangeNotification监视config.ini所在文件夹的状态变化。如果状态发生变化,就将bFileChange设为true
在定时器的回调里,只有bFileChangetrue时,才读取config.ini并判断配置是否发生改变,并设置bFileChangefalse

代码如下:
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;
}

运行结果如下:


image.png

优化

可以看到,在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);

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,444评论 4 365
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,867评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,157评论 0 248
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,312评论 0 214
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,673评论 3 289
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,802评论 1 223
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,010评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,743评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,470评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,696评论 2 250
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,187评论 1 262
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,538评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,188评论 3 240
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,127评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,902评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,889评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,741评论 2 274

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,105评论 18 139
  • linux资料总章2.1 1.0写的不好抱歉 但是2.0已经改了很多 但是错误还是无法避免 以后资料会慢慢更新 大...
    数据革命阅读 12,028评论 2 34
  • Ubuntu的发音 Ubuntu,源于非洲祖鲁人和科萨人的语言,发作 oo-boon-too 的音。了解发音是有意...
    萤火虫de梦阅读 98,544评论 9 468
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,019评论 11 349
  • 孩子,是一生最大的事业。 对于这句话,若已为人父母,应该都是认可的。很多人都把自己的100%,送给了自己的孩子,可...
    竹中剑阅读 720评论 0 0