在Invoke或者BeginInvoke的使用中無一例外地使用了委托Delegate,至於委托的本質請參考我的另一隨筆:對.net事件的看法。
一、為什么Control類提供了Invoke和BeginInvoke機制?
關於這個問題的最主要的原因已經是dotnet程序員眾所周知的,我在此費點筆墨再次記錄到自己的日志,以便日后提醒一下自己。
1、windows程序消息機制
Windows GUI程序是基於消息機制的,有個主線程維護着一個消息泵。這個消息泵讓windows程序生生不息。
Windows GUI程序的消息循環
Windows程序有個消息隊列,窗體上的所有消息是這個隊列里面消息的最主要來源。這里的while循環使用了GetMessage()這個方法,這是個阻塞方法,也就是隊列為空時方法就會被阻塞,從而這個while循環停止運動,這避免了一個程序把cpu無緣無故地耗盡,讓其它程序難以得到響應。當然在某些需要cpu最大限度運動的程序里面就可以使用另外的方法,例如某些3d游戲或者及時戰略游戲中,一般會使用PeekMessage()這個方法,它不會被windows阻塞,從而保證整個游戲的流暢和比較高的幀速。
這個主線程維護着整個窗體以及上面的子控件。當它得到一個消息,就會調用DispatchMessage方法派遣消息,這會引起對窗體上的窗口過程的調用。窗口過程里面當然是程序員提供的窗體數據更新代碼和其它代碼。
2、dotnet里面的消息循環
public static void Main(string[] args) { Form f = new Form(); Application.Run(f); }
Dotnet窗體程序封裝了上述的while循環,這個循環就是通過Application.Run方法啟動的。
3、線程外操作GUI控件的問題
如果從另外一個線程操作windows窗體上的控件,就會和主線程產生競爭,造成不可預料的結果,甚至死鎖。因此windows GUI編程有一個規則,就是只能通過創建控件的線程來操作控件的數據,否則就可能產生不可預料的結果。
因此,dotnet里面,為了方便地解決這些問題,Control類實現了ISynchronizeInvoke接口,提供了Invoke和BeginInvoke方法來提供讓其它線程更新GUI界面控件的機制。
public interface ISynchronizeInvoke { [HostProtection(SecurityAction.LinkDemand, Synchronization=true, ExternalThreading=true)] IAsyncResult BeginInvoke(Delegate method, object[] args); object EndInvoke(IAsyncResult result); object Invoke(Delegate method, object[] args); bool InvokeRequired { get; } }
如果是從創建控件的線程外操作windows窗體控件,那么就需要使用Invoke或者BeginInvoke方法,通過一個委托把調用封送到控件所屬的線程上執行。
二、消息機制---線程間和進程間通信機制
1、window消息發送
Windows消息機制是windows平台上的線程或者進程間通信機制之一。Windows消息值其實就是定義的一個數據結構,最重要的是消息的類型,它就是一個整數;然后就是消息的參數。消息的參數可以表示很多東西。
Windows提供了一些api用來向一個線程的消息隊列發送消息。因此,一個線程可以向另一個線程的消息隊列發送消息從而告訴對方做什么,這樣就完成了線程間的通信。有些api發送消息需要一個窗口句柄,這種函數可以把消息發送到指定窗口的主線程消息隊列;而有些則可以直接通過線程句柄,把消息發送到該線程消息隊列中。
用消息機制通信
SendMessage是windows api,用來把一個消息發送到一個窗口的消息隊列。這個方法是個阻塞方法,也就是操作系統會確保消息的確發送到目的消息隊列,並且該消息被處理完畢以后,該函數才返回。返回之前,調用者將會被暫時阻塞。
PostMessage也是一個用來發送消息到窗口消息隊列的api函數,但這個方法是非阻塞的。也就是它會馬上返回,而不管消息是否真的發送到目的地,也就是調用者不會被阻塞。
2、Invoke and BeginInvoke
Invoke or BeginInvoke
Invoke或者BeginInvoke方法都需要一個委托對象作為參數。委托類似於回調函數的地址,因此調用者通過這兩個方法就可以把需要調用的函數地址封送給界面線程。這些方法里面如果包含了更改控件狀態的代碼,那么由於最終執行這個方法的是界面線程,從而避免了競爭條件,避免了不可預料的問題。如果其它線程直接操作界面線程所屬的控件,那么將會產生競爭條件,造成不可預料的結果。
使用Invoke完成一個委托方法的封送,就類似於使用SendMessage方法來給界面線程發送消息,是一個同步方法。也就是說在Invoke封送的方法被執行完畢前,Invoke方法不會返回,從而調用者線程將被阻塞。
使用BeginInvoke方法封送一個委托方法,類似於使用PostMessage進行通信,這是一個異步方法。也就是該方法封送完畢后馬上返回,不會等待委托方法的執行結束,調用者線程將不會被阻塞。但是調用者也可以使用EndInvoke方法或者其它類似WaitHandle機制等待異步操作的完成。
但是在內部實現上,Invoke和BeginInvoke都是用了PostMessage方法,從而避免了SendMessage帶來的問題。而Invoke方法的同步阻塞是靠WaitHandle機制來完成的。
3、使用場合問題
如果你的后台線程在更新一個UI控件的狀態后不需要等待,而是要繼續往下處理,那么你就應該使用BeginInvoke來進行異步處理。
如果你的后台線程需要操作UI控件,並且需要等到該操作執行完畢才能繼續執行,那么你就應該使用Invoke。否則,在后台線程和主截面線程共享某些狀態數據的情況下,如果不同步調用,而是各自繼續執行的話,可能會造成執行序列上的問題,雖然不發生死鎖,但是會出現不可預料的顯示結果或者數據處理錯誤。
可以看到ISynchronizeInvoke有一個屬性,InvokeRequired。這個屬性就是用來在編程的時候確定,一個對象訪問UI控件的時候是否需要使用Invoke或者BeginInvoke來進行封送。如果不需要那么就可以直接更新。在調用者對象和UI對象同屬一個線程的時候這個屬性返回false。在后面的代碼分析中我們可以看到,Control類對這一屬性的實現就是在判斷調用者和控件是否屬於同一個線程的。
三、Delegate.BeginInvoke
通過一個委托來進行同步方法的異步調用(有點繞嘴),也是.net提供的異步調用機制之一。但是Delegate.BeginInvoke方法是從ThreadPool取出一個線程來執行這個方法,以獲得異步執行效果的。也就是說,如果采用這種方式提交多個異步委托,那么這些調用的順序無法得到保證。而且由於是使用線程池里面的線程來完成任務,使用頻繁,會對系統的性能造成影響。
Delegate.BeginInvoke同樣也是將一個委托方法封送到其它線程,從而通過異步機制執行一個方法。調用者線程則可以在完成封送以后去繼續它的工作。但是這個方法封送到的最終執行線程是運行庫從ThreadPool里面選取的一個線程。
這里需要糾正一個誤區,那就是Control類上的異步調用BeginInvoke並沒有開辟新的線程完成委托任務,而是讓界面控件的所屬線程完成委托任務的。看來異步操作就是開辟新線程的說法不一定准確。
四、用Reflector察看一些相關代碼
1、Control.BeginInvoke and Control.Invoke
public IAsyncResult BeginInvoke(Delegate method, params object[] args) { using (new MultithreadSafeCallScope()) { return (IAsyncResult) this.FindMarshalingControl().MarshaledInvoke(this, method, args, false); } } public object Invoke(Delegate method, params object[] args) { using (new MultithreadSafeCallScope()) { return this.FindMarshalingControl().MarshaledInvoke(this, method, args, true); } }
這里的FindMarshalingControl方法通過一個循環向上回溯,從當前控件開始回溯父控件,直到找到最頂級的父控件,用它作為封送對象。例如,我們調用窗體上一個進度條的Invoke方法封送委托,但是實際上會回溯到主窗體,通過這個控件對象來封送委托。因為主窗體是主線程消息隊列相關的,發送給主窗體的消息才能發送到界面主線程消息隊列。
我們可以看到Invoke和BeginInvoke方法使用了同樣的實現,只是MarshaledInvoke方法的最后一個參數值不一樣。
2、MarshaledInvoke
private object MarshaledInvoke(Control caller, Delegate method, object[] args, bool synchronous) { int num; if (!this.IsHandleCreated) { throw new InvalidOperationException(SR.GetString("ErrorNoMarshalingThread")); } if (((ActiveXImpl) this.Properties.GetObject(PropActiveXImpl)) != null) { IntSecurity.UnmanagedCode.Demand(); } bool flag = false; if ((SafeNativeMethods.GetWindowThreadProcessId(new HandleRef(this, this.Handle), out num) == SafeNativeMethods.GetCurrentThreadId()) && synchronous) { flag = true; } ExecutionContext executionContext = null; if (!flag) { executionContext = ExecutionContext.Capture(); } ThreadMethodEntry entry = new ThreadMethodEntry(caller, method, args, synchronous, executionContext); lock (this) { if (this.threadCallbackList == null) { this.threadCallbackList = new Queue(); } } lock (this.threadCallbackList) { if (threadCallbackMessage == 0) { threadCallbackMessage = SafeNativeMethods.RegisterWindowMessage(Application.WindowMessagesVersion + "_ThreadCallbackMessage"); } this.threadCallbackList.Enqueue(entry); } if (flag) { this.InvokeMarshaledCallbacks(); } else { //終於找到你了,PostMessage UnsafeNativeMethods.PostMessage(new HandleRef(this, this.Handle), threadCallbackMessage, IntPtr.Zero, IntPtr.Zero); } if (!synchronous) //如果是異步,那么馬上返回吧 { return entry; } if (!entry.IsCompleted) //同步調用沒結束,阻塞起來等待吧 { this.WaitForWaitHandle(entry.AsyncWaitHandle); } if (entry.exception != null) { throw entry.exception; } return entry.retVal; }
怎么樣,我們終於看到PostMessage了吧?通過windows消息機制實現了封送。而需要封送的委托方法作為消息的參數進行了傳遞。關於其它的代碼這里不作進一步解釋。
3、InvokeRequired
public bool InvokeRequired { get { using (new MultithreadSafeCallScope()) { HandleRef ref2; int num; if (this.IsHandleCreated) { ref2 = new HandleRef(this, this.Handle); } else { Control wrapper = this.FindMarshalingControl(); if (!wrapper.IsHandleCreated) { return false; } ref2 = new HandleRef(wrapper, wrapper.Handle); } int windowThreadProcessId = SafeNativeMethods.GetWindowThreadProcessId(ref2, out num); int currentThreadId = SafeNativeMethods.GetCurrentThreadId(); return (windowThreadProcessId != currentThreadId); } } }
終於看到了,這是在判斷windows窗體線程和當前的調用者線程是否是同一個,如果是同一個就沒有必要封送了,直接訪問這個GUI控件吧。否則,就不要那么直接表白了,就需要Invoke或者BeginInvoke做媒了。
出處:https://www.cnblogs.com/worldreason/archive/2008/06/09/1216127.html
=============================================================================
再發一個SendMessage的使用實例代碼,也是從網上找來的。
進程間通訊,其實用SendMessage就可以實現,剛找了一下以前做的東西,正好是進程間通訊的,主要是主程序調用校驗程序,校驗程序返回校驗結果。
using System; using System.Collections.Generic; using System.Text; using System.Runtime.InteropServices; using System.Diagnostics; namespace SendMsg { #region 結構體定義 [StructLayout(LayoutKind.Sequential)] public struct COPYDATASTRUCT { public int dwData; public int cbData; public int lpData; } //定義要傳遞的Struct [StructLayout(LayoutKind.Sequential)] struct SendMsgInfo { /// <summary> /// 參數(單號) /// </summary> [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 100)] public string AccessionNum; /// <summary> /// 參數(原文) /// </summary> [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 5000)] public string ReportTxt; /// <summary> /// 參數(Ris用戶) /// </summary> [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 100)] public string RisUid; /// <summary> /// 調用方法名 /// </summary> [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 100)] public string FunctionNum; /// <summary> /// 端口 /// </summary> public int Handle; /// <summary> /// 返回值 /// </summary> public bool Result; /// <summary> /// Err信息 /// </summary> [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 200)] public string ResultMsg; } #endregion class CaSendMsg { Process pro; const int WM_COPYDATA = 0x004A; private string _winFrmName = @"校驗程序"; private string _path = @"校驗.exe"; private int _interval = 500; private int _maxTimes = 5; /// <summary> /// 窗體名稱 /// </summary> public string WinFrmName { set { _winFrmName = value; } get { return _winFrmName; } } /// <summary> /// 程序路徑 /// </summary> public string Path { set { _path = value; } get { return _path; } } /// <summary> /// 等待輪序頻率 秒/次 /// </summary> public int Interval { set { _interval = value; } get { return _interval; } } /// <summary> /// 上限次數 /// </summary> public int MaxTimes { set { _maxTimes = value; } get { return _maxTimes; } } [DllImport("user32", EntryPoint = "SendMessageA")] public static extern int SendMessage(int hWnd, int wMsg, int wParam, ref COPYDATASTRUCT lParam); [DllImport("User32.dll", EntryPoint = "FindWindow")] public static extern int FindWindow(string lpClassName, string lpWindowName); /// <summary> /// ca校驗 /// </summary> /// <param name="h"></param> /// <returns></returns> public bool CACheck(SendMsgInfo h) { int hWnd = GetWinFrmHwnd(); if (hWnd <= 0) return false; return CACheck(h, hWnd); } /// <summary> /// 獲取程序句柄 /// </summary> /// <returns></returns> private int GetWinFrmHwnd() { int hWnd = FindWindow(null, _winFrmName); if (hWnd <= 0) { hWnd = StartNewPro(_path); } return hWnd; } /// <summary> /// 開啟新的校驗進程 /// </summary> /// <param name="path"></param> /// <returns></returns> private int StartNewPro(string path) { try { pro = new Process(); pro.StartInfo.FileName = path; pro.Start(); return pro.MainWindowHandle.ToInt32(); } catch { return 0; } } public bool CACheck(SendMsgInfo info, int hWnd) { try { int size = Marshal.SizeOf(typeof(SendMsgInfo)); byte[] Bytes = new byte[size]; //根據定義的尺寸分配內存塊 GCHandle GC = GCHandle.Alloc(Bytes, GCHandleType.Pinned); IntPtr ptr1 = GC.AddrOfPinnedObject(); //獲得Struct對應的IntPtr Marshal.StructureToPtr(info, ptr1, false); COPYDATASTRUCT SendData = new COPYDATASTRUCT(); SendData.lpData = ptr1.ToInt32(); SendData.cbData = size; if (hWnd > 0) { SendMessage(hWnd, WM_COPYDATA, 0, ref (SendData)); } return true; } catch { return false; } } public bool getCaResult(System.Windows.Forms.Message m) { COPYDATASTRUCT RecvData = (COPYDATASTRUCT)m.GetLParam(typeof(COPYDATASTRUCT)); SendMsgInfo info = (SendMsgInfo)Marshal.PtrToStructure((IntPtr)RecvData.lpData, typeof(SendMsgInfo)); //h.ResultMsg; return info.Result; } } }
接收消息
//接收消息 protected override void DefWndProc(ref System.Windows.Forms.Message m) { switch (m.Msg) { case SendMsg.WM_COPYDATA: COPYDATASTRUCT RecvData = (COPYDATASTRUCT)m.GetLParam(typeof(COPYDATASTRUCT)); SendMsgInfo h = (SendMsgInfo)Marshal.PtrToStructure((IntPtr)RecvData.lpData, typeof(SendMsgInfo)); textBox1.Text = h.MessageType.ToString(); textBox2.Text = h.MessageText; //返回校驗結果 SendMsgInfo returnInfo = new SendMsgInfo(); returnInfo.Handle = h.Handle; returnInfo.MessageNum = h.MessageNum; returnInfo.Result = true; returnInfo.ResultMsg = ""; SendMsg c = new SendMsg(); c.Send(returnInfo, returnInfo.Handle); break; default: base.DefWndProc(ref m); break; } }
出處:https://bbs.csdn.net/topics/390650236 請查看 #20 給出的代碼
https://blog.csdn.net/barenk/article/details/78686981
=====================================================================================
再給一個使用的示例,不同之處是參考傳遞的消息的結構,
namespace CshapMessage { /// <summary> /// MainWindow.xaml 的交互邏輯 /// </summary> public partial class MainWindow : Window { IntPtr hwnd; const int WM_COPYDATA = 0x004A; public struct COPYDATASTRUCT { public IntPtr dwData; public int cData; [MarshalAs(UnmanagedType.LPStr)] public string lpData; } [DllImport("User32.dll")] public static extern int SendMessage(int hwnd, int msg, int wParam, ref COPYDATASTRUCT IParam); [DllImport("User32.dll")] public static extern int FindWindow(string lpClassName, string lpWindowName); public MainWindow() { InitializeComponent(); this.Title = "CshapMessage"; this.Loaded += MainWindow_Loaded; this.Closed += MainWindow_Closed; } private void MainWindow_Closed(object sender, EventArgs e) { try { HwndSource.FromHwnd(hwnd).RemoveHook(WndProc); } catch (Exception) { } } private void MainWindow_Loaded(object sender, RoutedEventArgs e) { hwnd = new WindowInteropHelper(this).Handle; HwndSource.FromHwnd(hwnd).AddHook(new HwndSourceHook(WndProc)); } /// <summary> /// 向C++程序 CshapMessage發送消息 /// </summary> /// <param name="nMessgeId"></param> /// <param name="strSend"></param> /// <returns></returns> private bool CshapSendMessage(int nMessgeId, String strSend) { int WINDOW_HANDLE = FindWindow(null, "VcMessage");//VcMessage為向C++程序發送的窗口名稱 if (WINDOW_HANDLE != 0) { COPYDATASTRUCT cdata; cdata.dwData = (IntPtr)100;//這里可以傳入一些自定義的數據,但只能是4字節整數 cdata.lpData = strSend;//消息字符串 cdata.cData = System.Text.Encoding.Default.GetBytes(strSend).Length+1;//注意,這里的長度是按字節來算的 SendMessage(WINDOW_HANDLE, WM_COPYDATA, 0, ref cdata); } else { return false; } return true; } private void button_Click(object sender, RoutedEventArgs e) { String strSend = "C#發送的信息"; int nMessageId = 100; if (CshapSendMessage(nMessageId,strSend)) { MessageBox.Show("發送消息成功"); } else { MessageBox.Show("消息發送失敗,請打開VcMessage程序"); } } //接收消息。 private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { if (msg == WM_COPYDATA) { COPYDATASTRUCT cdata = new COPYDATASTRUCT(); Type mytype = cdata.GetType(); cdata = (COPYDATASTRUCT)Marshal.PtrToStructure(lParam, mytype); switch (cdata.dwData.ToInt32()) { case 1: { string strRecv = cdata.lpData; break; } default: break; } } return IntPtr.Zero; } } }