千家信息网

WPF线程模型和Dispatcher怎么用

发表于:2026-11-01 作者:千家信息网编辑
千家信息网最后更新 2026年11月01日,这篇文章将为大家详细讲解有关WPF线程模型和Dispatcher怎么用,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。WPF线程模型是从WPF的两个线程:一个用于处理
千家信息网最后更新 2026年11月01日WPF线程模型和Dispatcher怎么用

这篇文章将为大家详细讲解有关WPF线程模型和Dispatcher怎么用,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。

WPF线程模型是从WPF的两个线程:一个用于处理呈现和一个用于管理UI开始。并展开同时讨论Dispatcher的相关对象。

开始着手写这个WPF系列,这里的一站式,就是力争在每一个点上能把它讲透,当然,做不到那么尽善尽美,如果有不对的地方也欢迎朋友们指正,我会逐步补充,争取把这个系列写好。

通常,WPF应用程序从两个线程开始:一个用于处理呈现,一个用于管理UI。呈现线程有效地隐藏在后台运行,而 UI 线程则接收输入、处理事件、绘制屏幕以及运行应用程序代码。

UI 线程对一个名为Dispatcher的对象内的工作项进行排队。Dispatcher基于优先级选择工作项,并运行每一个工作项,直到完成。每个 UI 线程都必须至少有一个Dispatcher,并且每个Dispatcher都只能在一个线程中执行工作项。

这两段是MSDN上关于WPF线程模型的描述。主要介绍了两个概念:一,WPF中线程一分为二,一个用于呈现(Render),一个用于管理UI;二,在UI线程中,使用了一个名为Dispatcher的类帮助UI线程处理任务。

那么这个线程模型和Dispatcher到底是怎样的呢,它又有什么特点,有什么优缺点呢?在正式分析线程模型和Dispatcher之前,我先找一个插入点,希望这个插入点能为朋友们所理解。

作为一个Presentation的基架,WPF的使命就是要编写图形化的操作界面。而在Windows操作系统上,图形化界面是建立在消息机制这个基础上的,那么创建一个窗口,要经历哪些步骤呢?

1. 创建窗口类。 WNDCLASSEX wcex; RegisterClassEx(&wcex);

2. 创建窗口。CreateWindow(…); ShowWindow(…); UpdateWindow(…);

3. 建立消息泵。  

while (GetMessage(&msg, NULL, 0, 0))   {   TranslateMessage(&msg);   DispatchMessage(&msg);   }

打个比方,我们在一个自动化的厂房里生产设备。基于正规,我们会首先定义好该设备的模板,这就是创建窗口类,这里"类"更多表示类别的意思。模板定义完毕,我们可以正式生产设备了,这就是创建窗口,这个CreateWindow的时候会通过字符串来匹配到我们定义的模板(窗口类)。创建成功后,我们要让设备动起来,就要像人一样,体内一定要有类似于血液的流传机制,把命令传达到设备的各个部分,这就是消息泵,这个泵就像我们的心脏一样,源源不断的通过GetMessage并Dispatch来分发血液(消息)。既然我们通过消息来对设备下达指令,那么就要有消息队列来存储消息,在Windows中,线程为基本的调度单位,这个消息队列就在线程上,当循环使用GetMessage时,就是在当前线程的消息队列中任劳任怨的取出消息,然后分发到对应的窗口中去。

那么具体到WPF,它又是一个怎么样的情况,如何和老的技术兼容,又有什么新的突破呢?

WPF引入了Dispatcher的概念,这个Dispatcher的主要功能类似于Win32中的消息队列,在它的内部函数,仍然调用了传统的创建窗口类,创建窗口,建立消息泵等操作。Dispatcher本身是一个单例模式,构造函数私有,暴露了一个静态的CurrentDispatcher方法用于获得当前线程的Dispatcher。对于线程来说,它对Dispatcher是一无所知的,Dispatcher内部维护了一个静态的List_dispatchers, 每当使用CurrentDispatcher方法时,它会在这个_dispatchers中遍历,如果没有找到,则创建一个新的Dispatcher对象,加入到_dispatchers中去。Dispatcher内部维护了一个Thread的属性,创建Dispatcher时会把当前线程赋值给这个Thread的属性,下次遍历查找的时候就使用这个字段来匹配是否在_dispatchers中已经保存了当前线程的Dispatcher。

那么这个创建窗口,建立消息泵又是什么时候被调用的呢?在Dispatcher内部,维护了一个HwndWrapper的字段,在Dispatcher的构造函数中,调用了HwndWrapper的构造函数,这个创建窗口类,创建窗口就是在这个函数中被调用的。这里实际的类是MessageOnlyHwndWrapper,这个Message-Only,是Windows编程中常用的伎俩,创建一个隐藏窗口,仅仅用来派发消息。那么循环读取消息的消息泵又是什么时候建立起来的呢?

Dispatcher对外提供了一个静态的Run函数,顾名思义,就是启动Dispatcher,在函数内部,调用了PushFrame函数,在这个函数中,可以找到熟悉的GetMessage, TranslateAndDispatchMessage。那么这个PushFrame是怎么回事,Frame这个概念又是如何而来的呢?

这个就是WPF线程模型引入的一个新的概念,嵌套消息泵,就是在一个While(GetMessage(...))内部又启动了一个While(GetMessage(...))。每调用一次PushFrame,就会启动一个新的嵌套的消息泵。每调用一次GetMessage,就在线程的消息队列中取出一个消息,直至取出WM_QUIT的时候GetMessage才返回False。这个GetMessage函数Windows内部进行了处理,当消息队列为空时,挂起执行线程,避免死循环的发生。关于嵌套消息泵的优缺点,我们稍后再讲,先来看看Dispatcher是如何处理任务的:

Windows中定义了很多Message,以WM_开头,在注册窗口类的时候需要设置窗口过程函数,GetMessage取得的消息再分发到窗口过程函数中,整个过程为:

这个图来自于侯捷的经典书籍《深入浅出MFC》,1.首先创建Window并指定窗口的过程函数WndProc。2.当窗口创建时一个WM_CREATE被放入到消息队列中,3.消息泵通过GetMessage取得该消息后分发到窗口,窗口过程函数处理这个WM_CREATE消息…

那么WPF线程模型的Dispatcher在这个过程中扮演了什么角色呢?前面的1,2,3仍然如此,当窗口过程函数接收到消息时,它需要根据消息的类别把Windows消息转译成内部的RoutedEvent或者调用布局函数等来处理。前面提到了Dispatcher主要功能类似于Win32中的消息队列,这个队列中存放的对象是DispatcherOperation,这个DispatcherOperation,顾名思义,就是把每一个执行项封装成一个对象,类似:

这个队列的类型为PriorityQueue,是一个含有优先级的队列。WPF定义了这个优先级DispatcherPriority,有

当对这个PriorityQueue调用DeQueue时,就会取出优先级***的任务。那么这个队列中的任务是什么时候被添加的,又是什么时候被取出执行的呢?

Dispatcher暴露了两个方法,Invoke和BeginInvoke,这两个方法还有多个不同参数的重载。其中Invoke内部还是调用了BeginInvoke,一个典型的BeginInvoke参数如下:

public DispatcherOperation BeginInvoke(Delegate method, DispatcherPriority priority, params object[] args);

在这个BeginInvoke内部,会把执行函数method与参数args封装成DispatcherOperation,并按priority加入到PriorityQueue中,这个返回值就是内部创建的DispatcherOperation。也就是说每调用一次Invoke和BeginInvoke,就向Dispatcher中加入了一个任务,那么这个任务什么时候被执行呢?

DispatcherPriority定义了很多优先级,WPF将这些优先级主要分成两类。前台优先级和后台优先级,其中前台包括Loaded~Send,后台包括Background~Input。剩下的几个优先级除了Invalid和Inactive都属于空闲优先级,处理顺序同后台优先级。这个前台优先级和后台优先级的分界线是以Input来区分的,这里的Input指的是键盘输入和鼠标移动、点击等等。ProrityQueue的来源有:

当然,这里Hwnd级别Hook到的消息最终也是调用Dispatcher的Invoke/BeginInvoke方法加入到Dispatcher的队列中去的。当处理这个PriorityQueue时,会首先取得队列中的***优先级,如果它属于前台优先级,执行。如果属于后台优先级,那么它要去扫描线程的消息队列,看看其中是由有类似WM_MOUSEMOVE之类的Input消息。如果没有,执行。如果存在,则放弃执行,并启动一个Timer,当Timer唤起时继续判断是否可以执行。

那么处理PriorityQueue的时机呢?当你调用BeginInvoke,向队列中加入执行项的同时,也会调用处理Queue的判断。判断逻辑和上面类似,队列中***优先级是前台优先级,向隐藏窗口PostMessage,这个消息是Disptcher使用RegisterWinodwMessage注册的自定义消息。然后在GetMessage的时候如果取出这个自定义消息,则处理PriorityQueue。如果是后台优先级,扫描线程消息队列的Input消息,决定是否启动Timer还是PostMessage。

举个例子,在后台线程中向UI线程中使用Invoke来发送请求,经历的过程为:

1. 调用Invoke,对传入的参数DispatcherPriority进行判断,如果是Send,这是个特殊的优先级,直接切换线程上下文,执行任务并返回。如果是其他的优先级,调用BeginInvoke。

2. 在BeginInvoke中,把传入的Delegate和参数封装成DispatcherOperation,加入到PriorityQueue中。

3. 调用队列处理的请求函数,希望处理PriorityQueue。

4. 如果队列中***优先级属于前台优先级,调用PostMessage向隐藏窗口发送自定义消息。后台处理这里省略不表。

5. 在GetMessage中取得消息并分发到隐藏窗口,这里使用的是常见的SubWindow(注释一),消息通过Hook发送到Dispatcher的WndProcHook函数进行处理。

6. 在WndProcHook中,如果接收到的Window消息是Dispatcher自定义的消息,则真正处理PriorityQueue。

7. 处理PriorityQueue,从中取出一个任务,进行前后台优先级判断,决定是否处理还是启动Timer稍后处理。

回过头来,说一说嵌套的消息循环,这个要从模态对话框说起,一个通常的模态对话框场景如下:

SomeCodeA();

bool? result = dlg.ShowDialog();

SomeCodeB();

代码运行在UI线程中,当执行到dlg.ShowDialog时,启动模态对话框,等待用户点击Yes/No或者关闭对话框,对话框关闭后程序继续执行SomeCodeB代码。那么程序要在SomeCodeB处等待ShowDialog返回后才继续执行。当然你可以使用WaitHandle来同步,不过这个需要挂起当前(UI)线程,如果主窗口中有动画等UI动作,那么会停止得不到响应。这里WPF使用的是PushFrame,就是在ShowDialog内部又建立起了一个消息泵。While(GetMessage(…))。一方面,可以确保UI线程中的消息可以被处理;另一方面,因为是While循环,在对话框关闭时返回,可以确保SomeCodeB的执行顺序。

那么是不是这个嵌套的消息循环真的如此***呢?当然不是,它打开了一扇门的同时,也打开了另一扇门。一个情景,当收到WM_SIZE消息的时候,Layout系统开始处理,如果在这个处理过程中,又启动了PushFrame,那么嵌套的消息泵就会继续从消息队列中取出消息,如果下一个消息也是WM_SIZE,那么进行处理。假设这个消息处理结束后这个嵌套的消息泵返回了,那么***个WM_SIZE得以继续处理。这样就发生了错误,本来12的处理顺序变成了121。当然这种情况不仅仅发生在Layout中,所以WPF在Dispatcher中加入了一个DisableProcessing函数,在Layout等关键过程中调用了这个函数,在这个过程中停止pump消息和禁止PushFrame。

在WPF中,所有的WPF对象都派生自DispatcherObject,DispatcherObject暴露了Dispatcher属性用来取得创建对象线程对应的Dispatcher。鉴于线程亲缘性,DispatcherObject对象只能被创建它的线程所访问,其他线程修改DispatcherObject需要取得对应的Dispatcher,调用Invoke或者BeginInvoke来投入任务。一个UI线程至少有一个Dispatcher来建立消息泵处理任务,一个Dispatcher只能对应一个UI线程。那么UI线程和Render线程又如何呢?

开篇提到,WPF线程模型一分为二,一个是UI线程,一个是Render线程。这两个被设计成分离的关系,通过Channel(event)来进行通信。两者之间的数量关系是一个WPF进程只能有一个Render线程,旦可以有大于等于一个的UI线程。通常情况下是一个UI线程,也就是一个Dispatcher,那么什么情况下需要建立多个呢?

大多情况下是不需要的,少数情况下,比如MediaElement,或者Host其他ActiveX控件,我们期望在其他线程中创建,以提高性能。可以新建线程,在新线程中创建控件,并调用Dispatcher.Run启动Dispatcher。这样主Window和新控件就处在不同线程中,两者间的通信可以使用VisualTarget连接视觉树或者使用D3DImage拷贝新控件到主Window中显示。

开篇有益,WPF没有什么全新的技术,但提出了很多新的概念。

注释一: SubWindow,子窗口子类化。通常情况,所有同类别Window会共用同一个消息处理函数WndProc,子Window可以调用SetWindowLong用SubWndProc替换WndProc,这个通常称为Sub-Window。

关于"WPF线程模型和Dispatcher怎么用"这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,使各位可以学到更多知识,如果觉得文章不错,请把它分享出去让更多的人看到。

消息 线程 处理 优先级 函数 队列 就是 时候 过程 任务 模型 后台 对象 情况 两个 前台 对话框 设备 对话 循环 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 上海睿民互联网科技有限公司 中国互联网科技工作委员会毛军 科技企业和互联网企业 浩特互联网科技 远拓互联网科技 互联网养老 桔豆科技 联恩商钥互联网科技有限公司 农村开互联网科技讲堂 栖莱科技互联网股份有限公司 互联网公司科技股 广西互联网科技孵化中心 乌苏新华互联网科技做什么的 河南住房之家互联网科技 内蒙古九一互联网科技 互联网科技和法律行业 上海京穆互联网科技有限公司 从那以后互联网科技有限公司 上海同邻互联网科技有限公司规模 西安有哪些互联网高科技企业 网互联网络科技 互联网科技官网 中国互联网科技股 科技互联网政务服务 为想互联网科技 成都狮子座互联网科技 互联网科技大会案例 以后互联网后是啥科技 工业互联网展湾科技股份有限公司 元气玛特互联网科技公司是外包吗 伍佰万互联网科技是干嘛的
0