参考:http://www.cnblogs.com/zhouyinhui/archive/2008/01/27/1055261.html
第一次做WPF项目,要用到多线程,委托,自定义事件等技术。看了几篇网友技术文章觉得不错,做下笔记。
在多线程编程中,我们经常会需要在子线程中访问主线程的内容,特别是更改主窗体的UI界面内容,如果直接访问跨线程资源,系统就会报线程访问错误.查了MSDN发现由于这样的做法是线程不安全的。在WinForm中窗体类提供了Invoke方法以从子线程中访问主线程资源.在WPF中Window类并没有Invoke方法,但是WPF提供了专门负责线程调度工作的Window.Dispatcher类,每个线程都有一个,我们在一个线程中要让另一个线程做事情,其实就是调用目标Dispatcher调度完成.
Dispatcher
WPF规定了(事实上在.net2.0中便已规定了)UI元素只能由创建该元素的线程来访问。比如我们从新开的一个线程中访问主界面中的元素会出现运行时的异常。Dispatcher来维持着这一规定,并组织着消息循环。Dispatcher负责检测访问对象的线程与对象创建线程是否一致,不一致则抛出异常。值得一提的是,Dispatcher的消息循环中的Work Item是有优先级的,这可以让高优先级的项能有更多的工作时间。比如界面绘制比处理用户输入的优先级要高,这使得界面动画更加流畅。这也就是为什么,我们在调用Dispatcher.Invoke ( DispatcherPriority,…) 与Dispatcher. BeginInvoke (DispatcherPriority,…)要传入一个优先级参数的原因。下面是对各个优先级的说明:
优先级 |
说明 |
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类的对象。
对于阻塞的操作,不一定需要开启新线程
当我们遇到某个费时的操作是,第一反映往往是开启一个新线程,然后在后台去处理它,以便不阻塞我们的用户界面。当然,这是正确的想法。当并不是所有的都需如此。仔细想想界面阻塞的原因,我们知道其是由于时间被消耗在某个费时的Work Item上了,而那些处理用户界面的Work Item还在那里苦苦等候。So,我们只要别让他们苦苦等候就可以了,只要用户有界面操作我们就处理,线程上的其他空闲时间来处理我们的复杂操作。我们将复杂的费时的操作细化成很多不费时的小操作,在这些小操作之间的空隙处我们来处理相应用户界面的操作
阻塞的情况如下, MouseUp与MouseLeave会被阻塞:
(MouseDown)->(费时的,复杂操作)->(MouseUp)->(MouseLeave)…
细化后的情况如下,MouseUp与MouseLeave不会被阻塞:
(MouseDown)->(不费时的,小操作,复杂操作的1/n)->(MouseUp)->(不费时的,小操作,复杂操作的1/n) -> (MouseLeave)…
举一个简单的例子,假定我们的主界面上要显示一个数字,其为Window1的CurrentNum属性,我们已经将界面上的某个TextBlock与其绑定了起来:
Text="{Binding ElementName=window1,Path=CurrentNum}"
VerticalAlignment="Center"
HorizontalAlignment="Center" />
当我们点击界面上的一个按钮后,要求该数字被不停的累加,直到再次点击该按钮是停止.实际效果相当于:
{
this.CurrentNum++;
}
如果我们直接按照上面的While语句来书写程序,明显,当用户点击按钮后,整个线程将在这里被堵死,界面得不到更新,用户也没有办法再次点击按钮来停止这个循环,遭透了。
既不开启新线程又不阻塞界面应该怎么办呢?
我们知道this.CurrentNum++;语句以及更新绑定到CurrentNum属性的TextBlock并不耗费时间的,耗费时间的是他们的累加而成的死循环,所以,我们将这个循环分解成无数个仅仅由this.Current++语句组成的小方法,并在这些小方法的之间来处理用户界面:
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)…
用Delegate.Invoke()或Delegate.BeginInvoke()来开启新线程
除了new 一个Thread对象外,使用Delegate的Invoke或BeginInvoke方法也可以开启新的线程。
假设有下面这一个很费时的方法,我们应该如何使用Delegate来改造呢
{
Thread.Sleep(2000);
this.button_Test.Content = "OK!!!";
}
首先,我们声明一个可以用于TheHugeMethod方法的代理:
然后对TheHugeMethod构造一个NormalMethod类型的对象,并调用其Invoke方法(同步调用)或BeginInvoke方法(异步调用)
{
NormalMethod hugeMethodDelegate = new NormalMethod(this.TheHugeMethod);
hugeMethodDelegate.BeginInvoke(null, null);
}
由于是开启了新的线程,所以TheHugeMethod方法中对this.button_Test控件的调用语句也得改造一下:
{
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!!! ";
}
在新线程中执行消息循环
一般情况下我们不需要在新线程中执行消息循环了,因为我们常常是在新线程中执行一些后台操作而不需要用户在新线程中执行UI操作(比如我们在新线程中从网络上下载一些数据然后UI线程来显示这些数据)。当有时新线程却是需要消息循环的,最简单的例子是操作系统的“资源管理器”,每一个资源管理器窗口都在一个单独的线程中(它们都在同一个进程中)。
但当你按照如下方式编写代码来新建一个资源管理器窗口时,会出问题:
{
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的消息循环中。
{
Window1 newWindow = new Window1();
newWindow.Show();
System.Windows.Threading.Dispatcher.Run();
}
BackgroundWorker实质是:基于事件的异步模式
在多线程编程中,最爽的莫过于.net 提供了BackgroundWorker类了。其可以:
“在后台”执行耗时任务(例如下载和数据库操作),但不会中断您的应用程序。
同时执行多个操作,每个操作完成时都会接到通知。
等待资源变得可用,但不会停止(“挂起”)您的应用程序。
使用熟悉的事件和委托模型与挂起的异步操作通信。
我想大家对BackgroundWorker亦是再熟悉不过了,这里就不多做介绍了,另外“基于事件的异步模式”是WinForm的内容,但在WPF中完美运用(原因是WPF用DispatcherSynchronizationContext扩展了SynchronizationContext),可以参见MSDN“Event-based Asynchronous Pattern Overview”