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( );
        }
    }
相关文章
相关标签/搜索