C# 跨線程更新UI界面的適當的處理方式,友好的退出界面的機制探索

本文主要講講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( );
    }
}  

咱們頗有可能這麼寫,當咱們點擊了窗口的時候,會出現以下的異常異步

 

發送這個問題的根本緣由在於,當你點擊了窗體關閉,窗體全部的組件開始釋放資源,可是線程尚未當即關閉,因此針對上訴的代碼,咱們來進行優化。測試

 

 

 

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回收數據時又要佔用內存消耗,隨便用戶感受不出來,可是良好的開發習慣就用更少的內存,執行不少的東西。優化

 我剛開始思考若是避免退出異常的時候,既然它報錯爲空,我就加個判斷唄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( );
        }
    }
相關文章
相關標籤/搜索