当前位置: 代码网 > it编程>编程语言>C# > C#使用RegNotifyChangeKeyValue监听注册表更改的方法小结

C#使用RegNotifyChangeKeyValue监听注册表更改的方法小结

2024年06月14日 C# 我要评论
养成一个好习惯,调用 windows api 之前一定要先看文档regnotifychangekeyvalue 函数 (winreg.h) - win32 apps | microsoft learn

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

到此这篇关于c#使用regnotifychangekeyvalue监听注册表更改的几种方式的文章就介绍到这了,更多相关c# regnotifychangekeyvalue监听注册表内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com