深耕 WPF 开发的各位程序员大大们一定避不开使用 Dispatcher。跨线程访问 UI 当然免不了用到它,将某个任务延迟到当前任务之后执行也会用到它。Dispatcher.Invoke、Dispatcher.BeginInvoke 是过去大家经常使用的方法,而 .Net Framework 4.5 中微软为我们带来了 Dispatcher.InvokeAsync 方法,它和前面两个有何不同?

阅读本文将更深入地了解 Dispatcher 的工作机制。


本文是深入了解 WPF Dispatcher 的工作原理系列文章的一部分:

  1. Invoke/InvokeAsync 部分(本文)
  2. PushFrame 部分

回顾老旧的 BeginInvoke,看看新的 InvokeAsync

微软自 .Net Framework 3.0 为我们引入了 Dispatcher 之后,BeginInvoke 方法就已存在。不过,看这名字的 Begin 前缀,有没有一种年代感?没错!这是微软在 .Net Framework 1.1 时代就推出的 Begin/End 异步编程模型(APM,Asynchronous Programming Model)。虽说 Dispatcher.BeginInvoke 并不完全按照 APM 模型来实现(毕竟没有对应的 End,也没有返回 IAsyncResult),但这个类型毕竟也是做线程相关的事情,而且这个方法的签名明显还带着那个年代的影子。不止名字上带着 Begin 表示异步的执行,而且参数列表中还存在着 Delegateobject 这样古老的类型。要知道,现代化的方法可是 Action/Func 加泛型啊!

大家应该还对 .Net Framework 4.5 带给我们的重磅更新——async/await 异步模式感到兴奋,因为它让我们的异步代码变得跟同步代码一样写了。这是微软新推荐的异步编程模式,叫做 TAP(Task-based Asynchronous Pattern)。既然异步编程模式都换了,同为线程服务的 Dispatcher.BeginInvoke 怎能不改呢?于是,微软真的改了,就是从 .Net Framework 4.5 版本开始。

它叫做——Dispatcher.InvokeAsync


BeginInvoke 和 InvokeAsync 有什么不同?

这个还真得扒开微软的源码看一看呢!

[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
public DispatcherOperation BeginInvoke(DispatcherPriority priority, Delegate method); [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
public DispatcherOperation BeginInvoke(DispatcherPriority priority, Delegate method, object arg); [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
public DispatcherOperation BeginInvoke(DispatcherPriority priority, Delegate method, object arg, params object[] args); public DispatcherOperation BeginInvoke(Delegate method, params object[] args); public DispatcherOperation BeginInvoke(Delegate method, DispatcherPriority priority, params object[] args);

一共五个重载,前面三个都被微软做了标记,让你在智能感知列表中看不见。(这里吐槽一下 ReSharper,明明微软已经不让显示了嘛,干嘛还把人家显示出来……)后面两个暂时还看得见,但那又如何?!根本没啥区别好吗!!!

为什么会像上面那样吐槽,是因为我发现这五个不同的重载里面其实都调用了同一个内部方法:

[SecuritySafeCritical]
private DispatcherOperation LegacyBeginInvokeImpl(DispatcherPriority priority, Delegate method, object args, int numArgs)
{
ValidatePriority(priority, "priority");
if(method == null)
{
throw new ArgumentNullException("method");
}
DispatcherOperation operation = new DispatcherOperation(this, method, priority, args, numArgs);
InvokeAsyncImpl(operation, CancellationToken.None);
return operation;
}

这里让我忍不住吐槽的是两点:

  1. Legacy 是个什么鬼!词典上说这是“遗产,老化的”意思啊!很明显这是近乎被微软遗弃的代码啊!
  2. 既然这五个重载都用了被遗弃的方法,为什么只有前面三个看不见,后面两个看得见啊!还有,微软你干嘛不标记为 [Obsolete] 呢!

好,吐槽结束。我们再来看看 InvokeAsync 方法。

public DispatcherOperation InvokeAsync(Action callback);

public DispatcherOperation InvokeAsync(Action callback, DispatcherPriority priority);

[SecuritySafeCritical]
public DispatcherOperation InvokeAsync(Action callback, DispatcherPriority priority, CancellationToken cancellationToken); public DispatcherOperation<TResult> InvokeAsync<TResult>(Func<TResult> callback); public DispatcherOperation<TResult> InvokeAsync<TResult>(Func<TResult> callback, DispatcherPriority priority); [SecuritySafeCritical]
public DispatcherOperation<TResult> InvokeAsync<TResult>(Func<TResult> callback, DispatcherPriority priority, CancellationToken cancellationToken);

看吧,这才像微软新的 TAP 异步模式的代码啊。

不带 CancellationToken 的四个重载会汇聚到带 CancellationToken 的两个重载中,这两个重载代码除了泛型返回值以外几乎一模一样。所以我们拿第三个当研究对象看看:

[SecuritySafeCritical]
public DispatcherOperation InvokeAsync(Action callback, DispatcherPriority priority, CancellationToken cancellationToken)
{
if(callback == null)
{
throw new ArgumentNullException("callback");
}
ValidatePriority(priority, "priority");
DispatcherOperation operation = new DispatcherOperation(this, priority, callback);
InvokeAsyncImpl(operation, cancellationToken);
return operation;
}

你发现了什么?这与那个被遗弃的 LegacyBeginInvokeImpl 长得非常像。不,就是一模一样!你总不能说参数名称不同也要算吧……甚至……返回值类型也是一样的。

既然这样,我们总算是明白微软到底在做些什么了。其实微软在 .Net Framework 4.5 中已经把 BeginInvoke 的实现改造成了 TAP 异步模式,但方法名字和老旧的参数列表却始终是微软的一块心病,于是痛下决心新增了 6 个更加现代的方法免得产生兼容性问题。不过由于里面的实现一模一样,并没有额外带来什么 BUG,所以微软也不好意思标记为 [Obsolete] 已过时了。

既然两个方法一样,后文我也就没必要两个都说了,一切以新款的 InvokeAsync 为主。


InvokeAsync 的实现原理

前面一节几乎告诉我们,InvokeAsync 的关键就在 InvokeAsyncImpl 方法中。

  1. 用一个 DispatcerOperation 把我们传入的 Action/Func 包装起来。这样,我们传入的任务和优先级将在一起处理。
  2. DispatcherOperation 加入到一个 PriorityQueue<DispatcherOperation> 类型的队列中。这个队列内部实现是一个 SortedList,于是每次入队之后,出队的时候一定是按照优先级出队的。
  3. 调用 RequestProcessing,直至最后向某个隐藏窗口发送了一条消息。
  4. 那个隐藏窗口接收到了这条消息,然后从 PriorityQueue<DispatcherOperation> 队列中取出一条任务执行(真实情况复杂一点,后面会谈到)。

上面第 3 点的消息是这样发的:

UnsafeNativeMethods.TryPostMessage(new HandleRef(this, _window.Value.Handle), _msgProcessQueue, IntPtr.Zero, IntPtr.Zero);

等等,这句代码里面的 _window 是哪儿来的?为什么凭空出现了一个可以用来发送消息的窗口?于是,在 Dispatcher 构造函数中发现了这个窗口。这并不是我们平时所熟知的那个 Window 类,而是一个用于发送和接收 Dispatcher 调度器调度任务消息的 Win32 隐藏窗口。不信它是一个窗口?请进入 MessageOnlyHwndWrapper 类看,它的基类 HwndWrapper 中直接使用了方法 UnsafeNativeMethods.CreateWindowEx 创建了这个窗口,然后拿到了它的句柄 Handle

既然会向窗口发消息,自然而然可以 Hook 它的消息处理函数,就像下面这样:

// Create the message-only window we use to receive messages
// that tell us to process the queue.
MessageOnlyHwndWrapper window = new MessageOnlyHwndWrapper();
_window = new SecurityCriticalData<MessageOnlyHwndWrapper>( window ); _hook = new HwndWrapperHook(WndProcHook);
_window.Value.AddHook(_hook);

而这里处理的消息类型只有三种:

  1. 关掉这个隐藏窗口;
  2. 处理 Dispatcher 调度的任务(这个消息是在 Dispatcher 的静态构造函数中注册的);
  3. 定时器。

前面两个不难理解,但是为什么这里与定时器有关?!

继续调查,我们发现微软在 Dispatcher 中把所有不同种类的优先级分成了三个大类:

  1. 前台优先级(对应 DispatcherPriority.LoadedDispatcherPriority.Send,也就是数字 6~10)
  2. 后台优先级(对应 DispatcherPriority.BackgroundDispatcherPriority.Input,也就是数字 4~5)
  3. 空闲优先级(对应 DispatcherPriority.SystemIdleDispatcherPriority.ApplicationIdle,也就是数字 1~3)

在这里微软又开始逗我们玩了……因为在他的处理中,后面两个是完全相同的!所以严格意义上只分了两种——前台优先级和非前台优先级。而区分他们的一个分界点就是——用户的输入。

如果有用户的输入发生,那么会开启一个定时器,在定时器时间到达之前,所有的后台优先级任务都不会去执行。但前台优先级任务不受用户输入的影响。在这样的设定下,用户的输入不会随随便便被饿死,WPF 程序也就不会从输入层面开始卡顿了。

研究到这里,似乎 InvokeAsync 的执行原理差不多都清楚了。但是不要忘了这可是 TAP 异步模式的一项实践啊,这方法是要支持 await 并附带返回值的。

但这里就没有更多底层的内容了。我们注意到 InvokeAsync 的返回值是 DispatcherOperation 类型的,而这就是 InvokeAsync 方法中我们前面看到的代码中直接 new 出来的。DispatcherOperation 中有一个 Invoke 方法,这是无返回值的,但是它执行完后会置 TaskResult 值。另外 DispatcherOperation 实现了 GetAwaiter 方法,于是就可以使用 await。对于如何自己实现一个可以 await 的类,我可能会专门写一篇文章,但如果你现在就希望了解,可以阅读:How to write a custom awaiter – Lucian’s VBlog

而被我们遗弃的 BeginInvoke,由于内部调用了同一个函数,所以实现原理是完全一样的。而且,这么古老的函数也允许 await

Invoke 的实现原理

也许你会觉得奇怪。我们连“异步”的 InvokeAsync 的实现原理都了解了,同步的 Invoke 还有何难!

如果你这么认为,你一定忽略了一个很重要的问题——死锁!

如果是另一个线程调用到此线程的 Invoke,那么同步等待一下当然不会有问题。但是如果调用线程就是此线程本身呢?如果依然采用“同步等待”的方式,那么 UI 线程就会因为 Invoke 的调用而阻塞,然而 Invoke 中传入的 Action 是插入到 UI 线程执行的,如果 UI 线程正在等待 Invoke,还怎么插入得进去?!

所以,它一定有另外一套实现方式!而微软为这套实现方式做了两条路径:

  1. 如果是 10 的最高优先级,则直接调用 Invoke 里传入的任务;
  2. 如果是其他,则调用 DispatcherOperationWait 方法进行等待。

等等,这不还是 Wait 吗!然而进去 Wait 方法查看,你会发现,根本不是!

public DispatcherOperationStatus Wait(TimeSpan timeout)
{
// 省略一些前面的代码。 // We are the dispatching thread for this operation, so
// we can't block. We will push a frame instead.
DispatcherOperationFrame frame = new DispatcherOperationFrame(this, timeout);
Dispatcher.PushFrame(frame); // 省略一些后面的代码。 return _status;
}

它用了 Dispatcher.PushFrame。这样保证了在不阻塞线程的情况下进行“等待”。至于如何做到“不阻塞地等待”,请参阅本系列的第二篇文章 深入了解 WPF Dispatcher 的工作原理(PushFrame 部分)

总结

  1. 进入了 .Net Framework 4.5 及以上的开发者们,建议使用 InvokeAsync 代替 BeginInvoke
  2. Dispatcher 通过创建一个隐藏的消息窗口来让一个个 Invoke 到此线程的任务按照优先级执行;
  3. Invoke 使用 PushFrame 做到了不阻塞 UI 线程的等待。

参考资料

深入了解 WPF Dispatcher 的工作原理(Invoke/InvokeAsync 部分)的更多相关文章

  1. 深入了解 WPF Dispatcher 的工作原理(PushFrame 部分)

    在上一篇文章 深入了解 WPF Dispatcher 的工作原理(Invoke/InvokeAsync 部分) 中我们发现 Dispatcher.Invoke 方法内部是靠 Dispatcher.Pu ...

  2. 菜鸟学Struts2——Struts工作原理

    在完成Struts2的HelloWorld后,对Struts2的工作原理进行学习.Struts2框架可以按照模块来划分为Servlet Filters,Struts核心模块,拦截器和用户实现部分,其中 ...

  3. Struts2(二):工作原理

    struts可查看源码:https://github.com/apache/struts 在学习struts2之前,我先看了一些比较早版本对struts2的工作原理相关的介绍,顺便抄写过来,用来帮助自 ...

  4. WPF Dispatcher 一次小重构

    几个月之前因为项目需要,需要实现一个类似于WPF Dispatcher类的类,来实现一些线程的调度.之前因为一直做Asp.Net,根本没有钻到这个层次去,做的过程中,诸多不顺,重构了四五次,终于实现, ...

  5. struts2的工作原理

    在学习struts2就必须的了解一下它的工作原理: 首先来看一下这张图 这张工作原理图是官方提供的: 一个请求在Struts2框架中的处理大概分为以下几个步骤 1.客户端初始化一个指向Servlet容 ...

  6. 自己打断点走的struts流程&拦截器工作原理

    ①. 请求发送给 StrutsPrepareAndExecuteFilter ②. StrutsPrepareAndExecuteFilter 判定该请求是否是一个 Struts2 请 求(Actio ...

  7. Struts2 的工作原理

    Struts2 的工作原理: 1)client向server发出一个http请求.webserver对请求进行解析,假设在StrutsPrepareAndExecuteFilter的请求映射路径(在w ...

  8. WPF Dispatcher 频繁调度导致的性能问题

    问题 WPF Dispatcher 提供了UI线程之外的线程异步操作(请求)UI变化.一次Invoke/BeginInvoke调用产生一个DispatcherOperation,将挂在调度队列中,按照 ...

  9. 一篇笔记整理JVM工作原理

    首先要了解的 >>数据类型 Java虚拟机中,数据类型可以分为两类:基本类型和引用类型. 基本类型的变量保存原始值,即:他代表的值就是数值本身:而引用类型的变量保存引用值.“引用值”代表了 ...

随机推荐

  1. 自定义ajax

    // 动态添加script获取里面的数据,,可实现跨域,不跨的当然也可以 getFile:function(params){ try{ //创建script标签 var cbName=params.c ...

  2. [mybatis]Mapper XML 文件——statementType

    statementType:STATEMENT,PREPARED 或 CALLABLE(存储过程) 的一个.这会让 MyBatis 分别使用 Statement,PreparedStatement 或 ...

  3. Linux命令详解-mv

    mv命令是move的缩写,可以用来移动文件或者将文件改名(move (rename) files),是Linux系统下常用的命令,经常用来备份文件或者目录. 1.命令格式: mv [选项] 源文件或目 ...

  4. ngnix配置自解

    全局配置 user [user] [group]; #只有被设置为用户或用户组的成员才有nginx的启动权限.(#user nobody nobody <=> user nobody no ...

  5. 普通用户启动redis

    重庆231 Redis 服务器 redis用户启动 复制 /etc/init.d/redis 启动脚本到 /redisdata/redis3.2下面,修改内容 [root@localhost ~]# ...

  6. VUE基本安装

    // 安装脚手架 cnpm install -g vue-cli // 初始化项目 vue init webpack 项目名称 // 安装依赖 cd 项目名称 cnpm i // 安装stylus c ...

  7. zoj1654

    题解: 对于每一联通的x,y 检点 然后交叉的连边 然后二分图 代码: #include<cstdio> #include<cstring> #include<cmath ...

  8. jQuery实现点击式选项卡

    参考:jQuery权威指南jQuery初步jQuery选择器jQuery操作domjQuery操作dom事件jQuery插件jQuery操作AjaxjQuery动画与特效jQuery实现导航栏jQue ...

  9. Map以及其子类

    package com.Map; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; i ...

  10. 2018-2019 2 20165210 《网络对抗技术》Exp5 MSF基础

    2018-2019 2 20165210 <网络对抗技术>Exp5 MSF基础 实验内容: 一个主动攻击实践,如ms08_067(成功); 一个针对浏览器的攻击,如ms10_046(成功) ...