先上兩個通用Modbus幫助類,下麵這個是多線程不安全版,在多線程多電機同一埠通信下,可能造成步進電機丟步或者輸出口無響應等,還有個多線程安全版,只是基於這個不安全版加上了LOCK,THIS using Modbus.Device; using Sunny.UI; using System; us ...
養成一個好習慣,調用 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);