WPF学习与分享之二:DispatcherObject与WPF线程模型(下)

Posted on 2008-08-06 23:47  Nullnoid  阅读(7879)  评论(8编辑  收藏  举报

上一篇中我们分析了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消息队列的特点和限制:

  1. 任劳任怨的从消息队列中拿出消息,并调用对应的窗口处理函数
  2. 当消息Post进队列后就无法控制了
  3. 消息的处理支持有限的优先级,但都由OS控制,用户代码不能控制和改变优先级
  4. Application.DoEvents只有在处理完消息队列中所有消息后才返回,当然Win32没有这个限制,因为Message Pump就是用户创建的
  5. 可以添加钩子,偷偷的干些坏事

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也可以用于其他系统中。我们甚至可以利用这些在我们的系统中。