养成一个好习惯,调用 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);
发表评论