WPF的线程模型[zhuan]
谈到多线程,很多人对其可能都不太有好感,觉得麻烦与易出错。所以我们不排除有这样的情况:假设我对“多线程”、“异步”这些字眼潜意识地有些反感,所以在编码过程中能不用就不用,觉得延迟几百毫秒还是可以忍受的,如果系统中这种“可以忍受”的地方很多,最后我们会发现系统的性能变得一团糟,界面总是在“卡”(阻塞)。这里我们讨论一下WPF的多线程模型,以便利用它使我们的UI线程得到解脱。
1,UI线程
2,Dispatcher
优先级 | 说明 |
Inactive | |
SystemIdle | 仅当系统空闲时才将工作项目调度到 UI 线程。这是实际得到处理的项目的最低优先级。 |
ApplicationIdle | 仅当应用程序本身空闲时才将工作项目调度到 UI 线程。 |
ContextIdle | 仅在优先级更高的工作项目得到处理后才将工作项目调度到 UI 线程。 |
Background | 在所有布局、呈现和输入项目都得到处理后才将工作项目调度到 UI 线程。 |
Input | 以与用户输入相同的优先级将工作项目调度到 UI 线程。 |
Loaded | 在所有布局和呈现都完成后才将工作项目调度到 UI 线程。 |
Render | 以与呈现引擎相同的优先级将工作项目调度到 UI 线程。 |
DataBind | 以与数据绑定相同的优先级将工作项目调度到 UI 线程。 |
Normal | 以正常优先级将工作项目调度到 UI 线程。这是调度大多数应用程序工作项目时的优先级。 |
Send | 以最高优先级将工作项目调度到 UI 线程。 |
上面提到了Dispatcher维持着一个规矩“只有创建该对象的线程可以访问该对象”。这里的对象不仅仅是指一些UI控件(比如Button),而是所以的派生于DispatcherObject类的对象。我们做一个小小的试验,假设有如下这样一个类:
public class Data
{
private object theData = null;
public object TheData
{
get
{
return theData;
}
set
{
theData = value;
}
}
}
我们在UI线程中声明其一个实例,并在新线程中使用它:
private Data myData = new Data();
private void btnTest_Click(object sender, RoutedEventArgs e)
{
ThreadStart ts = new ThreadStart(this.UpdateData);
Thread newThread = new Thread(ts);
newThread.Start();
}
private void UpdateData()
{
this.myData.TheData = 5;
}
OK,不会有问题(我们暂不考虑跨多个线程访问是否安全等)。
但是,如果我们让Data类继承于DependencyObject类(其又继承于DispatcherObject类,在WPF中我们会经常这样做,因为我们要使用Dependency Property):
public class Data : DependencyObject
{
public object TheData
{
get
{
return (object)GetValue(TheDataProperty);
}
set
{
SetValue(TheDataProperty, value);
}
}
public static readonly DependencyProperty TheDataProperty =
DependencyProperty.Register("TheData", typeof(object), typeof(Data), new UIPropertyMetadata(null));
}
如果现在还按照以前的使用方式(在UI线程中声明其一个实例,并在新线程中使用它)来使用,你就会收到一个“由于其他线程拥有此对象,因此调用线程无法对其进行访问。”的System.InvalidOperationException。正确使用它的方式是使用Dispatcher的Invoke或BeginInvoke方法:
private void UpdateData()
{
if (this.myData.Dispatcher.CheckAccess())
{
this.myData.TheData = 5;
}
else
{
this.myData.Dispatcher.Invoke(DispatcherPriority.Normal, (ThreadStart)delegate
{
this.myData.TheData = 5;
});
}
}
更多的,你可以访问这篇Blog:http://www.cnblogs.com/zhouyinhui/archive/2007/05/10/742134.html
3,对于阻塞的操作,不一定需要开启新线程
当我们遇到某个费时的操作是,第一反映往往是开启一个新线程,然后在后台去处理它,以便不阻塞我们的用户界面。当然,这是正确的想法。当并不是所有的都需如此。仔细想想界面阻塞的原因,我们知道其是由于时间被消耗在某个费时的Work Item上了,而那些处理用户界面的Work Item还在那里苦苦等候。So,我们只要别让他们苦苦等候就可以了,只要用户有界面操作我们就处理,线程上的其他空闲时间来处理我们的复杂操作。我们将复杂的费时的操作细化成很多不费时的小操作,在这些小操作之间的空隙处我们来处理相应用户界面的操作
阻塞的情况如下, MouseUp与MouseLeave会被阻塞:
(MouseDown)->(费时的,复杂操作)->(MouseUp)->(MouseLeave)…
细化后的情况如下,MouseUp与MouseLeave不会被阻塞:
(MouseDown)->(不费时的,小操作,复杂操作的1/n)->(MouseUp)->(不费时的,小操作,复杂操作的1/n) -> (MouseLeave)…
举一个简单的例子,假定我们的主界面上要显示一个数字,其为Window1的CurrentNum属性,我们已经将界面上的某个TextBlock与其绑定了起来:
<TextBlock x:Name="textBlock_ShowNum"
Text="{Binding ElementName=window1,Path=CurrentNum}"
VerticalAlignment="Center"
HorizontalAlignment="Center" />
当我们点击界面上的一个按钮后,要求该数字被不停的累加,直到再次点击该按钮是停止.实际效果相当于:
while (this.IsCalculating)
{
this.CurrentNum++;
}
如果我们直接按照上面的While语句来书写程序,明显,当用户点击按钮后,整个线程将在这里被堵死,界面得不到更新,用户也没有办法再次点击按钮来停止这个循环,遭透了。
既不开启新线程又不阻塞界面应该怎么办呢?
我们知道this.CurrentNum++;语句以及更新绑定到CurrentNum属性的TextBlock并不耗费时间的,耗费时间的是他们的累加而成的死循环,所以,我们将这个循环分解成无数个仅仅由this.Current++语句组成的小方法,并在这些小方法的之间来处理用户界面:
public delegate void NormalDelegate();
void button_StartOrStop_Click(object sender, RoutedEventArgs e)
{
if (this.IsCalculating)
{
NormalDelegate calNumDelegate = new NormalDelegate(this.CalNum);
this.Dispatcher.BeginInvoke(DispatcherPriority.SystemIdle, calNumDelegate);
}
}
private void CalNum()
{
this.CurrentNum++;
if (this.IsCalculating)
{
NormalDelegate calNumDelegate = new NormalDelegate(this.CalNum);
this.Dispatcher.BeginInvoke(DispatcherPriority.SystemIdle, calNumDelegate);
}
}
上面的两段代码可以简单地如下示意:
阻塞的情况如下, MouseUp与MouseLeave会被阻塞:
(MouseDown)->(费时的While(true))->(MouseUp)->(MouseLeave)…
细化后的情况如下,MouseUp与MouseLeave不会被阻塞:
(MouseDown)->(不费时的CalNum)->(MouseUp)->(不费时的CalNum) -> (MouseLeave)…
4,用Delegate.Invoke()或Delegate.BeginInvoke()来开启新线程
除了new 一个Thread对象外,使用Delegate的Invoke或BeginInvoke方法也可以开启新的线程。
假设有下面这一个很费时的方法,我们应该如何使用Delegate来改造呢
private void TheHugeMethod()
{
Thread.Sleep(2000);
this.button_Test.Content = "OK!!!";
}
首先,我们声明一个可以用于TheHugeMethod方法的代理:
public delegate void NormalMethod();
然后对TheHugeMethod构造一个NormalMethod类型的对象,并调用其Invoke方法(同步调用)或BeginInvoke方法(异步调用)
void button_Test_Click(object sender, RoutedEventArgs e)
{
NormalMethod hugeMethodDelegate = new NormalMethod(this.TheHugeMethod);
hugeMethodDelegate.BeginInvoke(null, null);
}
由于是开启了新的线程,所以TheHugeMethod方法中对this.button_Test控件的调用语句也得改造一下:
private void TheHugeMethod()
{
Thread.Sleep(2000);
//will crash
//this.button_Test.Content = "OK!!!";
NormalMethod updateUIDelegate = new NormalMethod(this.UpdateUI);
this.button_Test.Dispatcher.BeginInvoke(DispatcherPriority.Normal, updateUIDelegate);
}
private void UpdateUI()
{
this.button_Test.Content = "OK!!! ";
}
5,在新线程中执行消息循环
但当你按照如下方式编写代码来新建一个资源管理器窗口时,会出问题:
private void button_NewWindow_Click(object sender, RoutedEventArgs e)
{
Thread newWindowThread = new Thread(new ThreadStart(ThreadStartingPoint));
newWindowThread.SetApartmentState(ApartmentState.STA);
newWindowThread.IsBackground = true;
newWindowThread.Start();
}
private void ThreadStartingPoint()
{
Window1 newWindow = new Window1();
newWindow.Show();
}
问题是newWindow闪现一下就消失了。因为该新窗口没有进入消息循环,当newWindow.Show()方法执行完毕后,新线程的一切都结束了。
正确的方法是在newWindow.Show();方法后加入Dispatcher.Run()语句,其会将主执行帧推入该Dispatcher的消息循环中。
private void ThreadStartingPoint()
{
Window1 newWindow = new Window1();
newWindow.Show();
System.Windows.Threading.Dispatcher.Run();
}
6,BackgroundWorker实质是:基于事件的异步模式
在多线程编程中,最爽的莫过于.net 提供了BackgroundWorker类了。其可以:
“在后台”执行耗时任务(例如下载和数据库操作),但不会中断您的应用程序。
同时执行多个操作,每个操作完成时都会接到通知。
等待资源变得可用,但不会停止(“挂起”)您的应用程序。
使用熟悉的事件和委托模型与挂起的异步操作通信。
我想大家对BackgroundWorker亦是再熟悉不过了,这里就不多做介绍了,另外“基于事件的异步模式”是WinForm的内容,但在WPF中完美运用(原因是WPF用DispatcherSynchronizationContext扩展了SynchronizationContext),可以参见MSDN“Event-based Asynchronous Pattern Overview”