建议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个建议》陆敏技

最新文章

  1. selenium使用笔记(一)
  2. MySQL 事物控制和锁定语句
  3. xcopy中提示“无效的参数数量”的解决方法
  4. Linux VFS Extended Attribute And Access Control Table
  5. UIImage 调整图片大小
  6. DNS协议 实践
  7. CF 314C Sereja and Subsequences(树状数组)
  8. How to Send Information (String, Image, Record) Between Two Applications
  9. csdn博客刷点击率代码
  10. Entity Framework Code First 映射继承关系
  11. WPF获取窗口句柄的方法
  12. bzoj:3616: War
  13. spring-boot 使用Aop通知打印控制器请求报文和返回报文
  14. 关于Springboot打包错误的问题 | Failed to execute goal org.springframework.boot:spring-boot-maven-plugin
  15. 关闭MySQL数据库的几种方法
  16. 【Javascript Demo】图片瀑布流实现
  17. Mac python3.6 安装即使用 psycopg2
  18. Gitlab-system-hooks
  19. 强制TFS用户与计算机(域)用户同步
  20. 为什么在AI领域网络安全更重要?先睹为快~

热门文章

  1. Oracle 10 Recycle Bin回收站
  2. 连接ES的Java项目报Too many open files错误
  3. 从一个开发的角度看负载均衡和LVS--FullNat
  4. MySQL多项模糊查询
  5. ZooKeeper架构
  6. HALCON中的算子大全(中英对照)
  7. 语义分析:C语言表达式的语法树生成——Python实现
  8. SDN openflow 学习小得
  9. totoise svn误将桌面作为checkout路径,界面一堆?
  10. Oracel官网下载各类版本的JDK