找回密码
 立即注册
首页 业界区 业界 深入WPF -- Dispatcher(补)

深入WPF -- Dispatcher(补)

昝梓菱 2025-5-29 15:37:26
  书接前文,前篇文章介绍了WPF中的Dispatcher,由于概念太多,可能不是那么好理解。这篇文章继续讨论,希望在线程和Dispatcher这个点上,能把它讲透。
  从哪说起?

    按照惯例,在深入问题之前,先找一个插入点,希望这个插入点能为朋友们所理解。
    新建一个Window程序,代码如下:
  
        
  1. int WINAPI _tWinMain(HINSTANCE hInstance,  HINSTANCE hPrevInstance,
  2.                      LPTSTR    lpCmdLine,  int       nCmdShow)
  3. {
  4.     <font color="#0000ff">RegisterWindowClass</font>(hInstance);                                          //1 
  5.     HWND hWnd = <font color="#800080">CreateWindow</font>(<font color="#ff0000">szWindowClass</font>, szTitle, WS_OVERLAPPEDWINDOW,        //2
  6.         CW_USEDEFAULT, 0, CW_USEDEFAULT,z 0, NULL, NULL, hInstance, NULL);
  7.     <font color="#800080">ShowWindow</font>(hWnd, nCmdShow);
  8.     MSG msg;                                                                     //3
  9.     while (GetMessage(&msg, NULL, 0, 0))
  10.     {
  11.         TranslateMessage(&msg);
  12.         DispatchMessage(&msg);
  13.     }
  14.     return (int) msg.wParam;
  15. }
复制代码
      
  其中的RegisterWindowClass
      
  1. WORD RegisterWindowClass(HINSTANCE hInstance)
  2. {
  3.     WNDCLASSEX wcex;
复制代码
      
  1.     wcex.lpszClassName  = <font color="#ff0000">szWindowClass</font>;
复制代码
      
  1.     wcex.lpfnWndProc    = <font color="#800080">WndProc</font>;
  2.     ...
  3. }
  4. LRESULT CALLBACK <font color="#800080">WndProc</font>(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
  5. {
  6.     switch (message)
  7.     {
  8.     case WM_PAINT:
  9.     ...
  10. }
复制代码
      
  这个创建窗口并显示的过程如下:
      
  • 调用RegisterWindowClass注册窗口类,关联其中的窗口过程WndProc。  
  • 调用CreateWindow创建窗口并显示。  
  • (主线程)进入GetMessage循环,取得消息后调用DispatchMessage分发消息。
  这里的GetMessage循环就是所谓的消息泵,它像水泵一样源源不断的从线程的消息队列中取得消息,然后调用DispatchMessage把消息分发到各个窗口,交给窗口的WndProc去处理。
  用一副图来表示这个过程:
1.png

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 


      
  • 鼠标点击。  
  • 操作系统底层获知这次点击动作,根据点击位置遍历找到对应的Hwnd,构建一个Window消息MSG,把这个消息加入到创建该Hwnd线程的消息队列中去。  
  • 应用程序主线程处于GetMessage循环中,每次调用GetMessage获取一个消息,如果线程的消息队列为空,则线程会被挂起,直到线程消息队列存在消息线程会被重新激活。  
  • 调用DispatchMessage分发消息MSG,MSG持有一个Hwnd的字段,指明了消息应该发往的Hwnd,操作系统在第2步构建MSG时会设置这个值。  
  • 消息被发往Hwnd,操作系统回调该Hwnd对应的窗口过程WndProc,由WndProc来处理这个消息。
  这是一个简略的Window消息处理流程,往具体说这个故事会很长,让我们把目光收回到WPF,看看WPF和即将介绍的Dispatcher在这个基础上都做了些什么,又有哪些出彩的地方。
仍然从Main函数说起

  作为应用程序的入口点,我们仍然从Main函数走进WPF。
  新建一个WPF工程,如下:
2.png

 
 
 
 
 
  默认的WPF工程中中是找不到传统的Program.cs文件的,它的App.xaml文件的编译动作为ApplicationDefinition,编译后,编译器会自动生成App.g.cs文件,包含了Main函数。如下:
      
  1.         [System.STAThreadAttribute()]
  2.         [System.Diagnostics.DebuggerNonUserCodeAttribute()]
  3.         public static void Main()
复制代码
      
  1.     {
  2.             WpfApplication3.App app = new WpfApplication3.App();
  3.             app.InitializeComponent();
  4.             app.Run();
  5.         }
复制代码
      
  这里出现了Application类,按MSDN上的解释,“Application 是一个类,其中封装了 WPF 应用程序特有的功能,包括:应用程序生存期;应用程序范围的窗口、属性和资源管理;命令行参数和退出代码处理;导航”等。
  调用app.Run()之后,按照前面Win32的步骤,应用程序应进入到一个GetMessage的消息泵之中,那么对WPF程序来说,这个消息泵是什么样的呢?又和Dispatcher有什么关系呢?
走进Dispatcher

  Dispatcher的构造函数是私有的,调用Dispacher.CurrentDispatcher会获得当前线程的Dispatcher,Dispatcher内部持有一个静态的所有Dispatcher的List。因为构造函数私有,只能调用CurrentDispatcher来获得Dispatcher,可以保证对同一个线程,只能创建一个Dispatcher。
  Dispatcher提供了一个Run函数,来启动消息泵,内部的核心代码是我们所熟悉的,如:
      
  1.         while (frame.Continue)
  2.         {
  3.             if (!GetMessage(ref msg, IntPtr.Zero, 0, 0))
  4.                 break;
  5.             TranslateAndDispatchMessage(ref msg);
  6.         }
复制代码
      
  这里出现了一个Frame的概念,暂且不谈,来看看Dispatcher相对于传统的消息循环,有哪些改进的地方。
Dispatcher的新意

  在Winform的消息循环中,
      
  • 为了线程安全,调用Control的Invoke或者BeginInvoke方法可以在创建控件的线程上执行委托,方法的返回值分别为object和IAsyncResult。尽管可以使用IAsyncResult的IsCompleted和AsyncWaitHandle等方法来轮询或者等待委托的执行,但对于对任务的控制来讲,这个粒度是不够的,我们不能取消(Cancel)一个已经调用BeginInvoke的委托任务,也不能更换两个BeginInvoke的执行顺序。  
  • 更为友好的接口支持,Windows编程中,在窗口消息循环中加入Hook是常见的需求,Dispatcher提供了DispatcherHooks类,以Event的形式对外提供了OperationAborted,OperationCompleted,OperationPosted等事件。
  这里的Operation指的是DispatcherOperation,为了更好的控制消息循环,WPF引入了DispatcherOperation来封装Window消息,这个DispatcherOperation如下:
DispatcherOpration

      
  1. [code]public sealed class DispatcherOperation
复制代码
  1. {
复制代码
  1.     public Dispatcher Dispatcher { get; }
复制代码
  1.     public DispatcherPriority Priority { get; set; }
复制代码
  1.     public object Result { get; }
复制代码
  1.     public DispatcherOperationStatus Status { get; }
复制代码
  1. [/code][code]    public event EventHandler Aborted;
复制代码
  1.     public event EventHandler Completed;
复制代码
  1. [/code][code]
复制代码
  1.     public bool Abort();
复制代码
  1.     public DispatcherOperationStatus Wait();
复制代码
  1.     public DispatcherOperationStatus Wait(TimeSpan timeout);
复制代码
  1. }
复制代码
[/code]      
  DispatcherOperation类看起来还是比较简单明了的,以属性的形式暴露了Result(结果),Status(状态),以及用事件来指出这个Operation何时结束或者取消。其中比较有意思的是Priority属性,从字面来看,它表示了DispatcherOperation的优先级,而且提供了get和set方法,也就是说,这个DispatcherOperation是可以在运行时更改优先级的。那么这个优先级是怎么回事,Dispatcher又是如何处理DispatcherOperation的呢,让我们深入DispatcherOperation,来看看它是如何被处理的。
深入DispatcherOperation(DO)

  所谓深入,也要有的放矢,从三个方面来谈一下DispatcherOperation:
      
  • DispatcherOperation是如何被创建的。  
  • DispatcherOperation是何时被执行的。  
  • DispatcherOperation是怎样被执行的。
  Dispatcher提供了BeginInvoke和Invoke两个方法,其中BeginInvoke的返回值是DispatcherOperation,Invoke函数的内部调用了BeginInvoke,也就是说,DispatcherOperation就是在这两个函数中被创建出来的。我们可以调用这两个函数创建新的DO,WPF内部也调用了这两个函数,把Window消息转化为DispatcherOperation,用一副图表示如下:
3.png

 
 
 
 
 
 
 
 
      
  • 窗口过程WndProc接收到Window消息,调用Dispatcher的Invoke方法,创建一个DispatcherOperation。Dispatcher内部持有一个DispatcherOperation的队列,用来存放所有创建出来的DispatcherOperation。默认一个DO被创建出来后,会加入到这个队列中去。WndProc调用Invoke的时候比较特殊,他传递的优先级DispatcherPriority为Send,这是一个特殊的优先级,在Invoke时传递Send优先级WPF会直接执行这个DO,而不把它加入到队列中去。  
  • 用户也可以随时调用Invoke或者BeginInvoke方法加入新的DO,在DispatcherOperation处理的时候也可能会调用BeginInvoke加入新的DO。
  DO被加入到Dispatcher的队列中去,那么这个队列又是何时被处理呢?Dispatcher在创建的时候,创建了一个隐藏的Window,在DO加入到队列后,Dispatcher会向自己的隐藏Window发送一个自定义的Window消息(DispatcherProcessQueue)。当收到这个消息后,会按照优先级和队列顺序取出第一个DO并执行:
      
  • 用户调用BeginInvoke。  
  • Dispatcher创建了一个DO,加入到DO队列中去,并向自己的隐藏窗口Post自定义消息(DispatcherProcessQueue)。  
  • 创建隐藏窗口时会Hook它的消息,当收到的消息为DispatcherProcessQueue时,按照优先级取出队列中的一个DO,并执行。
  每加入一个DO就会申请处理DO队列,在DO的优先级(DispatcherPriority)被改变的时候也会处理DO队列,DO在创建时声明了自己的优先级,这个优先级会影响到队列的处理顺序。
DispatcherTimer

  鉴于线程亲缘性,当需要创建Timer并访问UI对象时,多使用DispatcherTimer。DispatcherTimer的一个简单用法如下:
      
  1. [code]  var dispatcherTimer = new System.Windows.Threading.DispatcherTimer();   
复制代码
  1.   dispatcherTimer.Tick += new EventHandler(dispatcherTimer_Tick);
复制代码
  1.   dispatcherTimer.Interval = new TimeSpan(0,0,1);
复制代码
  1. <p>  dispatcherTimer.Start(); </p><p> </p>[code][code]
复制代码
[/code][/code][/code]      
  在DispatcherTimer的内部,Timer的Tick事件处理也被包装成了DispatcherOperation,并调用BeginInvoke加入到Dispatcher中去。当这个DO被执行后,如果DispatcherTimer的状态仍然为Enable,DispatcherTimer会继续调用BeginInvoke加入新的DO。关于Timer的时间处理,Dispatcher会向自己的隐藏窗口调用SetTimer并计算时间间隔,当然,因为DispatcherOperation有优先级,不能保状正好在时间间隔时执行这个DO,这个执行的时间会比预计时间偏后而不会超前。
UI线程和Dispatcher

  通常,WPF启动时具有两个线程,一个处理呈现(Render),另一个用于管理UI。关于Render线程,请参见前文。这个管理UI的线程通常被称为UI线程。在WPF中,所有UI对象的基类为DispatcherObject,WPF在对所有DispatcherObject属性操作前进行了线程亲缘性校验,只有在创建UI对象的线程中才可以访问该UI对象。
  前面提到,由于Dispatcher构造函数私有,一个线程最多只能有一个Dispatcher。对UI线程来说,Dispatcher的主要作用就是对任务项(DispatcherOperation)进行排队。对UI对象来说,DispatcherObject有一个Dispatcher属性,可以获得创建该UI对象线程的Dispatcher。这种设计通过Dispatcher统一了UI对象的操作,从使用上隔离了UI对象和线程间的关系。
多线程

  多线程操作简单分为两种:多工作线程和多UI线程,当然,也可以有多工作多UI线程,思路是一样的,省去不谈。
  程序启动时默认的主线程就是UI线程,它在调用Application.Run(也就是Dispatcher.Run)之后进入了一个GetMessage的循环中,对Window消息进行响应并构建执行一个个的DispatcherOperation。默认对UI对象的操作都是在这个主线程中,如果进行耗时很长的操作就会造成UI线程长时间不能继续响应Window消息,造成界面假死等一些的UI响应问题。对这种耗时较长的操作一般需要工作线程来帮忙,操作结束后再通过Dispatcher把结果Invoke到UI线程,如:
      
  1. [code]TextBlock textBlock = new TextBlock() { Text = "1" };
复制代码
  1. Thread thread = new Thread(new ThreadStart(() =>
复制代码
  1.     {
复制代码
  1.         //做一些耗时操作,这里用线程休眠10秒来模拟
复制代码
  1.          Thread.Sleep(TimeSpan.FromSeconds(10));
复制代码
  1.         textBlock.Dispatcher.Invoke(new Action(() =>
复制代码
  1.             {
复制代码
  1.                 textBlock.Text = "2";
复制代码
  1.             }));
复制代码
  1.     }));
复制代码
  1. thread.Start();
复制代码
[/code]      
  当然,除了新建工作线程,也可以使用BackgroundWorker或者线程池中线程来进行耗时操作,操作结束后需要调用UI对象Dispatcher的Invoke或者BeginInvoke方法来操作UI,否则会抛出InvalidOperationException来提示不可跨线程访问UI对象。
  这种多工作线程是很常见的,一般我们讨论的多线程大多指这种多工作线程单一UI线程,那么如何创建多UI线程的程序呢?
多UI线程

  在谈多UI线程之前,先说说多UI线程使用的场景:
  大多数情况下,我们是不需要多UI线程的,所谓多UI线程,就是指有两个或者两个以上的线程创建了UI对象。这种做法的好处是两个UI线程会分别进入各自的GetMessage循环,如果是需要多个监视实时数据的UI,或者说使用了DirectShow一些事件密集的程序,可以考虑新创建一个UI线程(GetMessage循环)来减轻单一消息泵的压力。当然,这样做的坏处也很多,不同UI线程中的UI对象互相访问是需要进行Invoke通信的,为了解决这个问题,WPF提供了VisualTarget来用于跨线程将一个对象树连接到另一个对象树,如:
      
  1. [code]    public class VisualHost : FrameworkElement
复制代码
  1.     {
复制代码
  1.         public Visual Child
复制代码
  1.         {
复制代码
  1.             get { return _child; }
复制代码
  1.             set
复制代码
  1.             {
复制代码
  1.                 if (_child != null)
复制代码
  1.                     RemoveVisualChild(_child);
复制代码
  1.                 _child = value;
复制代码
  1. [/code][code]                if (_child != null)
复制代码
  1.                     AddVisualChild(_child);
复制代码
  1.             }
复制代码
  1.         }
复制代码
  1. [/code][code]        protected override Visual GetVisualChild(int index)
复制代码
  1.         {
复制代码
  1.             if (_child != null && index == 0)
复制代码
  1.                 return _child;
复制代码
  1.             else
复制代码
  1.                 throw new ArgumentOutOfRangeException("index");
复制代码
  1.         }
复制代码
  1. [/code][code]        protected override int VisualChildrenCount
复制代码
  1.         {
复制代码
  1.             get { return _child != null ? 1 : 0; }
复制代码
  1.         }
复制代码
  1. [/code][code]        private Visual _child;
复制代码
  1.     }
复制代码
  1. [/code]
复制代码
     
  在另一个UI线程下的VisualTarget:
      
  1. [code]    Window win = new Window();
复制代码
  1.     win.Loaded += (s, ex) =>
复制代码
  1.         {
复制代码
  1.             <font color="#c0504d">VisualHost </font>vh = new <font color="#c0504d">VisualHost</font>();
复制代码
  1.             HostVisual hostVisual = new HostVisual();
复制代码
  1.             vh.Child = hostVisual;
复制代码
  1.             win.Content = vh;
复制代码
  1. [/code][code]            Thread thread = new Thread(new ThreadStart(() =>
复制代码
  1.             {
复制代码
  1.                 VisualTarget visualTarget = new VisualTarget(hostVisual);
复制代码
  1.                 DrawingVisual dv = new DrawingVisual();
复制代码
  1.                 using (var dc = dv.RenderOpen())
复制代码
  1.                 {
复制代码
  1.                     dc.DrawText(new FormattedText("UI from another UI thread",
复制代码
  1.                         System.Globalization.CultureInfo.GetCultureInfo("en-us"),
复制代码
  1.                         FlowDirection.LeftToRight,
复制代码
  1.                         new Typeface("Verdana"),
复制代码
  1.                         32,
复制代码
  1.                         Brushes.Black), new Point(10, 0));
复制代码
  1.                 }
复制代码
  1. [/code][code]                visualTarget.RootVisual = dv;
复制代码
  1.                 Dispatcher.Run();  //<font color="#008000">启动Dispatcher</font>
复制代码
  1.  
复制代码
  1.             }));
复制代码
  1.             thread.SetApartmentState(ApartmentState.STA);
复制代码
  1.             thread.IsBackground = true;
复制代码
  1.             thread.Start();
复制代码
  1.         };
复制代码
  1. <p>    win.Show();</p>[code] 
复制代码
 

[/code][/code]      
  当然,这个多UI线程只是为了更好的捋清线程间的关系,实际使用中的例子并不是很常见。
总结

  Dispatcher是WPF中很重要的一个概念,WPF所有UI对象都是运行在Dispatcher上的。Dispatcher的一些设计思路包括Invoke和BeginInvoke等从WinForm时代就是一直存在的,只是使用了Dispatcher来封装这些线程级的操作。剩余一些概念包括跨线程通信的Freezable以及详细的优先级顺序DispatcherPriority本文都没有细谈,以后的文章中会逐步介绍,希望大家多多支持,谢谢。
 
作者:周永恒   
出处:http://www.cnblogs.com/Zhouyongh  
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册