养成一个好习惯,调用 windows api 之前一定要先看文档
regnotifychangekeyvalue 函数 (winreg.h) - win32 apps | microsoft learn
同步阻塞模式
regnotifychangekeyvalue
的最后一个参数传递false
,表示以同步的方式监听。
同步模式会阻塞调用线程,直到监听的目标发生更改才会返回,如果在ui线程上调用,则会导致界面卡死,因此我们一般不会直接在主线程上同步监听,往往是创建一个新的线程来监听。
示例代码因为是控制台程序,因此没有创建新的线程。
registrykey hkey = registry.currentuser.createsubkey("software\\1-regmonitor"); string changebefore = hkey.getvalue("testvalue").tostring(); console.writeline($"testvalue的当前值是:{changebefore}, 时间:{datetime.now:hh:mm:ss}"); //此处创建一个任务,5s之后修改testvalue的值为一个新的guid task.delay(5000).continuewith(t => { string newvalue = guid.newguid().tostring(); console.writeline($"testvalue的值即将被改为:{newvalue}, 时间:{datetime.now:hh:mm:ss}"); hkey.setvalue("testvalue", newvalue); }); int ret = regnotifychangekeyvalue(hkey.handle, false, regnotifyfilter.changelastset, new safewaithandle(intptr.zero, true), false); if(ret != 0) { console.writeline($"出错了:{ret}"); return; } string currentvalue = hkey.getvalue("testvalue").tostring(); console.writeline($"testvalue的最新值是:{currentvalue}, 时间:{datetime.now:hh:mm:ss}"); hkey.close(); console.readline();
运行结果:
异步模式
regnotifychangekeyvalue
的最后一个参数传递true
,表示以异步的方式监听。
异步模式的关键点是需要创建一个事件,然后regnotifychangekeyvalue
会立即返回,不会阻塞调用线程,然后需要在其他的线程中等待事件的触发。
当然也可以在regnotifychangekeyvalue
返回之后立即等待事件,这样跟同步阻塞没有什么区别,如果不是出于演示目的,则没什么意义。
出于演示目的毫无意义的异步模式示例:
registrykey hkey = registry.currentuser.createsubkey("software\\1-regmonitor"); string changebefore = hkey.getvalue("testvalue").tostring(); console.writeline($"testvalue的当前值是:{changebefore}, 时间:{datetime.now:hh:mm:ss}"); //此处创建一个任务,5s之后修改testvalue的值为一个新的guid task.delay(5000).continuewith(t => { string newvalue = guid.newguid().tostring(); console.writeline($"testvalue的值即将被改为:{newvalue}, 时间:{datetime.now:hh:mm:ss}"); hkey.setvalue("testvalue", newvalue); }); manualresetevent manualresetevent = new manualresetevent(false); int ret = regnotifychangekeyvalue(hkey.handle, false, regnotifyfilter.changelastset, manualresetevent.safewaithandle, true); if(ret != 0) { console.writeline($"出错了:{ret}"); return; } console.writeline($"regnotifychangekeyvalue立即返回,时间:{datetime.now:hh:mm:ss}"); manualresetevent.waitone(); string currentvalue = hkey.getvalue("testvalue").tostring(); console.writeline($"testvalue的最新值是:{currentvalue}, 时间:{datetime.now:hh:mm:ss}"); hkey.close(); manualresetevent.close(); console.writeline("收工"); console.readline();
运行结果:
正经的代码大概应该这么写:
演示代码请忽略参数未判空,异常未处理等场景
class registrymonitor { private thread m_thread = null; private string m_keyname; private regnotifyfilter m_notifyfilter = regnotifyfilter.changelastset; public event eventhandler registrychanged; public registrymonitor(string keyname, regnotifyfilter notifyfilter) { this.m_keyname = keyname; this.m_notifyfilter = notifyfilter; this.m_thread = new thread(threadaction); this.m_thread.isbackground = true; } public void start() { this.m_thread.start(); } private void threadaction() { using(registrykey hkey = registry.currentuser.createsubkey(this.m_keyname)) { using(manualresetevent waithandle = new manualresetevent(false)) { int ret = regnotifychangekeyvalue(hkey.handle, false, this.m_notifyfilter, waithandle.safewaithandle, true); waithandle.waitone(); this.registrychanged?.invoke(this, eventargs.empty); } } } } static void main(string[] args) { string keyname = "software\\1-regmonitor"; registrykey hkey = registry.currentuser.createsubkey(keyname); string changebefore = hkey.getvalue("testvalue").tostring(); console.writeline($"testvalue的当前值是:{changebefore}, 时间:{datetime.now:hh:mm:ss}"); //此处创建一个任务,5s之后修改testvalue的值为一个新的guid task.delay(5000).continuewith(t => { string newvalue = guid.newguid().tostring(); console.writeline($"testvalue的值即将被改为:{newvalue}, 时间:{datetime.now:hh:mm:ss}"); hkey.setvalue("testvalue", newvalue); }); registrymonitor monitor = new registrymonitor(keyname, regnotifyfilter.changelastset); monitor.registrychanged += (sender, e) => { console.writeline($"{keyname}的值发生了改变"); string currentvalue = hkey.getvalue("testvalue").tostring(); console.writeline($"testvalue的最新值是:{currentvalue}, 时间:{datetime.now:hh:mm:ss}"); hkey.close(); }; monitor.start(); console.writeline("收工"); console.readline(); }
运行结果:
那么问题来了:
- 上面监听一个路径就需要创建一个线程,如果要监听多个路径,就需要创建多个线程,且他们什么事都不干,就在那等,这不太科学。
- 经常写c#的都知道,一般不建议代码中直接创建
thread
。 - 改成线程池或者
task
行不行?如果在线程池或者task
里面调用waitone
进行阻塞,那也是不行的。
接下来 ,我们尝试改造一下
基于线程池的异步模式
调用线程池的
registerwaitforsingleobject
,给一个事件注册一个回调,当事件触发时,则执行指定的回调函数,参考threadpool.registerwaitforsingleobject 方法 (system.threading) | microsoft learn
代码实例如下:
class registrymonitor { private string m_keyname; private regnotifyfilter m_notifyfilter = regnotifyfilter.changelastset; private registeredwaithandle m_registered = null; private registrykey m_key = null; private manualresetevent m_waithandle = null; public event eventhandler registrychanged; public registrymonitor(string keyname, regnotifyfilter notifyfilter) { this.m_keyname = keyname; this.m_notifyfilter = notifyfilter; } public void start() { this.m_key = registry.currentuser.createsubkey(this.m_keyname); this.m_waithandle = new manualresetevent(false); int ret = regnotifychangekeyvalue(this.m_key.handle, false, this.m_notifyfilter | regnotifyfilter.threadagnostic, this.m_waithandle.safewaithandle, true); this.m_registered = threadpool.registerwaitforsingleobject(this.m_waithandle, callback, null, timeout.infinite, true); } private void callback(object state, bool timedout) { this.m_registered.unregister(this.m_waithandle); this.m_waithandle.close(); this.m_key.close(); this.registrychanged?.invoke(this, eventargs.empty); } } static void main(string[] args) { for(int i = 1; i <= 50; i++) { string keyname = $"software\\1-regmonitor\\{i}"; registrykey hkey = registry.currentuser.createsubkey(keyname); hkey.setvalue("testvalue", guid.newguid().tostring()); string changebefore = hkey.getvalue("testvalue").tostring(); console.writeline($"{keyname} testvalue的当前值是:{changebefore}, 时间:{datetime.now:hh:mm:ss}"); registrymonitor monitor = new registrymonitor(keyname, regnotifyfilter.changelastset); monitor.registrychanged += (sender, e) => { console.writeline($"{keyname}的值发生了改变"); string currentvalue = hkey.getvalue("testvalue").tostring(); console.writeline($"{keyname} testvalue的最新值是:{currentvalue}, 时间:{datetime.now:hh:mm:ss}"); hkey.close(); }; monitor.start(); console.writeline($"{keyname}监听中..."); } console.writeline("收工"); console.readline(); }
运行结果:
可以看到,创建50个监听,而进程的总线程数只有7个。因此使用线程池是最佳方案。
注意事项
- 官方文档有说明,调用
regnotifychangekeyvalue
需要再持久化的线程中,如果不能保证调用线程持久化(如在线程池中调用),则可以加上reg_notify_thread_agnostic
标识 - 示例中的监听都是一次性的,重复监听只需要在事件触发后再次执行
regnotifychangekeyvalue
的流程即可
基础代码
/// <summary> /// 指示应报告的更改 /// </summary> [flags] enum regnotifyfilter { /// <summary> /// 通知调用方是添加还是删除了子项 /// </summary> changename = 0x00000001, /// <summary> /// 向调用方通知项属性(例如安全描述符信息)的更改 /// </summary> changeattributes = 0x00000002, /// <summary> /// 向调用方通知项值的更改。 这包括添加或删除值,或更改现有值 /// </summary> changelastset = 0x00000004, /// <summary> /// 向调用方通知项的安全描述符的更改 /// </summary> changesecurity = 0x00000008, /// <summary> /// 指示注册的生存期不得绑定到发出 regnotifychangekeyvalue 调用的线程的生存期。<b>注意</b> 此标志值仅在 windows 8 及更高版本中受支持。 /// </summary> threadagnostic = 0x10000000 } /// <summary> /// 通知调用方对指定注册表项的属性或内容的更改。 /// </summary> /// <param name="hkey">打开的注册表项的句柄。密钥必须已使用key_notify访问权限打开。</param> /// <param name="bwatchsubtree">如果此参数为 true,则函数将报告指定键及其子项中的更改。 如果参数为 false,则函数仅报告指定键中的更改。</param> /// <param name="dwnotifyfilter"> /// 一个值,该值指示应报告的更改。 此参数可使用以下一个或多个值。<br/> /// reg_notify_change_name 0x00000001l 通知调用方是添加还是删除了子项。<br/> /// reg_notify_change_attributes 0x00000002l 向调用方通知项属性(例如安全描述符信息)的更改。<br/> /// reg_notify_change_last_set 0x00000004l 向调用方通知项值的更改。 这包括添加或删除值,或更改现有值。<br/> /// reg_notify_change_security 0x00000008l 向调用方通知项的安全描述符的更改。<br/> /// reg_notify_thread_agnostic 0x10000000l 指示注册的生存期不得绑定到发出 regnotifychangekeyvalue 调用的线程的生存期。<b>注意</b> 此标志值仅在 windows 8 及更高版本中受支持。 /// </param> /// <param name="hevent">事件的句柄。 如果 fasynchronous 参数为 true,则函数将立即返回 ,并通过发出此事件信号来报告更改。 如果 fasynchronous 为 false,则忽略 hevent 。</param> /// <param name="fasynchronous"> /// 如果此参数为 true,则函数将立即返回并通过向指定事件发出信号来报告更改。 如果此参数为 false,则函数在发生更改之前不会返回 。<br/> /// 如果 hevent 未指定有效的事件, 则 fasynchronous 参数不能为 true。 /// </param> /// <returns></returns> [dllimport("advapi32.dll", charset = charset.unicode, setlasterror = true)] private static extern int regnotifychangekeyvalue(safehandle hkey, bool bwatchsubtree, regnotifyfilter dwnotifyfilter, safehandle hevent, bool fasynchronous);
发表评论