C# 跨线程更新UI界面的适当的处理方式,友好的退出界面的机制探索

本文主要讲讲C#窗体的程序中一个经常遇到的情况,就是在退出窗体的时候的,发生了退出的异常。

联系作者及加群方式(激活码在群里发放):Cooperation - HslCommunication 官网 

欢迎技术探讨

我们先来看看一个典型的场景,定时从PLC或是远程服务器获取数据,然后更新界面的标签,基本上实时更新的。我们可以把模型简化,简化到一个form窗体里面,开线程定时读取

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

    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. 优化不停的创建委托实例


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

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回收数据时又要占用内存消耗,随便用户感觉不出来,但是良好的开发习惯就用更少的内存,执行很多的东西。

 我刚开始思考如果避免退出异常的时候,既然它报错为空,我就加个判断呗

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

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 );

    }

}

  

显示界面的时候,判断下句柄是否创建,当前是否被释放。出现异常的频率少了,但是还是会出。下面提供了一个简单粗暴的思路,既然报错已释放,我就捕获异常

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

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,换个思路,我自己新增一个标记,指示窗体是否显示出来。当窗体关闭的时候,复位这个标记

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

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;

}

整个程序变成了这个样子,我们再多次测试测试,还是经常会出,这时候我们需要考虑更深层次的事了,我程序退出的时候需要做什么事?把采集线程关掉,停止刷新,这时候才能真正的退出

这时候就需要同步技术了,我们继续改造

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

    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显示的时候,就会形成死锁,所以方式一,改下下面的机制

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

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方法并不是特别的安全的方式,而且我们在退出的时候有可能会卡那么一会会,我们需要想想有没有更好的机制。

如果我们做一个等待的退出的窗口会不会更好?既不卡掉主窗口,又可以完美的退出,我们新建一个小窗口

去掉了边框,界面就是这么简单,然后改造代码

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

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( );

    }

}

 我们只要把这个退出前的操作传递给退出窗口即可以

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

    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,这样我们就完成了最终的程序,如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

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( );

    }

}

猜你喜欢

转载自blog.csdn.net/weixin_45499836/article/details/124042818