本文主要講講C#窗體的程序中一個常常遇到的狀況,就是在退出窗體的時候的,發生了退出的異常。安全
歡迎技術探討服務器
咱們先來看看一個典型的場景,定時從PLC或是遠程服務器獲取數據,而後更新界面的標籤,基本上實時更新的。咱們能夠把模型簡化,簡化到一個form窗體裏面,開線程定時讀取dom
public partial class Form1 : Form { public Form1( ) { InitializeComponent( ); } private void Form1_Load( object sender, EventArgs e ) { thread = new System.Threading.Thread( new System.Threading.ThreadStart( ThreadCapture ) ); thread.IsBackground = true; thread.Start( ); } private void ThreadCapture( ) { System.Threading.Thread.Sleep( 200 ); while (true) { // 咱們假設這個數據是從PLC或是遠程服務器獲取到的,由於可能比較耗時,咱們放在了後臺線程獲取,而且處於一直運行的狀態 // 咱們還假設獲取數據的頻率是200ms一次,而後把數據顯示出來 int data = random.Next( 1000 ); // 接下來是跨線程的顯示 Invoke( new Action( ( ) => { label1.Text = data.ToString( ); } ) ); System.Threading.Thread.Sleep( 200 ); } } private System.Threading.Thread thread; private Random random = new Random( ); } }
咱們頗有可能這麼寫,當咱們點擊了窗口的時候,會出現以下的異常異步
發送這個問題的根本緣由在於,當你點擊了窗體關閉,窗體全部的組件開始釋放資源,可是線程尚未當即關閉,因此針對上訴的代碼,咱們來進行優化。測試
private void ThreadCapture( ) { showInfo = new Action<string>( m => { label1.Text = m; } ); System.Threading.Thread.Sleep( 200 ); while (true) { // 咱們假設這個數據是從PLC或是遠程服務器獲取到的,由於可能比較耗時,咱們放在了後臺線程獲取,而且處於一直運行的狀態 // 咱們還假設獲取數據的頻率是200ms一次,而後把數據顯示出來 int data = random.Next( 1000 ); // 接下來是跨線程的顯示 Invoke( showInfo, data.ToString( ) ); System.Threading.Thread.Sleep( 200 ); } } private Action<string> showInfo; private System.Threading.Thread thread; private Random random = new Random( );
這樣就避免了每隔200ms頻繁的建立委託實例,這些委託實例在GC回收數據時又要佔用內存消耗,隨便用戶感受不出來,可是良好的開發習慣就用更少的內存,執行不少的東西。優化
我剛開始思考若是避免退出異常的時候,既然它報錯爲空,我就加個判斷唄ui
private void ThreadCapture( ) { showInfo = new Action<string>( m => { label1.Text = m; } ); System.Threading.Thread.Sleep( 200 ); while (true) { // 咱們假設這個數據是從PLC或是遠程服務器獲取到的,由於可能比較耗時,咱們放在了後臺線程獲取,而且處於一直運行的狀態 // 咱們還假設獲取數據的頻率是200ms一次,而後把數據顯示出來 int data = random.Next( 1000 ); // 接下來是跨線程的顯示 if(IsHandleCreated && !IsDisposed) Invoke( showInfo, data.ToString( ) ); System.Threading.Thread.Sleep( 200 ); } }
顯示界面的時候,判斷下句柄是否建立,當前是否被釋放。出現異常的頻率少了,可是仍是會出。下面提供了一個簡單粗暴的思路,既然報錯已釋放,我就捕獲異常this
private void ThreadCapture( ) { showInfo = new Action<string>( m => { label1.Text = m; } ); System.Threading.Thread.Sleep( 200 ); while (true) { // 咱們假設這個數據是從PLC或是遠程服務器獲取到的,由於可能比較耗時,咱們放在了後臺線程獲取,而且處於一直運行的狀態 // 咱們還假設獲取數據的頻率是200ms一次,而後把數據顯示出來 int data = random.Next( 1000 ); try { // 接下來是跨線程的顯示 if (IsHandleCreated && !IsDisposed) Invoke( showInfo, data.ToString( ) ); } catch (ObjectDisposedException) { break; } catch { throw; } System.Threading.Thread.Sleep( 200 ); } }
這樣子就簡單粗暴的解決了,可是仍是以爲不太好,因此不採用try..catch,換個思路,我本身新增一個標記,指示窗體是否顯示出來。當窗體關閉的時候,復位這個標記線程
private void ThreadCapture( ) { showInfo = new Action<string>( m => { label1.Text = m; } ); isWindowShow = true; System.Threading.Thread.Sleep( 200 ); while (true) { // 咱們假設這個數據是從PLC或是遠程服務器獲取到的,由於可能比較耗時,咱們放在了後臺線程獲取,而且處於一直運行的狀態 // 咱們還假設獲取數據的頻率是200ms一次,而後把數據顯示出來 int data = random.Next( 1000 ); // 接下來是跨線程的顯示 if (isWindowShow) Invoke( showInfo, data.ToString( ) ); else break; System.Threading.Thread.Sleep( 200 ); } } private bool isWindowShow = false; private Action<string> showInfo; private System.Threading.Thread thread; private Random random = new Random( ); private void Form1_FormClosing( object sender, FormClosingEventArgs e ) { isWindowShow = false; }
整個程序變成了這個樣子,咱們再屢次測試測試,仍是常常會出,這時候咱們須要考慮更深層次的事了,我程序退出的時候須要作什麼事?把採集線程關掉,中止刷新,這時候才能真正的退出orm
這時候就須要同步技術了,咱們繼續改造
private void ThreadCapture( ) { showInfo = new Action<string>( m => { label1.Text = m; } ); isWindowShow = true; System.Threading.Thread.Sleep( 200 ); while (true) { // 咱們假設這個數據是從PLC或是遠程服務器獲取到的,由於可能比較耗時,咱們放在了後臺線程獲取,而且處於一直運行的狀態 // 咱們還假設獲取數據的頻率是200ms一次,而後把數據顯示出來 int data = random.Next( 1000 ); // 接下來是跨線程的顯示,並檢測窗體是否關閉 if (isWindowShow) Invoke( showInfo, data.ToString( ) ); else break; System.Threading.Thread.Sleep( 200 ); // 再次檢測窗體是否關閉 if (!isWindowShow) break; } // 通知主界面是否準備退出 resetEvent.Set( ); } private System.Threading.AutoResetEvent resetEvent = new System.Threading.AutoResetEvent( false ); private bool isWindowShow = false; private Action<string> showInfo; private System.Threading.Thread thread; private Random random = new Random( ); private void Form1_FormClosing( object sender, FormClosingEventArgs e ) { isWindowShow = false; resetEvent.WaitOne( ); } }
根據思路咱們寫出了這個代碼。運行了一下,結果卡住不動了,分析下緣由是剛點擊退出的時候,若是發現了Invoke顯示的時候,就會造成死鎖,因此方式一,改下下面的機制
private void ThreadCapture( ) { showInfo = new Action<string>( m => { label1.Text = m; } ); isWindowShow = true; System.Threading.Thread.Sleep( 200 ); while (true) { // 咱們假設這個數據是從PLC或是遠程服務器獲取到的,由於可能比較耗時,咱們放在了後臺線程獲取,而且處於一直運行的狀態 // 咱們還假設獲取數據的頻率是200ms一次,而後把數據顯示出來 int data = random.Next( 1000 ); // 接下來是跨線程的顯示,並檢測窗體是否關閉 if (isWindowShow) BeginInvoke( showInfo, data.ToString( ) ); else break; System.Threading.Thread.Sleep( 200 ); // 再次檢測窗體是否關閉 if (!isWindowShow) break; } // 通知主界面是否準備退出 resetEvent.Set( ); } private System.Threading.AutoResetEvent resetEvent = new System.Threading.AutoResetEvent( false ); private bool isWindowShow = false; private Action<string> showInfo; private System.Threading.Thread thread; private Random random = new Random( ); private void Form1_FormClosing( object sender, FormClosingEventArgs e ) { isWindowShow = false; resetEvent.WaitOne( ); }
Invoke的時候改爲異步的機制,就能夠解決這個問題,可是BeginInvoke方法並非特別的安全的方式,並且咱們在退出的時候有可能會卡那麼一會會,咱們須要想一想有沒有更好的機制。
若是咱們作一個等待的退出的窗口會不會更好?既不卡掉主窗口,又能夠完美的退出,咱們新建一個小窗口
去掉了邊框,界面就是這麼簡單,而後改造代碼
public partial class FormQuit : Form { public FormQuit( Action action ) { InitializeComponent( ); this.action = action; } private void FormQuit_Load( object sender, EventArgs e ) { } // 退出前的操做 private Action action; private void FormQuit_Shown( object sender, EventArgs e ) { // 調用操做 action.Invoke( ); Close( ); } }
咱們只要把這個退出前的操做傳遞給退出窗口便可以
private void ThreadCapture( ) { showInfo = new Action<string>( m => { label1.Text = m; } ); isWindowShow = true; System.Threading.Thread.Sleep( 200 ); while (true) { // 咱們假設這個數據是從PLC或是遠程服務器獲取到的,由於可能比較耗時,咱們放在了後臺線程獲取,而且處於一直運行的狀態 // 咱們還假設獲取數據的頻率是200ms一次,而後把數據顯示出來 int data = random.Next( 1000 ); // 接下來是跨線程的顯示,並檢測窗體是否關閉 if (isWindowShow) Invoke( showInfo, data.ToString( ) ); else break; System.Threading.Thread.Sleep( 200 ); // 再次檢測窗體是否關閉 if (!isWindowShow) break; } // 通知主界面是否準備退出 resetEvent.Set( ); } private System.Threading.AutoResetEvent resetEvent = new System.Threading.AutoResetEvent( false ); private bool isWindowShow = false; private Action<string> showInfo; private System.Threading.Thread thread; private Random random = new Random( ); private void Form1_FormClosing( object sender, FormClosingEventArgs e ) { FormQuit formQuit = new FormQuit( new Action(()=> { isWindowShow = false; resetEvent.WaitOne( ); } )); formQuit.ShowDialog( ); } }
至此你的程序不再會出現上面的對象已經被釋放的異常了,退出的窗體顯示時間不定時,0-200毫秒。爲了有個明顯的逗留做用,咱們多睡眠200ms,這樣咱們就完成了最終的程序,以下:
public partial class Form1 : Form { public Form1( ) { InitializeComponent( ); } private void Form1_Load( object sender, EventArgs e ) { thread = new System.Threading.Thread( new System.Threading.ThreadStart( ThreadCapture ) ); thread.IsBackground = true; thread.Start( ); } private void ThreadCapture( ) { showInfo = new Action<string>( m => { label1.Text = m; } ); isWindowShow = true; System.Threading.Thread.Sleep( 200 ); while (true) { // 咱們假設這個數據是從PLC或是遠程服務器獲取到的,由於可能比較耗時,咱們放在了後臺線程獲取,而且處於一直運行的狀態 // 咱們還假設獲取數據的頻率是200ms一次,而後把數據顯示出來 int data = random.Next( 1000 ); // 接下來是跨線程的顯示,並檢測窗體是否關閉 if (isWindowShow) Invoke( showInfo, data.ToString( ) ); else break; System.Threading.Thread.Sleep( 200 ); // 再次檢測窗體是否關閉 if (!isWindowShow) {System.Threading.Thread.Sleep(50);break;} } // 通知主界面是否準備退出 resetEvent.Set( ); } private System.Threading.AutoResetEvent resetEvent = new System.Threading.AutoResetEvent( false ); private bool isWindowShow = false; private Action<string> showInfo; private System.Threading.Thread thread; private Random random = new Random( ); private void Form1_FormClosing( object sender, FormClosingEventArgs e ) { FormQuit formQuit = new FormQuit( new Action(()=> { System.Threading.Thread.Sleep( 200 ); isWindowShow = false; resetEvent.WaitOne( ); } )); formQuit.ShowDialog( ); } }