建议87:区分WPF和WinForm的线程模型

WPF和WinForm窗体应用程序都有一个要求,那就是UI元素(如Button、TextBox等)必须由创建它的那个线程进行更新。WinForm在这方面的限制并不是很严格,所以像下面这样的代码,在WinForm中大部分情况下还能运行(本建议后面会详细解释为什么会出现这种现象):

private void buttonStartAsync_Click(object sender, EventArgs e)
{
Task t = new Task(() =>
{
while (true)
{
label1.Text = DateTime.Now.ToString();
Thread.Sleep();
}
});
//如果有异常,就启动一个新任务
t.ContinueWith((task) =>
{
try
{
task.Wait();
}
catch (AggregateException ex)
{
foreach (Exception inner in ex.InnerExceptions)
{
MessageBox.Show(string.Format("异常类型:{0}{1}来自:{2}{3}异常内容:{4}", inner.GetType(),Environment.NewLine,
inner.Source, Environment.NewLine, inner.Message));
}
}
}, TaskContinuationOptions.OnlyOnFaulted);
t.Start();
}

但是,相同的一段代码如果放到WPF环境中,就肯定会抛出System.InvalidOperationException异常。

理论上,WinForm和WPF的线程模型非常接近,它们最后都是调用API(GetMessage或PeekMessage)来处理其他线程发送过来的消息,这些消息存储在系统的一个消息队列中。在WinForm和WPF中,创建主界面的线程就是主线程,也就是UI线程,UI线程负责处理该消息队列。只是两者在处理消息队列的上层机制上稍微有一些不同,这就造成了同样的代码得到不同的结果。

在WinForm框架中有一个ISynchronizeInvoke接口,所有的UI元素(表现为Control)都继承了该接口。其中,接口中的InvokdRequired属性表示了当前线程是否是创建它的线程。接口中的Invoke和BeginInvoke方法负责将消息发送到消息队列中,这样,UI线程就能够正确处理它了。那么,上面的这段代码在WinForm上的改进版本为(仅列出While循环部分):

while (true)
{
if (label1.InvokeRequired)
label1.BeginInvoke(new Action(() =>
{
label1.Text = DateTime.Now.ToString();
}));
else
label1.Text = DateTime.Now.ToString();
Thread.Sleep();
}

BeginInvoke方法接受的是一个Delegate类型的参数,在这里我们用一个Action来实现。

WPF应用程序的线程模型则完全依赖于DispatcherObject类型。所有的WPF控件都继承自一个抽象类Visual,而这个抽象类又最终继承自DispatcherObject类型。在这个DispatcherObject类型中有一个属性,两个方法。属性Dispatcher完成所有的工作线程和UI线程之间的调度任务。CheckAccess方法负责检测工作线程是否可以访问控件,如果是,则返回True;否则返回False。VerifyAccess方法则负责检测工作线程是否具有控件的访问权限,如果不能访问则抛出异常InvalidOperationException。

WinForm应用程序用类似CheckAccess的方式进行访问权限的判断;WPF应用程序则进行了改进,所有的UI控件都采用VerifyAccess的方式进行工作线程访问权限的判断。这直接决定了本建议开头处那个例子的输出,WPF只要判断出工作线程和UI线程不是同一个线程的,则直接抛出异常,而WinForm却有成功执行的余地。但是,WinForm的这种机制直接造成了程序的不稳定,因为即使在大部分情况下代码能很好的工作,可是在不确定的情况下,那样的代码中工作线程会直接操作UI元素,这样还是会抛出异常的。

考虑到WinForm在这个问题上的局限性,再次对WinForm的线程模型处理进行改进:

//用于表示主线程,在本例中就是UI线程
Thread mainThread; bool CheckAccess()
{
return mainThread == Thread.CurrentThread;
} void VerifyAccess()
{
if (!CheckAccess())
throw new InvalidOperationException("调用线程无法访问此对象,因为另一个线程拥有此对象");
} private void buttonStartAsync_Click(object sender, EventArgs e)
{
//当前线程就是主线程
mainThread = Thread.CurrentThread;
Task t = new Task(() =>
{
while (true)
{
if (!CheckAccess())
label1.BeginInvoke(new Action(() =>
{
label1.Text = DateTime.Now.ToString();
}));
else
label1.Text = DateTime.Now.ToString();
Thread.Sleep();
}
});
//如果有异常,就启动一个新任务
t.ContinueWith((task) =>
{
try
{
task.Wait();
}
catch (AggregateException ex)
{
foreach (Exception inner in ex.InnerExceptions)
{
MessageBox.Show(string.Format("异常类型:{0}{1}来自:{2}{3}异常内容:{4}", inner.GetType(), Environment.NewLine,
inner.Source, Environment.NewLine, inner.Message));
}
}
}, TaskContinuationOptions.OnlyOnFaulted);
t.Start();
}

在这段代码中,我们模拟WPF中DispatcherObject的两个方法CheckAccess和VerifyAccess对线程模型进行了重新处理,增强了系统的稳定性。在实际工作中,我们也可以提取这两个方法为扩展方法,以便项目中的所有UI类型都能使用到。

WPF支持这两个方法,其全部代码如下所示(注意查看While循环部分):

private void buttonStart_Click(object sender, RoutedEventArgs e)
{
Task t = new Task(() =>
{
while (true)
{
this.Dispatcher.BeginInvoke(new Action(() =>
{
textBlock1.Text = DateTime.Now.ToString();
}));
Thread.Sleep();
}
});
//为了捕获异常,启动了一个新任务
t.ContinueWith((task) =>
{
try
{
task.Wait();
}
catch (AggregateException ex)
{
foreach (Exception inner in ex.InnerExceptions)
{
MessageBox.Show(string.Format("异常类型:{0}{1}来自:{2}{3}异常内容:{4}", inner.GetType(), Environment.NewLine,
inner.Source, Environment.NewLine, inner.Message));
}
}
}, TaskContinuationOptions.OnlyOnFaulted);
t.Start();
}

注意 为了演示方便,本建议中的异常没有传递到主线程。在实际编码中,应当始终考虑将异常包装到主线程。

转自:《编写高质量代码改善C#程序的157个建议》陆敏技

编写高质量代码改善C#程序的157个建议——建议87:区分WPF和WinForm的线程模型的更多相关文章

  1. 编写高质量代码改善C#程序的157个建议[1-3]

    原文:编写高质量代码改善C#程序的157个建议[1-3] 前言 本文主要来学习记录前三个建议. 建议1.正确操作字符串 建议2.使用默认转型方法 建议3.区别对待强制转换与as和is 其中有很多需要理 ...

  2. 读书--编写高质量代码 改善C#程序的157个建议

    最近读了陆敏技写的一本书<<编写高质量代码  改善C#程序的157个建议>>书写的很好.我还看了他的博客http://www.cnblogs.com/luminji . 前面部 ...

  3. 编写高质量代码改善C#程序的157个建议——建议157:从写第一个界面开始,就进行自动化测试

    建议157:从写第一个界面开始,就进行自动化测试 如果说单元测试是白盒测试,那么自动化测试就是黑盒测试.黑盒测试要求捕捉界面上的控件句柄,并对其进行编码,以达到模拟人工操作的目的.具体的自动化测试请学 ...

  4. 编写高质量代码改善C#程序的157个建议——建议156:利用特性为应用程序提供多个版本

    建议156:利用特性为应用程序提供多个版本 基于如下理由,需要为应用程序提供多个版本: 应用程序有体验版和完整功能版. 应用程序在迭代过程中需要屏蔽一些不成熟的功能. 假设我们的应用程序共有两类功能: ...

  5. 编写高质量代码改善C#程序的157个建议——建议155:随生产代码一起提交单元测试代码

    建议155:随生产代码一起提交单元测试代码 首先提出一个问题:我们害怕修改代码吗?是否曾经无数次面对乱糟糟的代码,下决心进行重构,然后在一个月后的某个周一,却收到来自测试版的报告:新的版本,没有之前的 ...

  6. 编写高质量代码改善C#程序的157个建议——建议154:不要过度设计,在敏捷中体会重构的乐趣

    建议154:不要过度设计,在敏捷中体会重构的乐趣 有时候,我们不得不随时更改软件的设计: 如果项目是针对某个大型机构的,不同级别的软件使用者,会提出不同的需求,或者随着关键岗位人员的更替,需求也会随个 ...

  7. 编写高质量代码改善C#程序的157个建议——建议153:若抛出异常,则必须要注释

    建议153:若抛出异常,则必须要注释 有一种必须加注释的场景,即使异常.如果API抛出异常,则必须给出注释.调用者必须通过注释才能知道如何处理那些专有的异常.通常,即便良好的命名也不可能告诉我们方法会 ...

  8. 编写高质量代码改善C#程序的157个建议——建议152:最少,甚至是不要注释

    建议152:最少,甚至是不要注释 以往,我们在代码中不写上几行注释,就会被认为是钟不负责任的态度.现在,这种观点正在改变.试想,如果我们所有的命名全部采用有意义的单词或词组,注释还有多少存在的价值. ...

  9. 编写高质量代码改善C#程序的157个建议——建议151:使用事件访问器替换公开的事件成员变量

    建议151:使用事件访问器替换公开的事件成员变量 事件访问器包含两部分内容:添加访问器和删除访问器.如果涉及公开的事件字段,应该始终使用事件访问器.代码如下所示: class SampleClass ...

  10. 编写高质量代码改善C#程序的157个建议——建议150:使用匿名方法、Lambda表达式代替方法

    建议150:使用匿名方法.Lambda表达式代替方法 方法体如果过小(如小于3行),专门为此定义一个方法就会显得过于繁琐.比如: static void SampeMethod() { List< ...

随机推荐

  1. PoJ 1595 PrimeCuts

    Prime Cuts Time Limit: 1000MS   Memory Limit: 10000K Total Submissions: 9339   Accepted: 3562 Descri ...

  2. JAVA线程分析定位排查

    java开发中有的时间经常遇到某个线程消耗CPU高的问题,但是不清楚这个线程在做什么. 于是网上看了一些文章,找到了一个比较靠谱的办法(Linux下,Windows同理) : 首先使用top确认是否消 ...

  3. 【CVE】CVE-2018-4304 Apple多个操作系统函数拒绝服务漏洞

    TextImpact: Processing a maliciously crafted text file may lead to adenial of serviceDescription: A ...

  4. react.js的了解

    React很大的特点就是“轻”,再加上VDOM这个很好的idea让React非常非常快(在上面那个测试里面0.3s左右就载入完毕).另外React和Angular一个很大的不同就是React采用的是o ...

  5. Python的urllib和urllib2模块

    Python的urllib和urllib2模块都做与请求URL相关的操作,但他们提供不同的功能.他们两个最显着的差异如下: urllib2可以接受一个Request对象,并以此可以来设置一个URL的h ...

  6. ascii码与二进制码有何区别?

    ascii编码和二进制码是两个概念.ASCII主要是为了电脑显示和传输拉丁字母而发明的一套编码,二进制则是为了计算机方便计算.传输数据而使用的一种方法.ASCII(American Standard ...

  7. 构造方法PK实例方法

    1.构造方法 (1)用于对象初始化,一个类中至少有一个构造方法 (2)不能显示调用,只能在创建对象时,使用new来调用 (3)构造方法不能有返回值 (4)构造方法名称必须与类名一样 2.实例方法 (1 ...

  8. Dubbo与Zookeeper、SpringMVC整合和使用(负载均衡

    转载:http://blog.csdn.net/congcong68/article/details/41113239 互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架 ...

  9. ajax 回调函数

    回调函数 如果要处理$.ajax()得到的数据,则需要使用回调函数.beforeSend.error.dataFilter.success.complete. beforeSend 在发送请求之前调用 ...

  10. 6. H.264/AVC编码器原理

    1. H.264/AVC的应用 H.264 不仅具有优异的压缩性能,而且具有良好的网络亲和性,这对实时的视频通信是十分重要的.和 MPEG-4 中的重点是灵活性不同,H.264 着重在压缩的高效率和传 ...