养成一个好习惯,调用 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();
}

运行结果:

那么问题来了:

  1. 上面监听一个路径就需要创建一个线程,如果要监听多个路径,就需要创建多个线程,且他们什么事都不干,就在那等,这不太科学。
  2. 经常写C#的都知道,一般不建议代码中直接创建Thread
  3. 改成线程池或者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个。因此使用线程池是最佳方案。

注意事项

  1. 官方文档有说明,调用RegNotifyChangeKeyValue需要再持久化的线程中,如果不能保证调用线程持久化(如在线程池中调用),则可以加上REG_NOTIFY_THREAD_AGNOSTIC标识
  2. 示例中的监听都是一次性的,重复监听只需要在事件触发后再次执行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);

C#使用RegNotifyChangeKeyValue监听注册表更改的几种方式的更多相关文章

  1. Android App监听软键盘按键的三种方式与改变软键盘右下角确定键样式

    actionNone : 回车键,按下后光标到下一行actionGo : Go,actionSearch : 放大镜actionSend : SendactionNext : NextactionDo ...

  2. Android App监听软键盘按键的三种方式

    前言:   我们在android手机上面有时候会遇到监听手机软键盘按键的时候,例如:我们在浏览器输入url完毕后可以点击软键盘右下角的“GO”按键加载url页面:在点击搜索框的时候,点击右下角的sea ...

  3. 键盘-App监听软键盘按键的三种方式

    前言:   我们在android手机上面有时候会遇到监听手机软键盘按键的时候,例如:我们在浏览器输入url完毕后可以点击软键盘右下角的“GO”按键加载url页面:在点击搜索框的时候,点击右下角的sea ...

  4. android activity中监听View测量完成的4种方式

    在开发中经常碰到需要在activity初始化完成后获得控件大小的情况. 但是这个操作我们不能在oncreate.onresume等生命周期方法中调用,因为我们不知道何时view才能初始化完成 为此,特 ...

  5. Android App监听软键盘按键的三种方式(转)

    最近有类似需求,在csdn上刚好发现,粘贴过来,以防止忘记喽 前言:   我们在android手机上面有时候会遇到监听手机软键盘按键的时候,例如:我们在浏览器输入url完毕后可以点击软键盘右下角的“G ...

  6. Android 监听软键盘按键的三种方式

    前言: 我们在Android手机上面有时候会遇到监听手机软键盘按键的时候,例如:我们在浏览器输入url完毕后可以点击软键盘右下角的“Go”按键加载url页面:在点击搜索框的时候,点击右下角的searc ...

  7. 用注册表更改DNS的代码分享

    用注册表更改DNS,1秒切换完毕,快速又方便,不用麻烦的去等待了,支持远程路劲运行 最进我这里DNS老是间歇性掉,很不稳定,广州地区,如果你的DNS经常需要更换,试试这个批处理, 论坛很多人发过了更改 ...

  8. 注册Jdbc驱动程序的三种方式

    注册Jdbc驱动程序的三种方式 1. Class.forName("com.mysql.jdbc.Driver"); 2. DriverManager.registerDriver ...

  9. javascript获取表单值的7种方式

    见代码: <!doctype html> <html lang="en"> <head> <meta charset="UTF- ...

  10. JavaScript表单提交四种方式

    总结JavaScript表单提交四种方式 <!DOCTYPE html> <html> <head> <title>JavaScript表单提交四种方式 ...

随机推荐

  1. 51k+ Star!动画图解、一键运行的数据结构与算法教程!

    大家好,我是 Java陈序员. 我们都知道,<数据结构与算法> -- 是程序员的必修课. 无论是使用什么编程语音,亦或者是前后端开发,都需要修好<数据结构与算法>这门课! 在各 ...

  2. go-admin migrate 数据表迁移

    目录 视频教程 应用场景 目录说明 数据迁移 迁移步骤 配置数据库 常用命令示例 新建模型实例 3.1 方式一:不编译运行(推荐) 3.2 方式二:编译并运行迁移 3.3 方式三:golangIDE ...

  3. ruby http请求组件

    github地址 https://github.com/rest-client/rest-client gemfile里添加 gem 'rest-client', '~> 2.0' 执行 bun ...

  4. ssh端口转发实际应用

    目录 1 ssh本地端口转发 2 ssh远程端口转发 3 跨网络访问(网关功能) 4 动态端口转发(KX上网) SSH 会自动加密和解密所有 SSH 客户端与服务端之间的网络数据.而且,SSH 还能够 ...

  5. 零知识证明: Tornado Cash 项目学习

    前言 最近在了解零知识证明方面的内容,这方面的内容确实不好入门也不好掌握,在了解了一些基础的概念以后,决定选择一个应用了零知识证明的项目来进行进一步的学习.最终选择了 Tornado Cash 这个项 ...

  6. C语言:如何删除在可视化网页中未可见的内容(网页txt)

    我这个代码仅仅限制于在chrome浏览器中下载china daliy的网页中实现删除可视化内容,因为每个网页的超链接或者文本主内容分布不一样,但是学会了删除一个网页类型的不可视化内容之后,修改其他网页 ...

  7. JDK源码阅读-------自学笔记(十三)(java.text.DateFormat和SimpleDateFormat类)

    时间相关类总图

  8. CodePen 的国内替代「笔.COOL」,一个功能完备、使用便捷的在线HTML/CSS/JS编辑器和作品分享平台

    笔.COOL,是一个最近在国内崭露头角的在线HTML/CSS/JS编辑器和作品分享平台. 笔.COOL 提供了一个在线的 HTML.CSS 和 JavaScript 代码编辑器.无需任何安装,你只需打 ...

  9. grafana模板参考

    空的,把面板都删除了 { "__inputs": [ { "name": "DS_PROMETHEUS", "label" ...

  10. uni-app前端图表组件库,折线图横屏android失败的解决办法

    网址:https://www.ucharts.cn/v2/#/demo/index,需要登录,微信就可以 我在使用折现图时,业务需要把曲线图在点击全屏时横屏全屏显示.就做个全屏页面,点击后,把opti ...