DispatcherObject与WPF线程模型

用Reflector分析WPF时,发现几乎所有的类的继承自DispatcherObject类。而该类非常简单,只有CheckAccess和VerifyAccess两个方法,一个Dispatcher只读属性和一个私有成员变量。正是由于继承了这个类,使WPF中其他类具有了多线程处理的能力。在分析DispatcherObject前,我们先看看一般Win32或WinForm GUI应用程序中存在的问题和解决办法。"没有响应":Message Pump被阻塞 使用Windows时,最常碰到的问题就是窗口没有响应的。OS发现一个窗口一定时间内没有处理消息队列时,OS就会显示该窗口"没有响应"。消息队列是OS为窗口创建的一个线程相关的内部结构。所有对窗口的操作,如鼠标移到,键盘输入等,最终都是在该队列中添加了一条消息。一般我们称创建主窗口的线程为UI线程,该线程创建Message Pump,负责不断的从消息队列中读取消息事件,并执行相应的窗口函数。在Win32中,是通过调用GetMessage或PeekMessage来读取队列中的消息,并执行窗口处理函数WinProc。WinFom是对Win32 API封装,Application.Run()建立了Message Pump,从队列中读取消息,并调用对应的Event Handler。正是由于Windows这样的设计,如果Event Handler耗时很长时,UI线程无法继续处理消息队列中的其他消息(如WM_Paint),从而窗口没有响应或更新。这就是GUI应用程序中常碰到的Message Pump被阻塞的问题。 如下面的例子,在Button.Click Event Handler中,我们从数据库或文件中读出数据,并将每条记录加入List列表中。由于Message Pump被阻塞,UI线程无法处理WM_Paint消息,因而List只有在所以数据都加入后,才会更新显示。 1 private void button1_Click(object sender, EventArgs e) 2 { 3 // Clear the list 4 listBox1.Items.Clear(); 5 6 // Get data from database or file 7 for (Int32 i = 0; i < 100000; i++) 8 { 9 // Add the data to list 10 listBox1.Items.Add(i); 11 } 12 } 解决办法之一:Application.DoEvents 既然问题是由于没有处理消息队列造成的,那么比较直接的一个办法就是调用GetMessage或PeekMessage。WinForm中对于的函数就是Application.DoEvents()方法。 1 private void button1_Click(object sender, EventArgs e) 2 { 3 // Clear the list 4 listBox1.Items.Clear(); 5 6 // Get data from database or file 7 for (Int32 i = 0; i < 100000; i++) 8 { 9 // Add the data to list 10 listBox1.Items.Add(i); 11 Application.DoEvents(); 12 } 13 } 似乎这样就解决问题了,但是上面的方法实际上引入了一个更严重的问题:Reentrancy。运行上面的程序,我们先点击一次按钮,button1_Click被调用,listBox1.Items.Clear()清空列表,然后开始添加数据到列表。如果这个时候,我们再次点击按钮,一个Button Click消息被加入到消息队列。在没有调用Application.DoEvents时,这条新的消息只有在button1_Click处理完后,才会被调用。这样尽管界面有段时间没有响应,但list中的数据仍能保持完整。当我们加入Application.DoEvents()后,在第一次处理button1_Click过程中,新的消息再次被处理,button1_Click第二次被调用,同样listBox1被清空。但是由于第一次的button1_Click还执行完,后面的Add动作仍会在第二次button1_Click都执行完成后执行,List就变成了1,2,3,4,5,6,4,5,6。而这种行为造成程序不稳定性,有时候数据是好的,有时候又有重复的数据。 造成Reentrancy问题原因是由于DoEvents会处理消息队列中的所有消息,如果DoEvents能够提供只处理WM_Paint事件的话,就不会有这个问题。但是由于消息队列结构的设计不完全支持优先级,所以Win32或WinForm没法解决这个问题。 解决办法之二:拆分复杂的操作 另一个解决办法就是把复杂的操作拆分成很多小的操作,每次只执行一部分。这种办法主要有两个问题:不易拆分或增加了代码的难度,如何调用这些小的操作。如上面的例子中,我们在button1_Click中,我们创建一个System.Windows.Forms.Timer对象,在Tick事件中来添加list的Item。这种办法有比较大的局限,而且同样会存在Reentrancy的问题。解决办法之三:多线程 在看多线程前,让我们看看创建一个线程的消耗: 创建Thread核心对象(128K?),保留1M的Thread Stack地址空间 CPU Task Manager多一个Task 线程切换非常耗时 是的,如果是单CPU的机器,多线程就意味着性能降低,因为CPU增加了额外的工作。我们需要多线程的原因就是因为UI线程必须能够释放出来去处理消息队列。在Win32开发中,我们可以调用CreateThread来创建一个线程(Worker线程),来处理耗时的操作,在.Net中对应的就是创建一个Thread对象。这种办法的问题是,每次创建一个Thread,OS都会创建Thread对象,而使用完后该Thread就会被销毁。创建销毁线程对象都是耗时的工作。.Net提供了线程池,可以重复利用线程来提高性能。我们可以用ThreadPool类,或者Delegate的BeginInvoke来使用.Net线程池。 于是,上面的代码很自然的就改为: 1 private void button1_Click(object sender, EventArgs e) 2 { 3 ThreadPool.QueueUserWorkItem(new WaitCallback(DoWork)); 4 } 6 private void DoWork(Object state) 7 { 8 listBox1.Items.Clear(); 9 for (Int32 i = 0; i < 10000; i++) 10 { 11 listBox1.Items.Add(i); 12 } 13 } 于是编译执行上面的代码,噢,没有问题。但是当你发布给用户时,用户就会抱怨,这个程序有时候突然就没了,于是噩梦就开始了。造成这个问题的原因就是由于Worker线程访问了控件,窗口等GDI对象。为了帮助找到这种问题,在用VS2008调试上面程序时,程序会报Cross-Operation的异常。但是,在Release版本中,程序就会崩溃,并且没有好的工具或方法能够找到这个问题。所以,在设计代码时,就一定注意避免Worker线程访问控件。为什么Windows会有这样的限制呢?我想这个主要是由于性能的考虑。如果Windows GDI对象要支持多线程的访问,则每次访问时都需要lock来保证数据的完整性,不然界面的行为就会很怪异,每次GDI操作都判断lock的话,界面将会非常非常的慢。如果不用lock的方式,而采用判断Thread.ID的方式,那么每个对象都必须在创建时有个成员变量来保存 Thread ID。在Windows 32的时代,内存是很宝贵的资源,所以Windows选择了忽略这个问题也是可以理解的。那么,我们如何可以解决这个问题呢?在WinForm中,定义了一个ISynchronizeInvoke的接口,Control实现了这个接口。该接口中InvokdRequired属性,用来判断调用线程是否是Control的创建线程,而BeginInvoke和Invoke方法,这是会发消息到Control对应的消息队列,来告诉UI线程来更新Control。于是,上面的代码就变成: 1 public Form1() 2 { 3 InitializeComponent(); 4 updateList = new UpdateUICallBack(UpdateList); 5 } 6 7 private void button1_Click(object sender, EventArgs e) 8 { 9 ThreadPool.QueueUserWorkItem(new WaitCallback(DoWork)); 10 } 11 12 private delegate void UpdateUICallBack(Int32 value); 13 private UpdateUICallBack updateList; 14 private void DoWork(Object state) 15 { 16 for (Int32 i = 0; i < 10000; i++) 17 { 18 updateList(i); 19 } 20 } 21 22 private void UpdateList(Int32 value) 23 { 24 if (listBox1.InvokeRequired) 25 listBox1.Invoke(updateList, new Object[] { value}); 26 else 27 listBox1.Items.Add(value); 28 } 一切似乎很完美了,当在Worker线程上调用updateList时,listBox1.InvokeRequried返回True,调用listBox1.Invoke(),Worker线程会等待Invoke()返回,而Invoke()则会调用Win32 API SendMessage()给UI线程的消息队列,UI线程再次调用UpdateList更新ListBox后返回。当然,如果我们不想Worker线程被阻塞的话,可以通过调用BeginInvoke()和EndInvoke()的异步方法。不过,不管那种方式,代码都会变得比较难看。而且,尽管InvokeRequired是个属性,实际上,会执行很多操作。InvokeRequired拿到调用线程的ID,然后尝试去得到Control的创建线程ID。而得到Control的创建线程ID会是一个耗时的操作。因为Control创建时并没有保存线程ID到成员变量中。所以InvokeRequired会遍历Control的父Control,一层层,直到找到最外面的父窗口。因而InvokeRequired是一个耗时的操作。为了解决InvokeRequired的性能问题,我们可以在创建窗口的时候,保存一个Thead的引用,然后再来判断,于是代码变为: 1 public partial class Form1 : Form 2 { 3 private Thread ownerThread; 4 public Form1() 5 { 6 InitializeComponent(); 7 updateList = new UpdateUICallBack(UpdateList); 8 ownerThread = Thread.CurrentThread; 9 } 10 11 private Boolean CheckAccess() 12 { 13 return ownerThread == Thread.CurrentThread; 14 } 15 16 private void VerifyAccess() 17 { 18 if (!CheckAccess()) 19 throw new Exception("Invoke Needed"); 20 } 22 private void UpdateList(Int32 value) 23 { 24 if (!CheckAccess()) 25 listBox1.Invoke(updateList, new Object[] { value }); 26 else 27 listBox1.Items.Add(value); 28 } 29 } 在上一篇中我们分析了Win32和WinForm编写GUI应用程序会面对的主要问题。总结下来最重要的就是:如何高效的从Worker线程中更新界面。所以首先让我们看看WPF中是如何达到这个目的的。 DispatcherObject的使用 DispatcherObject类有两个成员方法,CheckAccess和VerifyAccess。CheckAccess功能和WinForm中Control.InvokeRequired属性相同,当调用线程与对象的创建线程不是同一个实例时,返回False。VerifyAccess则是抛出异常。 DispatcherObject还有一个Dispatcher的属性,返回控件的创建Dispatcher。通过其Invoke方法,我们可以将界面更新的操作Marshal到控件的创建线程。如下例: 1 TextBox textbox = new TextBox(); 2 if (!textbox.CheckAccess) 3 { 4 textbox.Dispatcher.Invoke(DispatcherPriority.Send, delegate { }); 5 } CheckAccess()的实现方法类似于上篇的最后一个例子,通过比较两个线程实例,因而速度很快,只需要几个IL指令。继承DispatcherObject的类,在内部都调用VerifyAccess来做判断,因而当非创建线程调用时,就会抛出异常。这样的好处是我们可以较早的发现代码的问题,不像WinForm那样不确定的发生。 好了,下面我们来看看真正做事情的Dispatcher类,在这之前让我们再回顾一个Win32的消息循环。 Win32 消息循环 在Win32中,消息Pump的建立是通过循环的调用GetMessage,当收到WM_Quit类型的消息是,退出。而我们对窗口或控件属性的修改,如颜色,位置都实际都是调用PostMessage或SendMessage。当调用线程与窗口的Owner线程是同一个,则SendMessage直接调用窗口函数;如不同则将消息放置在窗口对应的消息队列中,由GetMessage取出并进行处理。 WinForm实际是建立在Win32的基础上,所以当我们写lable.Text = "Hello World"时,实际是调用了PostMessage等方法。 下面我们看看Win32消息队列的特点和限制: 任劳任怨的从消息队列中拿出消息,并调用对应的窗口处理函数 当消息Post进队列后就无法控制了 消息的处理支持有限的优先级,但都由OS控制,用户代码不能控制和改变优先级 Application.DoEvents只有在处理完消息队列中所有消息后才返回,当然Win32没有这个限制,因为Message Pump就是用户创建的 可以添加钩子,偷偷的干些坏事 WPF中的Dispatcher的主要功能类似于Win32中消息队列,当并不是替换消息循环,而是建立在消息循环之上。首先,让我们看看Dispatcher的结构: 线程相关的Dispatcher的创建 当我们在一个线程上第一次创建WPF对象时,DispatcherObject构造函数会调用Dispatcher.CurrentDispatcher赋值给私有变量_dispatcher。Dispatcher.CurrentDispatcher通过Thread.CurrentThread来找到该线程对应的Dispatcher实例,如不存在则创建一个。Dispatcher的构造函数,首先创建了一个具有11个级优先级(DispatcherPriority)的优先队列PriorityQueue,这个优先队列用来保存不同优先级的操作。然后调用Win32 API RegisterClassEx注册窗口处理函数,并调用CreateWindowEx创建一个不可见的窗口。 Dispatcher有两个静态方法用来建立消息循环,定义如下: 1 public sealed class Dispatcher 2 { 3 public static void Run() 4 { 5 PushFrame(new DispatcherFrame()); 6 } 7 public static void PushFrame(DispatcherFrame frame) {…} 8 } 尽管是静态方法,实际上是调用当前线程对应的Dispatcher实例的方法。Application.Run()内部实际就是调用了Dispatcher.Run(),进而调用Dispatcher.PushFrame(…)。当我们创建一个WPF项目时,VS自动生成的App.xaml,编译后的代码为 1 public static void Main() 2 { 3 WpfHello.App app = new WpfHello.App(); 4 app.InitializeComponent(); 5 app.Run(); 6 } 当调用PushFrame()或Run()后,内部开始循环调用Win32 GetMessage方法,而建立消息循环。如下面的例子中,在VS缺省的WPF项目中,创建了一个新的线程,在该线程中创建一个新的WPF窗口,并启动消息循环: 1 public partial class App : Application 2 { 3 protected override void OnStartup(StartupEventArgs e) 4 { 5 // Create a new thread 6 Thread monitorThread = new Thread(QueueMonitor.ThreadMain); 7 monitorThread.SetApartmentState(ApartmentState.STA); 8 monitorThread.Start(this); 9 10 base.OnStartup(e); 11 } 12 } 13 14 class QueueMonitor 15 { 16 public static void ThreadMain(Object obj) 17 { 18 QueueMonitor monitor = new QueueMonitor(obj as Application); 19 } 20 21 private Application m_target; 22 private DispatcherFrame m_frame; 23 private MonitorWindow m_window; 24 public QueueMonitor(Application target) 25 { 26 m_target = target; 27 28 m_frame = new DispatcherFrame(); 29 m_window = new MonitorWindow(); 30 31 m_window.Closed += MonitorWindow_Closed; 32 m_window.Visibility = Visibility.Visible; 33 34 Dispatcher.Run(); 35 // Dispatcher.PushFrame(m_frame); 36 } 37 38 void MonitorWindow_Closed(object sender, EventArgs e) 39 { 40 Dispatcher.ExitAllFrames(); 41 // m_frame.Continue = false; 42 } 43 } 消息循环的控制 上个例子中,我们也可以使用PushFrame方法。相比于Run(),我们可以通过PushFrame的参数DispatcherFrame对象来控制什么时候退出消息循环。如需退出当前层的循环,只需将对应的DispatcherFrame.Continue赋值false。我们可以嵌套的调用PushFrame,这点上类似Application.DoEvents,更具灵活性,但我们仍不能只要求处理特定优先级的消息。 另外可以调用Dispatcher.ExitAllFrames方法退出当前线程的所有消息循环。Dispatcher还提供一个方法DisableProcessing,该方法可以暂停消息循环处理消息。下面是这个方法的使用方法: 1 using (Dispatcher.DisableProcessing()) 2 { 3 Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Render, delegate { }); 4 } 该方法返回的DispatcherProcessingDisabled对象实现了IDisposable接口,在Dispose中恢复消息循环。该方法也可以嵌套的调用。 下面我们来看看如何投递消息,及Dispatcher如何处理的。 Dispatcher优先队列 Dispatcher的Invoke和BeginInvoke方法,用来向Dispatcher的优先队列放置任务,前者是同步方法,后者是异步。这两个方法,第二个参数是个delegate对象,代表要执行的任务。第一个参数定义任务的优先级,具体定义可以看DispatcherPriority枚举类型。总的来说,我们可以划分为两类,Background和Foreground,另一个比较特殊的是DispatcherPriority.Send。 当在Invoke中指定Send时,且CheckAccess为真,则Invoke直接调用delegate执行任务。如果不是,则调用BeginInvoke,并等待结果,或超时。 BeginInvoke执行时,首先将该任务封装成DispatcherOperation对象,并放置在对应的优先队列中。然后判断是Background还是Foreground,如果是Foreground,则调用PostMessage往Win32消息队列中投递一条消息,然后返回。如果是Background,则检查是否有Timer,如没有则创建一个Timer,Timer会不断循环的投递消息到Win32消息队列,来触发消息的处理。当有Foreground消息是,则删掉Timer。通过这种方式,系统在空闲的时候可以处理Background消息。 当有Win32消息投递到Win32队列时,注册的窗口函数被执行,从优先队列中取出一个DispatcherOperation来执行。完成后,则投递新的Win32消息来触发下次执行,或等待Timer消息。 BeginInvoke的返回值则为DispatcherOperation对象,通过她我们可以取消,等待,或者调整该任务的优先级。在后面的系列中,我们在具体看不同Foreground优先级的使用。 优先队列钩子Hook 与Win32类似,我们也可以对消息的处理添加Hook,可以添加下面的Event Handler: 1 Dispatcher.CurrentDispatcher.Hooks.OperationAborted += new DispatcherHookEventHandler (Hooks_OperationAborted); 2 Dispatcher.CurrentDispatcher.Hooks.OperationCompleted += ; 3 Dispatcher.CurrentDispatcher.Hooks.OperationPosted += ; 4 Dispatcher.CurrentDispatcher.Hooks.OperationPriorityChanged += Event Handler的参数中,我们可以获得对应的Dispatcher和DispatcherOperation对象。通过这种办法,我们可以过滤,查询或改变任务的优先级等操作。 DispatcherTimer DispatcherTimer类似WinForm中的Timer。我们可以在构造函数中指定优先级,Dispatcher实例等等。 总结 总的来说,与Win32比较,如果给WPF中的线程和消息循环的机制打分的话,我觉得可以打4分的高分。WPF解决了Win32中没有优先级,跨线程调用性能的问题,友好的编程接口。如果说不足的话,如果PushFrame可以支持优先级,对Reentrancy的问题,可能能更好的控制;另外,DispatcherOperation无法获得名字,如我们要开发一个队列监视程序的话很不方便。这些我认为可以扣0.5分,那另外0.5分是什么呢? 最后,如果我们看这些类的话,全部都在Windowsbase.dll中,也就是说System.Windows.Threading中的类,如DispatcherObject,Dispatcher也可以用于其他系统中。我们甚至可以利用这些在我们的系统中。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值