WPF的线程模型

原文:WPF的线程模型

WPF的线程模型

周银辉

谈到多线程,很多人对其可能都不太有好感,觉得麻烦与易出错。所以我们不排除有这样的情况:假设我对“多线程”、“异步”这些字眼潜意识地有些反感,所以在编码过程中能不用就不用,觉得延迟几百毫秒还是可以忍受的,如果系统中这种“可以忍受”的地方很多,最后我们会发现系统的性能变得一团糟,界面总是在“卡”(阻塞)。这里我们讨论一下WPF的多线程模型,以便利用它使我们的UI线程得到解脱。

1,UI线程

传说WPF应用程序都至少有两个线程,一个用于UI绘制,其隐藏于后台,另一个用于管理UI,包括用响应用户输入执行后台代码等。MSDN上对这两个线程是这样描述的:“Typically, WPF applications start with two threads: one for handling rendering and another for managing the UI. The rendering thread effectively runs hidden in the background while the UI thread receives input, handles events, paints the screen, and runs application code. Most applications use a single UI thread, although in some situations it is best to use several.”后者便是所谓的UI线程,是我们需要经常面对的。

 

2,Dispatcher

WPF规定了(事实上在.net2.0中便已规定了)UI元素只能由创建该元素的线程来访问。比如我们从新开的一个线程中访问主界面中的元素会出现运行时的异常。Dispatcher来维持着这一规定,并组织着消息循环。Dispatcher负责检测访问对象的线程与对象创建线程是否一致,不一致则抛出异常。值得一提的是,Dispatcher的消息循环中的Work Item是有优先级的,这可以让高优先级的项能有更多的工作时间。比如界面绘制比处理用户输入的优先级要高,这使得界面动画更加流畅。这也就是为什么,我们在调用Dispatcher.Invoke ( DispatcherPriority,…) 与Dispatcher. BeginInvoke (DispatcherPriority,…)要传入一个优先级参数的原因。下面是对各个优先级的说明:


优先级


说明


Inactive


工作项目已排队但未处理。


SystemIdle


仅当系统空闲时才将工作项目调度到 UI 线程。这是实际得到处理的项目的最低优先级。


ApplicationIdle


仅当应用程序本身空闲时才将工作项目调度到 UI 线程。


ContextIdle


仅在优先级更高的工作项目得到处理后才将工作项目调度到 UI 线程。


Background


在所有布局、呈现和输入项目都得到处理后才将工作项目调度到 UI 线程。


Input


以与用户输入相同的优先级将工作项目调度到 UI 线程。


Loaded


在所有布局和呈现都完成后才将工作项目调度到 UI 线程。


Render


以与呈现引擎相同的优先级将工作项目调度到 UI 线程。


DataBind


以与数据绑定相同的优先级将工作项目调度到 UI 线程。


Normal


以正常优先级将工作项目调度到 UI 线程。这是调度大多数应用程序工作项目时的优先级。


Send


以最高优先级将工作项目调度到 UI 线程。

上面提到了Dispatcher维持着一个规矩“只有创建该对象的线程可以访问该对象”。这里的对象不仅仅是指一些UI控件(比如Button),而是所以的派生于DispatcherObject类的对象。我们做一个小小的试验,假设有如下这样一个类:

    public class Data

    {

        private object theData = null;

        public object TheData

        {

            get

            {

                return theData;

            }

            set

            {

                theData = value;

            }

        }

}

我们在UI线程中声明其一个实例,并在新线程中使用它:

 private Data myData = new Data();

        private void btnTest_Click(object sender, RoutedEventArgs e)

        {

            ThreadStart ts = new ThreadStart(this.UpdateData);

            Thread newThread = new Thread(ts);

            newThread.Start();

        }

        private void UpdateData()

        {

            this.myData.TheData = 5;

        }

OK,不会有问题(我们暂不考虑跨多个线程访问是否安全等)。

但是,如果我们让Data类继承于DependencyObject类(其又继承于DispatcherObject类,在WPF中我们会经常这样做,因为我们要使用Dependency Property):

    public class Data : DependencyObject

    {

        public object TheData

        {

            get

            {

                return (object)GetValue(TheDataProperty);

            }

            set

            {

                SetValue(TheDataProperty, value);

            }

        }

        public static readonly DependencyProperty TheDataProperty =

            DependencyProperty.Register("TheData", typeof(object), typeof(Data), new UIPropertyMetadata(null));

    }

如果现在还按照以前的使用方式(在UI线程中声明其一个实例,并在新线程中使用它)来使用,你就会收到一个“由于其他线程拥有此对象,因此调用线程无法对其进行访问。”的System.InvalidOperationException。正确使用它的方式是使用Dispatcher的Invoke或BeginInvoke方法:

        private void UpdateData()

        {

            if (this.myData.Dispatcher.CheckAccess())

            {

                this.myData.TheData = 5;

            }

            else

            {

                this.myData.Dispatcher.Invoke(DispatcherPriority.Normal, (ThreadStart)delegate

                {

                    this.myData.TheData = 5;

                });

            }

        }

更多的,你可以访问这篇Blog:http://www.cnblogs.com/zhouyinhui/archive/2007/05/10/742134.html

3,对于阻塞的操作,不一定需要开启新线程

当我们遇到某个费时的操作是,第一反映往往是开启一个新线程,然后在后台去处理它,以便不阻塞我们的用户界面。当然,这是正确的想法。当并不是所有的都需如此。仔细想想界面阻塞的原因,我们知道其是由于时间被消耗在某个费时的Work Item上了,而那些处理用户界面的Work Item还在那里苦苦等候。So,我们只要别让他们苦苦等候就可以了,只要用户有界面操作我们就处理,线程上的其他空闲时间来处理我们的复杂操作。我们将复杂的费时的操作细化成很多不费时的小操作,在这些小操作之间的空隙处我们来处理相应用户界面的操作

阻塞的情况如下, MouseUp与MouseLeave会被阻塞:

(MouseDown)->(费时的,复杂操作)->(MouseUp)->(MouseLeave)…

细化后的情况如下,MouseUp与MouseLeave不会被阻塞:

(MouseDown)->(不费时的,小操作,复杂操作的1/n)->(MouseUp)->(不费时的,小操作,复杂操作的1/n) -> (MouseLeave)…

举一个简单的例子,假定我们的主界面上要显示一个数字,其为Window1的CurrentNum属性,我们已经将界面上的某个TextBlock与其绑定了起来:

            <TextBlock x:Name="textBlock_ShowNum"

                       Text="{Binding ElementName=window1,Path=CurrentNum}"

                       VerticalAlignment="Center"

                       HorizontalAlignment="Center" />

当我们点击界面上的一个按钮后,要求该数字被不停的累加,直到再次点击该按钮是停止.实际效果相当于:

            while (this.IsCalculating)

            {

                this.CurrentNum++;

            }

如果我们直接按照上面的While语句来书写程序,明显,当用户点击按钮后,整个线程将在这里被堵死,界面得不到更新,用户也没有办法再次点击按钮来停止这个循环,遭透了。

既不开启新线程又不阻塞界面应该怎么办呢?

我们知道this.CurrentNum++;语句以及更新绑定到CurrentNum属性的TextBlock并不耗费时间的,耗费时间的是他们的累加而成的死循环,所以,我们将这个循环分解成无数个仅仅由this.Current++语句组成的小方法,并在这些小方法的之间来处理用户界面:

public delegate void NormalDelegate();

        void button_StartOrStop_Click(object sender, RoutedEventArgs e)

        {

            if (this.IsCalculating)

            {

                NormalDelegate calNumDelegate = new NormalDelegate(this.CalNum);

                this.Dispatcher.BeginInvoke(DispatcherPriority.SystemIdle, calNumDelegate);

            }

        }

        private void CalNum()

        {

            this.CurrentNum++;

            if (this.IsCalculating)

            {

                NormalDelegate calNumDelegate = new NormalDelegate(this.CalNum);

                this.Dispatcher.BeginInvoke(DispatcherPriority.SystemIdle, calNumDelegate);

            }

        }

上面的两段代码可以简单地如下示意:

阻塞的情况如下, MouseUp与MouseLeave会被阻塞:

(MouseDown)->(费时的While(true))->(MouseUp)->(MouseLeave)…

细化后的情况如下,MouseUp与MouseLeave不会被阻塞:

(MouseDown)->(不费时的CalNum)->(MouseUp)->(不费时的CalNum) -> (MouseLeave)…

4,用Delegate.Invoke()或Delegate.BeginInvoke()来开启新线程

除了new 一个Thread对象外,使用Delegate的Invoke或BeginInvoke方法也可以开启新的线程。

假设有下面这一个很费时的方法,我们应该如何使用Delegate来改造呢

        private void TheHugeMethod()

        {

            Thread.Sleep(2000);

            this.button_Test.Content = "OK!!!";

        }

首先,我们声明一个可以用于TheHugeMethod方法的代理:

public delegate void NormalMethod();

然后对TheHugeMethod构造一个NormalMethod类型的对象,并调用其Invoke方法(同步调用)或BeginInvoke方法(异步调用)

        void button_Test_Click(object sender, RoutedEventArgs e)

        {

            NormalMethod hugeMethodDelegate = new NormalMethod(this.TheHugeMethod);

            hugeMethodDelegate.BeginInvoke(null, null);

        }

由于是开启了新的线程,所以TheHugeMethod方法中对this.button_Test控件的调用语句也得改造一下:

        private void TheHugeMethod()

        {

            Thread.Sleep(2000);

            //will crash

            //this.button_Test.Content = "OK!!!";

            NormalMethod updateUIDelegate = new NormalMethod(this.UpdateUI);

            this.button_Test.Dispatcher.BeginInvoke(DispatcherPriority.Normal, updateUIDelegate);

        }

        private void UpdateUI()

        {

            this.button_Test.Content = "OK!!! ";

        }

5,在新线程中执行消息循环

一般情况下我们不需要在新线程中执行消息循环了,因为我们常常是在新线程中执行一些后台操作而不需要用户在新线程中执行UI操作(比如我们在新线程中从网络上下载一些数据然后UI线程来显示这些数据)。当有时新线程却是需要消息循环的,最简单的例子是操作系统的“资源管理器”,每一个资源管理器窗口都在一个单独的线程中(它们都在同一个进程中)。

但当你按照如下方式编写代码来新建一个资源管理器窗口时,会出问题:

        private void button_NewWindow_Click(object sender, RoutedEventArgs e)

        {

            Thread newWindowThread = new Thread(new ThreadStart(ThreadStartingPoint));

            newWindowThread.SetApartmentState(ApartmentState.STA);

            newWindowThread.IsBackground = true;

            newWindowThread.Start();

        }

        private void ThreadStartingPoint()

        {

            Window1 newWindow = new Window1();

            newWindow.Show();

        }

问题是newWindow闪现一下就消失了。因为该新窗口没有进入消息循环,当newWindow.Show()方法执行完毕后,新线程的一切都结束了。

正确的方法是在newWindow.Show();方法后加入Dispatcher.Run()语句,其会将主执行帧推入该Dispatcher的消息循环中。

        private void ThreadStartingPoint()

        {

            Window1 newWindow = new Window1();

            newWindow.Show();

            System.Windows.Threading.Dispatcher.Run();

        }

6,BackgroundWorker实质是:基于事件的异步模式

在多线程编程中,最爽的莫过于.net 提供了BackgroundWorker类了。其可以:

“在后台”执行耗时任务(例如下载和数据库操作),但不会中断您的应用程序。

同时执行多个操作,每个操作完成时都会接到通知。

等待资源变得可用,但不会停止(“挂起”)您的应用程序。

使用熟悉的事件和委托模型与挂起的异步操作通信。

我想大家对BackgroundWorker亦是再熟悉不过了,这里就不多做介绍了,另外“基于事件的异步模式”是WinForm的内容,但在WPF中完美运用(原因是WPF用DispatcherSynchronizationContext扩展了SynchronizationContext),可以参见MSDN“Event-based Asynchronous Pattern Overview”

原文地址:https://www.cnblogs.com/lonelyxmas/p/10643510.html

时间: 2024-09-29 03:55:11

WPF的线程模型的相关文章

编写高质量代码改善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(()

多线程——线程模型

什么是程序? 安装在磁盘上的一段指令集合,它是静态的概念. 什么是进程? 它是运行中的程序,是动态的概念,每个进程都有独立的资源空间. 什么是线程? 线程,又称为轻量级进程,是程序执行流的最小单元,是程序中一个单一的顺序控制流程.线程是进程的一个实体,是被系统独立调度和分派的基本单位. 什么是多线程? 多线程则指的是在单个程序中可以同时运行多个不同的线程执行不同的任务. 多线程的特点 ①   一个进程可以包含一个或多个线程. ②   一个程序实现多个代码同时交替运行就需要产生多个线程. ③  

JS线程模型&amp;Web Worker

js线程模型 客户端javascript是单线程,浏览器无法同时运行两个事件处理程序 设计为单线程的理论是,客户端的javascript函数必须不能运行太长时间,否则会导致web浏览器无法对用户输入做出响应.这也是为什么Ajax的API都是异步的,以及为什么客户端Javascript不能使用一个简单的异步load()或者require()函数来加载javascript库 如果应用程序不得不执行太多的计算而导致明显的延迟,应该允许文档在执行这个计算之前完全载入,并且确保告诉用户正在进行计算并且浏览

线程模型的综述

本文首先介绍了一些线程基础,比如并发.并行.内存分配.系统调用.POSIX线程.接着通过strace分析了线程与进程的区别.最后以Android.Golang等线程模型进行了分析. 基础 1. 什么是并发(Concurrent),什么是并行(Parallels)? 并发指同时进行多个计算任务. 并行指通过切换时间片模拟进行多个计算任务. 详细可以参考Difference between concurrent programming and parallel programming - stack

Netty线程模型

一.Reactor模型 1.单线程模型 Reactor单线程模型,指的是所有的IO操作都在同一个NIO线程上面完成,NIO线程的职责如下: 1)作为NIO服务端,接收客户端的TCP连接: 2)作为NIO客户端,向服务端发起TCP连接: 3)读取通信对端的请求或者应答消息: 4)向通信对端发送消息请求或者应答消息 Reactor单线程模型示意图如下所示: 由于Reactor模式使用的是异步非阻塞IO,所有的IO操作都不会导致阻塞,理论上一个线程可以独立处理所有IO相关的操作.从架构层面看,一个NI

Netty系列之Netty线程模型

1. 背景 1.1. Java线程模型的演进 1.1.1. 单线程 时间回到十几年前,那时主流的CPU都还是单核(除了商用高性能的小机),CPU的核心频率是机器最重要的指标之一. 在Java领域当时比较流行的是单线程编程,对于CPU密集型的应用程序而言,频繁的通过多线程进行协作和抢占时间片反而会降低性能. 1.1.2. 多线程 随着硬件性能的提升,CPU的核数越来越越多,很多服务器标配已经达到32或64核.通过多线程并发编程,可以充分利用多核CPU的处理能力,提升系统的处理效率和并发性能. 相关

MyCat线程模型分析

参考MyCat权威指南,对MyCat-Server里面的线程模型做简要分析: 1. 线程模型图 根据MyCat权威指南,做出以下线程模型图: MyCat的线程模型主要分为三部分(: 网络通讯线程.业务线程和定时任务线程,下面分别介绍这些线程的使用: [温馨提示] 这里排除JVM自身使用的线程,只关注MyCat服务所使用的线程,如果需要详细了解MyCat里面使用的所有线程,请参考<MyCat权威指南>-> 开发篇 -> MyCat线程模型分析 2. 网络通讯线程 MyCat-Serv

看我是如何处理自定义线程模型---java

看过我之前文章的园友可能知道我是做游戏开发,我的很多思路和出发点是按照游戏思路来处理的,所以和web的话可能会有冲突,不相符合. 来说说为啥我要自定义线程模型呢? 按照我做的mmorpg或者mmoarpg游戏划分,线程被划分为,主线程,全局同步线程,聊天线程,组队线程,地图线程,以及地图消息分发派送线程等: 一些列,都需要根据我的划分,以及数据流向做控制. 游戏服务器,主要要做的事情,肯定是接受玩家的 命令请求 -> 相应的操作 -> 返回结果: 在服务器端所有的消息都会注册到消息管理器里,然

【转】netty线程模型

Netty服务器线程模型概览 博客分类: netty java 一切从ServerBootstrap开始 ServerBootstrap 负责初始话netty服务器,并且开始监听端口的socket请求. Java代码   bootstrap bootstrap = new ServerBootstrap( new NioServerSocketChannelFactory( Executors.newCachedThreadPool(),//boss线程池 Executors.newCached