• 编写高质量代码改善C#程序的157个建议——建议87:区分WPF和WinForm的线程模型


    建议87:区分WPF和WinForm的线程模型

    WPF和WinForm窗体应用程序都有一个要求,那就是UI元素(如Button、TextBox等)必须由创建它的那个线程进行更新。WinForm在这方面的限制并不是很严格,所以像下面这样的代码,在WinForm中大部分情况下还能运行(本建议后面会详细解释为什么会出现这种现象):

    private void buttonStartAsync_Click(object sender, EventArgs e)  
    {  
        Task t = new Task(() =>
            {  
                while (true)  
                {  
                    label1.Text = DateTime.Now.ToString();  
                    Thread.Sleep(1000);  
                }  
            });  
        //如果有异常,就启动一个新任务  
        t.ContinueWith((task) =>
        {  
            try  
            {  
                task.Wait();  
            }  
            catch (AggregateException ex)  
            {  
                foreach (Exception inner in ex.InnerExceptions)  
                {  
                    MessageBox.Show(string.Format("异常类型:{0}{1}来自:{2}{3}异常内容:{4}", inner.GetType(),Environment.NewLine,  
                        inner.Source, Environment.NewLine, inner.Message));  
                }  
            }  
        }, TaskContinuationOptions.OnlyOnFaulted);  
        t.Start();  
    } 

    但是,相同的一段代码如果放到WPF环境中,就肯定会抛出System.InvalidOperationException异常。


    理论上,WinForm和WPF的线程模型非常接近,它们最后都是调用API(GetMessage或PeekMessage)来处理其他线程发送过来的消息,这些消息存储在系统的一个消息队列中。在WinForm和WPF中,创建主界面的线程就是主线程,也就是UI线程,UI线程负责处理该消息队列。只是两者在处理消息队列的上层机制上稍微有一些不同,这就造成了同样的代码得到不同的结果。

    在WinForm框架中有一个ISynchronizeInvoke接口,所有的UI元素(表现为Control)都继承了该接口。其中,接口中的InvokdRequired属性表示了当前线程是否是创建它的线程。接口中的Invoke和BeginInvoke方法负责将消息发送到消息队列中,这样,UI线程就能够正确处理它了。那么,上面的这段代码在WinForm上的改进版本为(仅列出While循环部分):

    while (true)  
    {  
        if (label1.InvokeRequired)  
            label1.BeginInvoke(new Action(() =>
                {  
                    label1.Text = DateTime.Now.ToString();  
                }));  
        else  
            label1.Text = DateTime.Now.ToString();  
        Thread.Sleep(1000);  
    } 

    BeginInvoke方法接受的是一个Delegate类型的参数,在这里我们用一个Action来实现。

    WPF应用程序的线程模型则完全依赖于DispatcherObject类型。所有的WPF控件都继承自一个抽象类Visual,而这个抽象类又最终继承自DispatcherObject类型。在这个DispatcherObject类型中有一个属性,两个方法。属性Dispatcher完成所有的工作线程和UI线程之间的调度任务。CheckAccess方法负责检测工作线程是否可以访问控件,如果是,则返回True;否则返回False。VerifyAccess方法则负责检测工作线程是否具有控件的访问权限,如果不能访问则抛出异常InvalidOperationException。

    WinForm应用程序用类似CheckAccess的方式进行访问权限的判断;WPF应用程序则进行了改进,所有的UI控件都采用VerifyAccess的方式进行工作线程访问权限的判断。这直接决定了本建议开头处那个例子的输出,WPF只要判断出工作线程和UI线程不是同一个线程的,则直接抛出异常,而WinForm却有成功执行的余地。但是,WinForm的这种机制直接造成了程序的不稳定,因为即使在大部分情况下代码能很好的工作,可是在不确定的情况下,那样的代码中工作线程会直接操作UI元素,这样还是会抛出异常的。

    考虑到WinForm在这个问题上的局限性,再次对WinForm的线程模型处理进行改进:

    //用于表示主线程,在本例中就是UI线程  
    Thread mainThread;  
     
    bool CheckAccess()  
    {  
        return mainThread == Thread.CurrentThread;  
    }  
     
    void VerifyAccess()  
    {  
        if (!CheckAccess())  
            throw new InvalidOperationException("调用线程无法访问此对象,因为另一个线程拥有此对象");  
    }  
     
    private void buttonStartAsync_Click(object sender, EventArgs e)  
    {  
        //当前线程就是主线程  
        mainThread = Thread.CurrentThread;  
        Task t = new Task(() =>
            {  
                while (true)  
                {  
                    if (!CheckAccess())  
                        label1.BeginInvoke(new Action(() =>
                            {  
                                label1.Text = DateTime.Now.ToString();  
                            }));  
                    else  
                        label1.Text = DateTime.Now.ToString();  
                    Thread.Sleep(1000);  
                }  
            });  
        //如果有异常,就启动一个新任务  
        t.ContinueWith((task) =>
        {  
            try  
            {  
                task.Wait();  
            }  
            catch (AggregateException ex)  
            {  
                foreach (Exception inner in ex.InnerExceptions)  
                {  
                    MessageBox.Show(string.Format("异常类型:{0}{1}来自:{2}{3}异常内容:{4}", inner.GetType(), Environment.NewLine,  
                        inner.Source, Environment.NewLine, inner.Message));  
                }  
            }  
        }, TaskContinuationOptions.OnlyOnFaulted);  
        t.Start();  
    } 

    在这段代码中,我们模拟WPF中DispatcherObject的两个方法CheckAccess和VerifyAccess对线程模型进行了重新处理,增强了系统的稳定性。在实际工作中,我们也可以提取这两个方法为扩展方法,以便项目中的所有UI类型都能使用到。

    WPF支持这两个方法,其全部代码如下所示(注意查看While循环部分):

    private void buttonStart_Click(object sender, RoutedEventArgs e)  
    {  
        Task t = new Task(() =>
        {  
            while (true)  
            {  
                this.Dispatcher.BeginInvoke(new Action(() =>
                    {  
                        textBlock1.Text = DateTime.Now.ToString();  
                    }));  
                Thread.Sleep(1000);  
            }  
        });  
        //为了捕获异常,启动了一个新任务  
        t.ContinueWith((task) =>
        {  
            try  
            {  
                task.Wait();  
            }  
            catch (AggregateException ex)  
            {  
                foreach (Exception inner in ex.InnerExceptions)  
                {  
                    MessageBox.Show(string.Format("异常类型:{0}{1}来自:{2}{3}异常内容:{4}", inner.GetType(), Environment.NewLine,  
                        inner.Source, Environment.NewLine, inner.Message));  
                }  
            }  
        }, TaskContinuationOptions.OnlyOnFaulted);  
        t.Start();  
    } 


    注意 为了演示方便,本建议中的异常没有传递到主线程。在实际编码中,应当始终考虑将异常包装到主线程。

    转自:《编写高质量代码改善C#程序的157个建议》陆敏技

  • 相关阅读:
    C#的委托事件总结
    iOS的录屏功能
    Unity通过NTP获取网络时间
    Unity的弱联网Json数据传输
    Unity场景和代码合并以及UnityYAMLMerge的使用
    Unity学习--捕鱼达人笔记
    Yomob广告在cocos2dx安卓平台的Demo
    谷歌广告Admob在cocos2dx上通过回调实现底部Banner
    JZ2440 裸机驱动 第13章 LCD控制器(2)
    JZ2440 裸机驱动 第13章 LCD控制器(1)
  • 原文地址:https://www.cnblogs.com/jesselzj/p/4743508.html
Copyright © 2020-2023  润新知