本文主要講講C#窗體的程序中一個經常遇到的情況,就是在退出窗體的時候的,發生了退出的異常。
聯系作者及加群方式(激活碼在群里發放):http://www.hslcommunication.cn/Cooperation
歡迎技術探討
我們先來看看一個典型的場景,定時從PLC或是遠程服務器獲取數據,然后更新界面的標簽,基本上實時更新的。我們可以把模型簡化,簡化到一個form窗體里面,開線程定時讀取
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( );
}
}
我們很有可能這么寫,當我們點擊了窗口的時候,會出現如下的異常

發送這個問題的根本原因在於,當你點擊了窗體關閉,窗體所有的組件開始釋放資源,但是線程還沒有立即關閉,所以針對上訴的代碼,我們來進行優化。
1. 優化不停的創建委托實例
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回收數據時又要占用內存消耗,隨便用戶感覺不出來,但是良好的開發習慣就用更少的內存,執行很多的東西。
我剛開始思考如果避免退出異常的時候,既然它報錯為空,我就加個判斷唄
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 );
}
}
顯示界面的時候,判斷下句柄是否創建,當前是否被釋放。出現異常的頻率少了,但是還是會出。下面提供了一個簡單粗暴的思路,既然報錯已釋放,我就捕獲異常
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;
}
整個程序變成了這個樣子,我們再多次測試測試,還是經常會出,這時候我們需要考慮更深層次的事了,我程序退出的時候需要做什么事?把采集線程關掉,停止刷新,這時候才能真正的退出
這時候就需要同步技術了,我們繼續改造
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( );
}
}
