理解Windows窗体和WPF中的跨线程调用

你曾开发过Windows窗体程序,可能会注意到有时事件处理程序将抛出InvalidOperationException异常,信息为“ 线程调用非法:在非创建控件的线程上访问该控件”。这种Windows窗体应用程序中 线程调用时的一个最为奇怪的行为就是,有些时候它没什么问题,可有些时候却会出现问题。在 WPF(Windows Presentation Foundation)中,这个行为有所改变。 WPF线程调用将永远不会成功。不管怎样,至少这能让你在开发过程中更容易地找到问题的所在。

在Windows窗体中,解决方法是首先检查Control.InvokeRequired属性,若Control. InvokeRequired属性为true,那么调用ControlInvoke()。在WPF中,可以使用System.Windows.Threading.Dispatcher中的Invoke()和BeginInvoke()方法。这两种情况中都发生了很多事情,你也同样有别的选择。这两个API为你做了很多事情,不过在某些情况下仍有可能会失败。因为这些方法将用来处理线程调用,因此若是没有正确使用(甚至是正确使用但没有完全理解其行为)的话,也有可能会导致竞争条件的出现。

无论是Windows窗体还是WPF,问题的成因都很简单:Windows控件使用的是组件对象模型(Component Object Model,COM)单线程单元(Single-threaded Apartment,STA)模型,因为其底层的控件是单元线程(apartment-threaded)的。此外,很多控件都用消息泵(message pump)来完成操作。因此,这种模型就需要所有调用该控件的方法都和创建该控件的方法位于同一个线程上。Invoke、BeginInvoke和EndInvoke调度方法都需要在正确的线程上调用。两种模型的底层代码非常相似,因此这里将以Windows窗体的API为例。不过当调用方法有所区别时,我将同时给出两个版本。其具体的做法非常复杂,但仍需要深入了解。

首先,我们来看一段简单的泛型代码,能够让你在遇到此种情况时得到一定的简化。匿名委托让仅在一处使用的小方法更加易于编写。不过,匿名委托却并不能与接受System.Delegate类型的方法(例如Control.Invoke)配合使用。因此,你需要首先定义一个非抽象的委托类型,随后在使用Control. Invoke时传入。

private void OnTick(object sender, EventArgs e)

{

    Action action = () =>

        toolStripStatusLabel1.Text =

            DateTime.Now.ToLongTimeString();

    if (this.InvokeRequired)

        this.Invoke(action);

    else

        action();

}

C# 3.0大大简化了上述代码。System.Core.Action委托定义了一类专门的委托类型,用来表示不接受任何参数并返回void的方法。lambda表达式也能够更加简单地定义方法体。但若你仍旧需要支持C# 2.0,那么需要编写如下的代码。

delegate void Invoker();

private void OnTick20(object sender, EventArgs e)

{

    Action action = delegate()

    {

        toolStripStatusLabel1.Text =

            DateTime.Now.ToLongTimeString();

    };

    if (this.InvokeRequired)

        this.Invoke(action);

    else

        action();

}

WPF中,则需要使用控件上的System.Threading.Dispatcher对象来执行封送操作。

private void UpdateTime()

{

    Action action = () => textBlock1.Text =

        DateTime.Now.ToString();

    if (System.Threading.Thread.CurrentThread !=

        textBlock1.Dispatcher.Thread)

    {

        textBlock1.Dispatcher.Invoke

            (System.Windows.Threading.DispatcherPriority.Normal,

            action);

    }

    else

    {

        action();

    }

}

这种做法让事件处理程序的实际逻辑变得更加模糊,让代码难以阅读和维护。这种做法还需要引入一个委托定义,仅仅用来满足方法的签名。

使用一小段泛型代码即可改善这种情况。下面的这个ControlExtensions静态类所包含的泛型方法适用于调用不超过两个参数的委托。再添加一些重载即可支持更多的参数。此外,其中的方法还可使用委托定义来调用目标方法,既可以直接调用,也可以通过Control.Invoke的封送。

public static class ControlExtensions

{

    public static void InvokeIfNeeded(this Control ctl,

        Action doit)

    {

        if (ctl.InvokeRequired)

            ctl.Invoke(doit);

        else

            doit();

    }

    public static void InvokeIfNeeded<T>(this Control ctl,

        Action<T> doit, T args)

    {

        if (ctl.InvokeRequired)

            ctl.Invoke(doit, args);

        else

            doit(args);

    }

}

在多线程环境中使用InvokeIfNeeded能够很大程度上简化事件处理程序的代码。

private void OnTick(object sender, EventArgs e)

{

    this.InvokeIfNeeded(() => toolStripStatusLabel1.Text =

        DateTime.Now.ToLongTimeString());

}

对于WPF控件,也可以创建出一系列类似的扩展。

public static class WPFControlExtensions

{

    public static void InvokeIfNeeded(

        this System.Windows.Threading.DispatcherObject ctl,

        Action doit,

        System.Windows.Threading.DispatcherPriority priority)

    {

        if (System.Threading.Thread.CurrentThread !=

            ctl.Dispatcher.Thread)

        {

            ctl.Dispatcher.Invoke(priority,

                doit);

        }

        else

        {

            doit();

        }

    }

    public static void InvokeIfNeeded<T>(

        this System.Windows.Threading.DispatcherObject ctl,

        Action<T> doit,

        T args,

        System.Windows.Threading.DispatcherPriority priority)

    {

        if (System.Threading.Thread.CurrentThread !=

            ctl.Dispatcher.Thread)

        {

            ctl.Dispatcher.Invoke(priority,

                doit, args);

        }

        else

        {

            doit(args);

        }

    }

}

WPF版本没有检查InvokeRequired,而是检查了当前线程的标识,并于将要进行控件交互的线程进行比较。DispatcherObject是很多WPF控件的基类,用来为WPF控件处理线程之间的分发操作。注意,在WPF中还可以指定事件处理程序的优先级。这是因为WPF应用程序使用了两个UI线程。一个线程用来专门处理UI呈现,以便让UI总是能够及时呈现出动画等效果。你可以通过指定优先级来告诉框架哪类操作对于用户更加重要:要么是UI呈现,要么是处理某些特定的后台事件。

这段代码有几个优势。虽然使用了匿名委托定义,不过事件处理程序的核心仍位于事件处理程序中。与直接使用Control.IsInvokeRequired或ControlInvoke相比,这种做法更加易读且易于维护。在ControlExtensions中,使用了泛型方法来检查InvokeRequired或是比较两个线程,这也就让使用者从中解脱了起来。若是代码仅在单线程应用程序中使用,那么我也不会使用这些方法。不过若是程序最终可能在多线程环境中运行,那么不如使用上面这种更加完善的处理方式。

若想支持C# 2.0,那么还要做一些额外的工作。主要在于无法使用扩展方法和lambda表达式语法。这样,代码将变得有些臃肿。

// 定义必要的Action:

public delegate void Action;

public delegate void Action<T>(T arg);

// 3个和4个参数的Action定义省略

public static class ControlExtensions

{

    public static void InvokeIfNeeded(Control ctl, Action doit)

    {

        if (ctl.InvokeRequired)

            ctl.Invoke(doit);

        else

            doit();

    }

    public static void InvokeIfNeeded<T>( Control ctl,

        Action<T> doit, T args)

    {

        if (ctl.InvokeRequired)

            ctl.Invoke(doit, args);

        else

            doit(args);

    }

}

// 其他位置:

private void OnTick20(object sender, EventArgs e)

{

    ControlExtensions.InvokeIfNeeded(this, delegate()

    {

        toolStripStatusLabel1.Text =

          DateTime.Now.ToLongTimeString();

    });

}

在将这个方法应用到事件处理程序之前,我们来仔细看看InvokeRequired和Control.Invoke所做的工作。这两个方法并非没有什么代价,也不建议将这种模式应用到各处。Control.InvokeRequired用来判断当前代码是运行于创建该控件的线程之上,还是运行于另一个线程之上。若是运行于另一个线程之上,那么则需要使用封送。大多数情况下,这个属性的实现还算简单:只要检查当前线程的ID,并与创建该控件的线程ID进行比较即可。若二者匹配,那么则无需Invoke,否则就需要Invoke。这个比较并不需要花费太多时间,WPF版本的这类扩展方法也是执行了同样的检查。

不过其中还有一些边缘情况。若需要判断的控件还没有被创建,在父控件已创建好,正在创建子控件时就可能发生这个情况。那么此时,虽然C#对象已经存在,不过其底层的窗口句柄仍旧为null。此时也就无法进行比较,因此框架本身将花费一定代价来处理这种情况。框架将沿着控件树向上寻找,看看是否有上层控件已被创建。若是框架能够找到一个创建好了的窗体,那么该窗体将作为封送窗体。这是一个非常合理的假设,因为父控件将要负责创建子控件。这种做法可以保证子控件将会与父控件在同一个线程上创建。找到合适的父控件之后,框架即可执行同样的检查,比较当前线程的ID和创建该父控件的线程的ID。

不过,若是框架无法找到任何一个已创建的父窗体,那么则需要找到一些其他类型的窗体。若在层次体系中无法找到可用的窗体,那么框架将开始寻找暂存窗体(parking window),暂存窗体让你不会被某些Win32 API奇怪的行为所干扰。简而言之,有些对窗体的修改(例如修改某些样式)需要销毁并重新创建该窗体。暂存窗体就是用来在父窗体被销毁并重新创建的过程中用来临时保存其中的控件的。在这段时间内,UI线程仅运行于暂存窗体中。

WPF中,得益于Dispatcher类的使用,上述很多过程都得到了简化。每个线程都有一个Dispatcher。在第一次访问某个控件的Dispatcher时,类库将察看该线程是否已经拥有了Dispatcher。若已经存在,那么直接返回。如果没有的话,那么将创建一个新的Dispatcher对象,并关联在控件及其所在的线程之上。

不过这其中仍旧有可能存在着漏洞和发生失败。有可能所有的窗体,包括暂存窗体都没有被创建。在这种情况下,InvokeRequired将返回false,表示无需将调用封送到另一个线程上。这种情况可能会比较危险,因为这个假设可能是错误的,但框架也仅能做到如此了。任何需要访问窗体句柄的方法都无法成功执行,因为现在还没有任何窗体。此外,封送也自然会失败。若是框架无法找到任何可以封送的控件,自然也无法将当前调用封送到UI线程上。于是框架选择了一个可能在稍后出现的失败,而不是当前会立即出现的失败。幸运的是,这种情况在实际中非常少见。不过在WPF中,Dispatcher还是包含了额外的代码来预防这种情况。

总结一下InvokeRequired的相关内容。一旦控件创建完成,那么InvokeRequired的效率将会不错,且也能保证安全。不过若是目标控件尚未被创建,那么InvokeRequired则可能会耗费比较长的时间。而若是没有创建好任何控件,那么InvokeRequired则可能要相当长的时间,同时其结论也无法保证正确。但虽然Control.InvokeRequired有可能耗时较长,也比非必要地调用Control.Invoke要高效得多。且在WPF中,很多边缘情况都得到了优化,性能要比Windows窗体的实现提高不少。

接下来看看Control.Invoke的执行过程。(Control.Invoke的执行非常复杂,因此这里将仅做简要介绍。)首先,有一个特殊情况是虽然调用了Invoke方法,不过当前线程却和控件的创建线程一样。这是个最为简单的特例,框架将直接调用委托。即当InvokeRequired返回false时仍旧调用Control.Invoke()将会有微小的损耗,不过仍旧是安全的。

在真正需要调用Invoke时会发生一些有趣的情况。Control.Invoke能够通过将消息发送至目标控件的消息队列来实现线程调用。Control.Invoke还创建了一个专门的结构,其中包含了调用委托所需要的所有信息,包括所有的参数、调用栈以及委托的目标等。参数均会被预先复制出来,以避免在调用目标委托之前被修改(记住这是在多线程的世界中)。

在创建好这个结构并添加到队列中之后,Control.Invoke将向目标对象发送一条消息。Control.Invoke随后将在等待UI线程处理消息并调用委托时组合使用旋转等待(spin wait)和休眠。这部分的处理包含了一个重要的时间问题。当目标控件开始处理Invoke消息时,它并不会仅仅执行一个委托,而是处理掉队列中所有的委托。若你使用的是Control.Invoke的同步版本,那么不会看到任何效果。不过若是混合使用了Control.Invoke和Control.BeginInvoke,那么行为将有所不同。这部分内容将在稍后继续介绍,目前需要了解的是,控件的WndProc将在开始处理消息时处理掉每一个等待中的Invoke消息。对于WPF,可控制的要多一些,因为可以指定异步操作的优先级。你可以让Dispatcher将消息放在队列中时给出三种优先级:(1)基于系统或应用程序的当前状况;(2)使用普通优先级;(3)高优先级。

当然,这些委托中可能会抛出异常,且异常无法线程传递。因此框架将把对委托的调用用try/catch包围起来并捕获所有的异常。随后在UI线程完成处理之后,其中发生的异常将被复制到专门的数据结构中,供原线程分析。

在UI线程处理结束之后,Control.Invoke将察看UI线程中抛出的所有异常。如果确有异常发生,那么将在后台线程中重新抛出。若没有异常,那么将继续进行普通的处理。可以看到,调用一个方法的过程并不简单。

Control.Invoke将在执行封送调用时阻塞后台线程,虽然实际上在多线程环境中运行,不过仍旧让人觉得是同步的行为。

不过这可能不是你所期待的。很多时候,你希望让工作线程触发一个事件之后继续进行下面的操作,而不是同步地等待UI。这时则应该使用BeginInvoke。该方法的功能和Control.Invoke基本相同,不过在向目标控件发送消息之后,BeginInvoke将立即返回,而不是等待目标委托完成。BeginInvoke支持发送消息(可能在稍后才会处理)后立即返回到调用线程上。你可以根据需要为ControlExtensions类添加相应的异步方法,以便简化异步线程UI调用的操作。虽然与前面的那些方法相比,这些方法带来的优势不那么明显,不过为了保持一致,我们还是在ControlExtensions中给出。

public static void QueueInvoke(this Control ctl, Action doit)

{

    ctl.BeginInvoke(doit);

}

public static void QueueInvoke<T>(this Control ctl,

    Action<T> doit, T args)

{

    ctl.BeginInvoke(doit, args);

}

QueueInvoke并没有在一开始检查InvokeRequired。这是因为即使当前已经运行于UI线程之上,你仍可能想要异步地调用方法。BeginInvoke()就实现了这个功能。Control.BeginInvoke将消息发送至目标控件,然后返回。随后目标控件将在其下一次检查消息队列时处理该消息。若是在UI线程中调用的BeginInvoke,那么实际上这并不是异步的:当前操作后就会立即执行该调用。

这里我忽略了BeginInvoke所返回的Asynch结果对象。实际上,UI更新很少带有返回值。这会大大简化异步处理消息的过程。只需简单地调用BeginInvoke,然后等待委托在稍后的某个时候执行即可。但编写委托方法时需要格外小心,因为所有的异常都会在线程封送中被默认捕获。

在结束这个条目之前,我再来简单介绍一下控件的WndProc。当WndProc接收到了Invoke消息之后,将执行InvokeQueue中的每一个委托。若是希望按照特定的顺序处理事件,且你还混合使用了Invoke和BeginInvoke,那么可能会在时间上出现问题。可以保证的是,使用Control. BeginInvoke或Control.Invoke调用的委托将按照其发出的顺序执行。BeginInvoke仅仅会在队列中添加一个委托。不过稍后的任意一个Control.Invoke调用均会让控件开始处理队列中所有的消息,包括先前由BeginInvoke添加的委托。“稍后的某一时间”处理委托意味着你无法控制“稍后的某一事件”到底是何时。“现在”处理委托则意味着应用程序先执行所有等待的异步委托,然后处理当前的这一个。很有可能的是,某个由BeginInvoke发出的异步委托将在Invoke委托调用之前改变了程序的状态。因此需要小心地编写代码,确保在委托中重新检查程序的状态,而不是依赖于调用Control.Invoke时传入的状态。

简单举例,如下版本的事件处理程序很难显示出那段额外的文字。

private void OnTick(object sender, EventArgs e)

{

    this.InvokeAsynch(() => toolStripStatusLabel1.Text =

        DateTime.Now.ToLongTimeString());

    toolStripStatusLabel1.Text += "  And set more stuff";

}

这是因为第一个修改会被暂存于队列中,随后在开始处理接下来的消息时才会修改文字。而此时,第二条语句已经给标签添加了额外的文字。

Invoke和InvokeRequired为你默默地做了很多的工作。这些工作都是必需的,因为Windows窗体控件构建于STA模型之上。这个行为在最新的WPF中依旧存在。在所有最新的.NET Framework代码之下,原有的Win32 API并没有什么变化。因此这类消息传递以及线程封送仍旧可能导致意料之外的行为。你必须对这些方法的工作原理及其行为有着充分的理解。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/388732.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

什么是嵌入式系统

在我们的日常生活中&#xff0c;我们经常使用许多使用嵌入式系统技术设计的电气和电子电路和套件。计算机&#xff0c;手机&#xff0c;平板&#xff0c;笔记本电脑&#xff0c;数字电子系统以及其他电子和电子设备都是使用嵌入式系统设计的。 什么是嵌入式系统&#xff1f;将硬…

面向数据科学家的实用统计学_数据科学家必知的统计数据

面向数据科学家的实用统计学Beginners usually ignore most foundational statistical knowledge. To understand different models, and various techniques better, these concepts are essential. These work as baseline knowledge for various concepts involved in data …

字符串、指针、引用、数组基础

1.字符串&#xff1a;字符是由单引号所括住的单个字母、数字或符号。若将单引号改为双引号&#xff0c;该字符就会变成字符串。它们之间主要的差别是&#xff1a;双引号的字符串“A”会比单引号的字符串’A’在字符串的最后补上一个结束符’\0’&#xff08;Null字符&#xff0…

suse安装php,SUSE下安装LAMP

安装Apache可以看到编译安装Apache出错&#xff0c;rpm包安装gcc (首先要安装GCC)makemake install修改apache端口cd /home/sxit/apache2vi conf/httpd.confListen 8000启动 apache/home/root/apache2/bin/apachectl start(stop restart)http://localhost:8000安装一下PHP开发…

自己动手写事件总线(EventBus)

2019独角兽企业重金招聘Python工程师标准>>> 本文由云社区发表 事件总线核心逻辑的实现。 <!--more--> EventBus的作用 Android中存在各种通信场景&#xff0c;如Activity之间的跳转&#xff0c;Activity与Fragment以及其他组件之间的交互&#xff0c;以及在某…

viz::viz3d报错_我可以在Excel中获得该Viz吗?

viz::viz3d报错Have you ever found yourself in the following situation?您是否遇到以下情况&#xff1f; Your team has been preparing and working tireless hours to create and showcase the end product — an interactive visual dashboard. It’s a culmination of…

php 数组合并字符,PHP将字符串或数组合并到一个数组内方法

本文主要和大家分享PHP将字符串或数组合并到一个数组内方法&#xff0c;有两种方法&#xff0c;希望希望能帮助到大家。一般写法&#xff1a;<?php /*** add a string or an array to another array** param array|string $val* param array $array*/function add_val_to_a…

xcode 4 最低的要求是 10.6.6的版本,如果你是 10.6.3的版本,又不想升级的话。可以考虑通过修改版本号的方法进行安装

xcode 4 最低的要求是 10.6.6的版本&#xff0c;如果你是 10.6.3的版本&#xff0c;又不想升级的话。可以考虑通过修改版本号的方法进行安装。 一、打开控制台&#xff1b; 二、使用root用户&#xff1b; 命令&#xff1a;sudo -s 之后输入密码即可 三、编辑 /System/Library/C…

android 调试技巧

1.查看当前堆栈 Call tree new Exception(“print trace”).printStackTrace(); &#xff08;在logcat中打印当前函数调用关系&#xff09; 2.MethodTracing 性能分析与优&#xff08; 函数占用CPU时间&#xff0c; 调用次数&#xff0c; 函数调用关系&#xff09; a) 在程序…

Xml序列化

xml序列化 实现思路 通过程序生成一个xml文件来备份手机短信. 先获取手机短信的内容 —>通过xml备份.StringBuffer 代码如下public void click(View view) {StringBuffer sb new StringBuffer();sb.append("<?xml version\"1.0\" encoding\"UTF-8\…

java 添加用户 数据库,跟屌丝学DB2 第二课 建立数据库以及添加用户

在安装DB2 之后&#xff0c;就可以在 DB2 环境中创建自己的数据库。首先考虑数据库应该使用哪个实例。实例(instance) 提供一个由数据库管理配置(DBM CFG)文件控制的逻辑层&#xff0c;可以在这里将多个数据库分组在一起。DBM CFG 文件包含一组 DBM CFG 参数&#xff0c;可以使…

iphone视频教程

公开课介绍 本课程共28集 翻译至第15集 网易正在翻译16-28集 敬请关注 返回公开课首页 一键分享&#xff1a;  网易微博开心网豆瓣网新浪微博搜狐微博腾讯微博邮件 讲师介绍 名称&#xff1a;Alan Cannistraro 课程介绍 如果你对iPhone Development有兴趣&#xff0c;以下是入…

在Python中有效使用JSON的4个技巧

Python has two data types that, together, form the perfect tool for working with JSON: dictionaries and lists. Lets explore how to:Python有两种数据类型&#xff0c;它们一起构成了使用JSON的理想工具&#xff1a; 字典和列表 。 让我们探索如何&#xff1a; load a…

Vlan中Trunk接口配置

Vlan中Trunk接口配置 参考文献&#xff1a;HCNA网络技术实验指南 模拟器&#xff1a;eNSP 实验环境&#xff1a; 实验目的&#xff1a;掌握Trunk端口配置 掌握Trunk端口允许所有Vlan配置方法 掌握Trunk端口允许特定Vlan配置方法 实验拓扑&#xff1a; 实验IP地址 &#xff1a;…

django中的admin组件

Admin简介&#xff1a; Admin:是django的后台 管理的wed版本 我们现在models.py文件里面建几张表&#xff1a; class Author(models.Model):nid models.AutoField(primary_keyTrue)namemodels.CharField( max_length32)agemodels.IntegerField()# 与AuthorDetail建立一对一的关…

虚拟主机创建虚拟lan_创建虚拟背景应用

虚拟主机创建虚拟lanThis is the Part 2 of the MediaPipe Series I am writing.这是我正在编写的MediaPipe系列的第2部分。 Previously, we saw how to get started with MediaPipe and use it with your own tflite model. If you haven’t read it yet, check it out here.…

.net程序员安全注意代码及服务器配置

概述 本人.net架构师&#xff0c;软件行业为金融资讯以及股票交易类的软件产品设计开发。由于长时间被黑客攻击以及骚扰。从事高量客户访问的服务器解决架构设计以及程序员编写指导工作。特此总结一些.net程序员在代码编写安全以及服务器设置安全常用到的知识。希望能给对大家…

文件的读写及其相关

将软件布置在第三方电脑上会出现无法提前指定绝对路径的情况&#xff0c;这回影响到后续的文件读写&#xff1b;json文件是数据交换的一种基本方法&#xff0c;为了减少重复造轮子&#xff0c;经行标准化代码。关于路径&#xff1a; import os workspaceos.getcwd() pathos.pat…

接口测试框架2

现在市面上做接口测试的工具很多&#xff0c;比如Postman&#xff0c;soapUI, JMeter, Python unittest等等&#xff0c;各种不同的测试工具拥有不同的特色。但市面上的接口测试工具都存在一个问题就是无法完全吻合的去适用没一个项目&#xff0c;比如数据的处理&#xff0c;加…

python 传不定量参数_Python中的定量金融

python 传不定量参数The first quantitative class for vanilla finance and quantitative finance majors alike has to do with the time value of money. Essentially, it’s a semester-long course driving notions like $100 today is worth more than $100 a year from …