2008年9月30日

前一篇中我们看到通过使用PowerThreading中的AsyncResult<T>类,我们可以很方便的将一个同步操作封装成异步的方式。同时使用这种方法和PInvoke,我们也可以为现有的C++设备库,如蓝牙设备提供一个.Net的异步类库。这样我们可以实现大部分对设备访问的.Net异步类库。

但当我们有特殊要求时,如果调整LCD亮度时,就需要调用Window API中的DeviceIoControl函数。PInvoke加上使用IO完成端口是件繁琐且容易出错的事情,幸运地是,我们有Richard,通过使用PowerThreading中的DeviceIO类,我们可以很方便的实现一个异步设备操作类。下面通过实现一个简单的异步打开光驱的方法,来看看如何使用Wintellect.IO.DeviceIO。

获得光驱句柄

在Win32中要访问一个设备,首先我们要调用CreateFile方法来获得该设备的文件句柄。PowerTheading中DeviceIO类对CreateFile进行了封装。我们可以容易通过创建一个DeviceIO对象来代表光驱。如下例:

 1 public class CDDrive : IDisposable
 2 {
 3     private const Int32 LOCK_TIMEOUT = 10000// 10s
 4     private const Int32 LOCK_RETRIES = 20;
 5     private const String DRIVE_PATH_TPL = @"{0}:";
 6     private const String FILE_PATH_TPL = @"\\.\{0}:";
 7 
 8     public Char DriveLetter { getprivate set; }
 9     private DeviceIO m_cdrom = null;
10 
11     public CDDrive(Char driveletter)
12     {
13         if (!Char.IsLetter(driveletter))
14         {
15             throw new ArgumentException("无效盘符""letter");
16         }
17 
18         // 判断该盘符是否是光驱
19 var tmp = Win32Functions.GetDriveType(
20                 String.Format(DRIVE_PATH_TPL, driveletter));
21 
22         if ((tmp != DriveType.CDRom))
23         {
24             throw new ArgumentException("无效盘符""letter");
25         }
26 
27         DriveLetter = driveletter;
28 
29         this.Open();
30     }
31 
32     public void Open()
33     {
34         this.Close();
35 
36         // 异步方式打开CDROM文件句柄,如失败抛出Win32Exception异常
37         m_cdrom = new DeviceIO(
38                 String.Format(FILE_PATH_TPL, DriveLetter),
39                 FileAccess.ReadWrite, FileShare.ReadWrite, true);
40     }
41 
42     public void Close()
43     {
44         this.Dispose(true);
45     }
46 
47     public void Dispose()
48     {
49         this.Dispose(true);
50     }
51 
52     ~CDDrive()
53     {
54         Dispose(false);
55     }
56 
57     protected virtual void Dispose(Boolean disposing)
58     {
59         if (disposing)
60         {
61             if (m_cdrom != null)
62             {
63                 m_cdrom.Dispose();
64                 m_cdrom = null;
65             }
66         }
67         GC.SuppressFinalize(this);
68     }
69 }

 

DeviceIO有两个构造函数,主要区别在于第一个参数。我们可以传入文件路径,或者传入一个已获得的文件句柄。如果文件路径不对,或者访问方式不对,会抛出Win32Exception的异常。DeviceIO实现了IDisposable接口,当不需要使用时可以调用Dispose方法来释放文件句柄。CDDrive也实现了IDisposable接口,及Finalizer以确保光驱的句柄能够释放。

设备操作控制码

在Win32中,如果我们要向IO设备发出指令,需要调用DeviceIoControl方法。该方法需要设备文件句柄,操作控制码,传入数据及返回数据的Buffer。文件句柄在前面已经获得,而这个操作控制码在Win32中,是一个DWORD类型,代表了不同的操作,如打开光驱,调亮LCD等等。PowerThreading库中的DeviceControlCode 类可以帮助我们创建一个托管的结构。如弹出光驱的操作控制码IOCTL_STORAGE_EJECT_MEDIA定义如下:

 1 public static class IoControlCode
 2 {
 3     public static DeviceControlCode IOCTL_STORAGE_EJECT_MEDIA =
 4         new DeviceControlCode(
 5             DeviceType.MassStorage, 0x0202,
 6             DeviceMethod.Buffered, DeviceAccess.Read);
 7 
 8     public static DeviceControlCode FSCTL_LOCK_VOLUME =
 9         new DeviceControlCode( 
10             DeviceType.FileSystem, 0x6,
11             DeviceMethod.Buffered, DeviceAccess.Any);
12 
13     public static DeviceControlCode FSCTL_DISMOUNT_VOLUME =
14         new DeviceControlCode(
15             DeviceType.FileSystem, 0x8,
16             DeviceMethod.Buffered, DeviceAccess.Any);
17 
18     public static DeviceControlCode IOCTL_STORAGE_MEDIA_REMOVAL =
19         new DeviceControlCode(
20             DeviceType.MassStorage, 0x201,
21             DeviceMethod.Buffered, DeviceAccess.Read);
22 }

 

操作控制码分为4个部分:设备类型,操作,设备方法及访问方式。这个与WinSDK中Winioctl.h中定义相同,我们可以根据这个头文件很容易的创建对应的DeviceControlCode结构。下面是IOCTL_STORAGE_EJECT_MEDIA在头文件中的定义:

1 #define IOCTL_STORAGE_EJECT_MEDIA             
2 CTL_CODE(IOCTL_STORAGE_BASE, 0x0202, METHOD_BUFFERED, FILE_READ_ACCESS)

在上面的IoControlCode静态类中,定义了4个操作控制码。这是因为如果我们弹出光驱或Eject可移动媒体,我们不能简单发出IOCTL_STORAGE_EJECT_MEDIA操作控制码,首先我们得发出FSCTL_LOCK_VOLUME锁住光驱以防止其他人写入,在发出FSCTL_DISMOUNT_VOLUME卸载卷,在发出IOCTL_STORAGE_MEDIA_REMOVAL移除媒体,最后才发出IOCTL_STORAGE_EJECT_MEDIA弹出光驱。具体可参考Microsoft KB 165721

发出设备操作

在第一步中,通过创建DeviceIO对象,我们已经获得了光驱的文件句柄。DeviceIO类提供了同步和异步的两套3个方法,来帮助我们发送操作到设备。同步方法内部实际调用的异步方法,所以下面我们看看3个异步方法:

 1     public IAsyncResult BeginControl(
 2         DeviceControlCode deviceControlCode, object inBuffer, 
 3         AsyncCallback asyncCallback, object state);
 4     public void EndControl(IAsyncResult result);
 5 
 6     public IAsyncResult BeginGetArray<TElement>(
 7         DeviceControlCode deviceControlCode, object inBuffer, 
 8         int maxElements, AsyncCallback asyncCallback, 
 9         object state) where TElement : struct;
10     public TElement[] EndGetArray<TElement>(IAsyncResult result) where TElement : struct;
11 
12     public IAsyncResult BeginGetObject<TResult>(
13         DeviceControlCode deviceControlCode, object inBuffer, 
14         AsyncCallback asyncCallback, 
15         object state) where TResult : new();
16     public TResult EndGetObject<TResult>(IAsyncResult result) where TResult : new();
17 

如果操作不需要返回数据时,可以使用BeginControl;如有返回数据可调用BeginGetObject,TResult对应返回数据的类型;而当返回的数据是数组是可使用BeginGetArray,TElement是返回数组的元素类型。我们可以在http://pinvoke.net/ 网站或google上查找TResult和TElement对应的托管类型定义。下面是弹出光驱的代码:

 1     public IAsyncResult BeginEject(
 2                 AsyncCallback callback, Object state)
 3     {
 4         AsyncResult ar = new AsyncResult(callback, state);
 5 
 6         ThreadPool.QueueUserWorkItem(
 7                 new WaitCallback(delegate {
 8 
 9                     // 调用IOCTL_STORAGE_EJECT_MEDIA,尝试Lock光驱
10                     for (Int32 tryCount = 0; tryCount < LOCK_RETRIES; tryCount++)
11                     {
12                         try
13                         {
14                             m_cdrom.Control(IoControlCode.FSCTL_LOCK_VOLUME);
15                             break;
16                         }
17                         catch (Exception ex)
18                         {
19                             // 如最后一次仍不能获得光驱,则返回异常
20                             if (tryCount == LOCK_RETRIES - 1)
21                             {
22                                 ar.SetAsCompleted(ex, false);
23                                 return;
24                             }
25                         }
26                         Thread.Sleep(LOCK_RETRIES);
27                     }
28 
29                     try
30                     {
31                         // 调用FSCTL_DISMOUNT_VOLUME卸载卷
32                         m_cdrom.Control(IoControlCode.FSCTL_DISMOUNT_VOLUME);
33 
34                         // 调用IOCTL_STORAGE_MEDIA_REMOVAL移除媒体
35                         m_cdrom.Control(IoControlCode.IOCTL_STORAGE_MEDIA_REMOVAL,
36                             new PREVENT_MEDIA_REMOVAL(false));
37 
38                         // 调用IOCTL_STORAGE_EJECT_MEDIA异步方法弹出光驱
39                         m_cdrom.EndControl(m_cdrom.BeginControl(
40                             IoControlCode.IOCTL_STORAGE_EJECT_MEDIA, nullnullnull));
41 
42                         ar.SetAsCompleted(nullfalse);
43 
44                     }
45                     catch (Exception ex)
46                     {
47                         ar.SetAsCompleted(ex, false);
48                     }
49                 }),
50                 null);
51 
52         return ar;
53     }
54 
55     public void EndEject(IAsyncResult asyncResult)
56     {
57         AsyncResult ar = (AsyncResult)asyncResult;
58         ar.EndInvoke();
59     }

上面的代码中,我们只使用了Control方法,其中IOCTL_STORAGE_MEDIA_REMOVAL需要传入数据。GetObject和GetArray的使用方法类似。

由于我们得循环的检查Lock是否成功,我们不得不使用线程池中的线程顺序的发出操作指令,因而该线程并没有最优化。如果我们都采取异步方式,我们不得不写很多回调或匿名函数,代码将变得很难看。这也是APM代码很麻烦的一个原因。后面的文章我们在来看看有没有更好的办法。

参考:

Asynchronous Device Operations by Jeffery Richard

KB165721: How To Ejecting Removable Media in Windows NT/Windows 2000/Windows XP by Microsoft

posted @ 2008-09-30 00:21 Nullnoid 阅读(1572) 评论(3) 编辑

2008年9月9日

Note: 本篇主要内容来自Jeffery Richard的Implementing the CLR Asynchronous Programming Model,看过的同学可以略过。

前一篇中介绍了使用APM开发多线程程序的有点,同时关于如何使用APM方式的文章也很多了。所以,这篇主要想看看如何使用Jeffery Richard的Power Threading类库,来开发一个支持APM的类。

为什么要自己实现APM

前一篇中,提到了受计算约束(Computing-Bound)与受I/O约束(I/O-Bound)的区别。如果我们是一个受计算约束类型的任务,完全可以将这个方法变成委托(Delegate)的形式,然后通过BeginInvoke/EndInvoke来实现APM;另外,FCL本身也封装了大量对I/O异步操作的类,如FileStream,NetworkStream等,那我们为什么还要自己实现APM呢。Jeffery Richard列举了4个原因:

  1. 尽管FCL提供很多与设备通讯的异步类,但仍有部分未提供,如并口;或者FCL提供的功能不够
  2. 对现有的I/O类库添加功能,如我们在WebHttpRequest可以开发一个HTTP的过滤类等
  3. 实现受计算约束的APM
  4. 对不支持异步模式设备提供异步访问方式

PowerThreading类库中的AsyncResult和AsyncResult<T>

APM的核心就是IAsyncResult接口。当调用类异步方式BeginXxx时,返回IAsyncResult的对象。为了返回满足这个接口的对象,我们的类需要维护的一个 是否完成的状态IsCompleted;一个ManuelResetEvent对象及一个委托保持回调函数。这些虽然比较简单,但很麻烦。使用PowerThreading类库中AsyncResult和AsyncResult<T>,我们可以非常容易的实现支持APM的类。

AsyncResult用于没有返回值,而AsyncResult<T>用于返回类型为T的任务。下面的例子中,我们看看如何使用AsyncResult<T>。

首先,我们准备实现一个能够过滤Stream对象的类,类定义如下:

 1 class StreamFilter
 2 {
 3    private Stream                   m_stream;
 4    public StreamFilter(Stream stream)
 5    {
 6    }
 7 
 8    public IAsyncResult BeginParse(
 9             AsyncCallback callback, Object state)
10    {
11    }
12 
13    public Stream Parse()
14    {
15    }
16 
17    public Stream EndParse(IAsyncResult asyncResult)
18    {
19    }
20 }

Parse()是同步访问接口,执行主要的任务,而BeginParse()和EndParse()是对应的异步接口。下面是使用AsyncResult<T>后的代码

 1 class StreamFilter
 2 {
 3    private Stream                   m_stream;
 4    public StreamFilter(Stream stream)
 5    {
 6       m_stream = stream;
 7    }
 8 
 9    public IAsyncResult BeginParse(
10             AsyncCallback callback, Object state)
11    {
12       //创建一个IAsyncResult对象来标志异步操作
13       AsyncResult<Stream> ar = 
14             new AsyncResult<Stream>(callback, state);
15 
16       // 调用辅助函数,并传递AsycnResult对象
17       // 如果使用Anonymouse Method更简洁
18       ThreadPool.QueueUserWorkItem(ParseHelper,ar);
19 
20       return ar; // 返回IAsyncResult
21    }
22 
23    private void ParseHelper(Object asyncResult)
24    {
25       var ar = (AsyncResult<Stream>)asyncResult;
26       try
27       {
28          // 执行真正的任务
29          Stream result = Parse();
30 
31          // 更新AsycnResult状态
32          ar.SetAsCompleted(result, false);
33       } catch (Exception e)
34       {
35          // 保存Exception对象
36          ar.SetAsCompleted(e, false);
37       }
38    }
39 
40    public Stream Parse()
41    {
42       // 分析m_stream,并返回过滤后的stream对象
43       Stream filterStream = null;
44       return filterStream;
45    }
46 
47    public Stream EndParse(IAsyncResult asyncResult)
48    {
49       var ar = (AsyncResult<Stream>)asyncResult;
50       // 等待任务完成
51       return ar.EndInvoke();
52    }
53 }

 

比较重要的是ParseHelper函数,这个函数在新的线程中执行,通过ar.SetAsCompleted来更新状态。 如果结合匿名方法或者Limbda表达式,代码可以变得更加的紧凑。

 

参考:

Implementing the CLR Asynchronous Programming Model by Jeffery Richard

Power Threading类库

posted @ 2008-09-09 19:21 Nullnoid 阅读(2683) 评论(0) 编辑

2008年9月8日

前段时间看奥运,一下子懒了下来,就停止更新了。本来上一篇,就准备写XAML和Extension的东西,不过最近回顾了前面写的东西,觉得有必要总结一下.Net中的异步编程模式 (APM) 。计划分四个部分:
  • 如何实现支持APM的类
  • 如果实现支持APM的硬件设备类
  • Event-based的APM
  • Continuation-passing Style(CPS)的APM
有些内容相关的文章已经很多了,我写的内容也主要来源于MSDN CONCURRENT AFFAIRS系列。我主要想结合这些内容,看如果使用PowerThreading类库来简化我们的开发 在开始之前,让我们看看异步编程模式APM:

APM简介

APM的概念简单来说就是,主线程创建一个线程执行比较费时的任务,而自己继续执行其他任务。通过使用ThreadPool或者Thread,我们可以很容易的创建线程并让其执行任务,问题的难点在于主线程如何知道该任务是否结束,及如果取消,控制该任务的执行等。

.Net 1.x中定义了IAsyncResult接口,并且类库中会执行费时任务的类都同时提供了同步和异步的API,如FileStream同步读文件的Read方法,对应的异步版本BeginRead和EndRead。主线程调用BeginRead方法时,该方法立即返回一个IAsyncResult对象,而同时FileStream开始执行硬盘读操作。IAsyncResult可以看作的这个读操作的一个"Handler",定义如下:

1 public interface IAsyncResult {
2    WaitHandle AsyncWaitHandle { get; } // 用于等待直到完成方式
3    Boolean    IsCompleted     { get; } // 由于轮询查看方式
4    Object     AsyncState      { get; } // 用于回调方式
5    Boolean    CompletedSynchronously { get; } // 几乎重来不用
6 }

通过这个"Handler"我们可以采用3种方式来等待任务的结束。从这个接口定义,我们可以看出,IAsyncResult方法的局限性。当主线程发出异步任务后,无法取消该任务,也无法知道该任务的执行进度等。我们将后面的内容中,在看如果改善这些问题。

另外,BeginXxx和EndXxx必须成对调用,BeginXxx用于触发任务的执行,EndXxx则用于获得任务执行的返回。尽管有的任务不需要知道结果,但EndXxx还是得调用,否则会造成内存泄漏。如果可以采用回调方式,比较常见的方法就是在回调函数中调用EndXxx。实现回调函数,是比较讨厌的事情,必需通过一定方法,类成员变量,AsyncState等,将FileStream对象传递进去。通过C# 2.0中Anonymous Method及 Limbda,我们可以简化代码,如下面的例子,通过Anonymous Method局部变量的捕获功能,request局部变量漂亮的传递到回调函数中。

 1 // C#2 匿名函数
 2 var request = HttpWebRequest.Create("http://www.google.com");
 3 var result = request.BeginGetResponse(
 4       delegate(IAsyncResult ar_)
 5       {
 6          var response = request.EndGetResponse(ar_);
 7          ProcessData(response);
 8       },
 9       null
10    );
11 
12 // C#3 Limbda
13 result = request.BeginGetResponse( 
14       ar_ => {
15          var response = request.EndGetResponse(ar_);
16          ProcessData(response);
17       },
18       null
19    );

Threadpool与Computing-Bound, I/O-Bound操作

刚开始看CLR via C#中,开始对Computing-Bound operation和IO-Bound operation的区别不是很理解。后来,结合.Net ThreadPool及Window中IO API及thread pool,才弄明白。

首先,我们知道.Net ThreadPool类中的线程分为Worker Thread和I/O Thread。缺省情况下,Worker Thread是CPU*25个,而I/O Thread是1000个。.Net的ThreadPool是基于Windows OS本身提供的Thread Pool的。所以我们先看看Windows本身提供的Thread Pool。

Windows 的Thread Pool (Vista以前)中的线程也分为两类I/O Worker Thread和non-I/O Worker Thread。当我们调用Windows API对I/O如文件进行同步读写时,该线程创建一个IRP的设备请求,并将IRP发送给device stack,然后在核心态等待其完成。而当我们用异步方式时,该线程发送完IRP后,则返回,继续后续的操作。Windows有很多种方式可以通知该I/O操作的完成,与Thread Pool相关的有两种。一种是将完成notification放在该线程的APC队列中,该队列只有当线程进入等待状态是,才会被读取;而另一种方式则是I/O Completion Port,我们可以把这样也认为是个队列,而读取这个队列可以通过GetQueuedCompletionStatus API函数。Windows的Thread Pool的分类就是根据读取不同的队列。I/O Worker Thread读取的是APC队列,也就是当线程完成一个任务后,会进入等待状态;而non-I/O Worker Thread则对应的读取I/O Completion Port队列。APC相比于I/O Completion Port存在很多问题,且性能较差,但是为了向后兼容还是不得不支持。因而在.Net中,只封装了I/O Completion Port,也就是.Net中的I/O Thread对于Windows中的non-I/O Worker Thread。呵呵,希望我还清醒。而APC对于的I/O Worker Thread则没有.Net Thread Pool对应的。

所以在.Net线程池中,I/O Thread实际就是I/O完成端口,而Worker Thread可以看成.Net通过Thread类预先创建的一组线程。.Net及ThreadPool类中提供的方法,如QueueUserWorkItem, Timer, delegate回调等使用的都是Worker Thread。而.Net中对I/O操作的封装,如FileStream, NetworkStream等则是使用的IO Thread。

让我们在回头看CLR via C#中提到的计算约束Computing-Bound和I/O约束I/O-Bound的操作。当我们调用FileStream.BeginRead读文件时,BeginRead并没有创建新的线程去执行读操作,读操作(IRP)被设备执行,同时该线程继续执行其他任务。而当设备完成读操作后,线程池(IO Thread)中的一个线程开始执行回调函数。

而当我们执行的是Computing-Bound的操作时,开始我们就新创建一个线程或通过ThreadPool.QueueUserWorkItem使用线程池中的线程来执行操作,任务完成后,这个线程会执行回调函数。

通过上面的分析,我们可以看到真的不同操作类型,Computing-Bound vs I/O-Bound,类库实现的方式是不同。当然,我们也可以异步Computing-Bound的方式来同步调用FileStream.Read,但是这样我们就没有利用I/O完成端口这个高性能的特性。

本来想这篇就想写如果使用PowerThreading来实现APM的,限于篇幅,就该在下篇吧。希望上面的内容对大家有帮助

参考:

《CLR via C#》第23章 by Jeffery Richard

posted @ 2008-09-08 22:23 Nullnoid 阅读(2329) 评论(2) 编辑

2008年8月6日

上一篇中我们分析了Win32和WinForm编写GUI应用程序会面对的主要问题。总结下来最重要的就是:如何高效的从Worker线程中更新界面。所以首先让我们看看WPF中是如何达到这个目的的。

DispatcherObject的使用

DispatcherObject类有两个成员方法,CheckAccess和VerifyAccess。CheckAccess功能和WinForm中Control.InvokeRequired属性相同,当调用线程与对象的创建线程不是同一个实例时,返回False。VerifyAccess则是抛出异常。

DispatcherObject还有一个Dispatcher的属性,返回控件的创建Dispatcher。通过其Invoke方法,我们可以将界面更新的操作Marshal到控件的创建线程。如下例:

1 TextBox textbox = new TextBox(); 
2 if (!textbox.CheckAccess) 
3 
4   textbox.Dispatcher.Invoke(DispatcherPriority.Send, delegate { }); 
5 

CheckAccess()的实现方法类似于上篇的最后一个例子,通过比较两个线程实例,因而速度很快,只需要几个IL指令。继承DispatcherObject的类,在内部都调用VerifyAccess来做判断,因而当非创建线程调用时,就会抛出异常。这样的好处是我们可以较早的发现代码的问题,不像WinForm那样不确定的发生。

好了,下面我们来看看真正做事情的Dispatcher类,在这之前让我们再回顾一个Win32的消息循环。

Win32 消息循环

在Win32中,消息Pump的建立是通过循环的调用GetMessage,当收到WM_Quit类型的消息是,退出。而我们对窗口或控件属性的修改,如颜色,位置都实际都是调用PostMessage或SendMessage。当调用线程与窗口的Owner线程是同一个,则SendMessage直接调用窗口函数;如不同则将消息放置在窗口对应的消息队列中,由GetMessage取出并进行处理。WinForm实际是建立在Win32的基础上,所以当我们写lable.Text = "Hello World"时,实际是调用了PostMessage等方法。

下面我们看看Win32消息队列的特点和限制:

  1. 任劳任怨的从消息队列中拿出消息,并调用对应的窗口处理函数
  2. 当消息Post进队列后就无法控制了
  3. 消息的处理支持有限的优先级,但都由OS控制,用户代码不能控制和改变优先级
  4. Application.DoEvents只有在处理完消息队列中所有消息后才返回,当然Win32没有这个限制,因为Message Pump就是用户创建的
  5. 可以添加钩子,偷偷的干些坏事

WPF中的Dispatcher的主要功能类似于Win32中消息队列,当并不是替换消息循环,而是建立在消息循环之上。首先,让我们看看Dispatcher的结构:

线程相关的Dispatcher的创建

当我们在一个线程上第一次创建WPF对象时,DispatcherObject构造函数会调用Dispatcher.CurrentDispatcher赋值给私有变量_dispatcher。Dispatcher.CurrentDispatcher通过Thread.CurrentThread来找到该线程对应的Dispatcher实例,如不存在则创建一个。Dispatcher的构造函数,首先创建了一个具有11个级优先级(DispatcherPriority)的优先队列PriorityQueue,这个优先队列用来保存不同优先级的操作。然后调用Win32 API RegisterClassEx注册窗口处理函数,并调用CreateWindowEx创建一个不可见的窗口。

Dispatcher有两个静态方法用来建立消息循环,定义如下:

1 public sealed class Dispatcher 
2 
3   public static void Run() 
4   { 
5     PushFrame(new DispatcherFrame()); 
6   }
7   public static void PushFrame(DispatcherFrame frame) {…} 
8 

尽管是静态方法,实际上是调用当前线程对应的Dispatcher实例的方法。Application.Run()内部实际就是调用了Dispatcher.Run(),进而调用Dispatcher.PushFrame(…)。当我们创建一个WPF项目时,VS自动生成的App.xaml,编译后的代码为

1 public static void Main() 
2 
3   WpfHello.App app = new WpfHello.App(); 
4   app.InitializeComponent(); 
5   app.Run(); 
6 

当调用PushFrame()或Run()后,内部开始循环调用Win32 GetMessage方法,而建立消息循环。如下面的例子中,在VS缺省的WPF项目中,创建了一个新的线程,在该线程中创建一个新的WPF窗口,并启动消息循环:

 1 public partial class App : Application
 2 {
 3    protected override void OnStartup(StartupEventArgs e)
 4    {
 5       // Create a new thread 
 6       Thread monitorThread = new Thread(QueueMonitor.ThreadMain);
 7       monitorThread.SetApartmentState(ApartmentState.STA);
 8       monitorThread.Start(this);
 9 
10       base.OnStartup(e);
11    }
12 }
13 
14 class QueueMonitor
15 {
16    public static void ThreadMain(Object obj)
17    {
18       QueueMonitor monitor = new QueueMonitor(obj as Application);
19    }
20 
21    private Application m_target;
22    private DispatcherFrame    m_frame;
23    private MonitorWindow      m_window;
24    public QueueMonitor(Application target)
25    {
26       m_target = target;
27 
28       m_frame = new DispatcherFrame();
29       m_window = new MonitorWindow();
30 
31       m_window.Closed += MonitorWindow_Closed;
32       m_window.Visibility = Visibility.Visible;
33 
34       Dispatcher.Run();
35       // Dispatcher.PushFrame(m_frame);
36    }
37 
38    void MonitorWindow_Closed(object sender, EventArgs e)
39    {
40       Dispatcher.ExitAllFrames();
41       // m_frame.Continue = false;
42    }
43 }

消息循环的控制

上个例子中,我们也可以使用PushFrame方法。相比于Run(),我们可以通过PushFrame的参数DispatcherFrame对象来控制什么时候退出消息循环。如需退出当前层的循环,只需将对应的DispatcherFrame.Continue赋值false。我们可以嵌套的调用PushFrame,这点上类似Application.DoEvents,更具灵活性,但我们仍不能只要求处理特定优先级的消息。

另外可以调用Dispatcher.ExitAllFrames方法退出当前线程的所有消息循环。Dispatcher还提供一个方法DisableProcessing,该方法可以暂停消息循环处理消息。下面是这个方法的使用方法:

1 using (Dispatcher.DisableProcessing()) 
2 
3   Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Render, delegate { }); 
4 

该方法返回的DispatcherProcessingDisabled对象实现了IDisposable接口,在Dispose中恢复消息循环。该方法也可以嵌套的调用。

下面我们来看看如何投递消息,及Dispatcher如何处理的。

Dispatcher优先队列

Dispatcher的Invoke和BeginInvoke方法,用来向Dispatcher的优先队列放置任务,前者是同步方法,后者是异步。这两个方法,第二个参数是个delegate对象,代表要执行的任务。第一个参数定义任务的优先级,具体定义可以看DispatcherPriority枚举类型。总的来说,我们可以划分为两类,Background和Foreground,另一个比较特殊的是DispatcherPriority.Send。

当在Invoke中指定Send时,且CheckAccess为真,则Invoke直接调用delegate执行任务。如果不是,则调用BeginInvoke,并等待结果,或超时。

BeginInvoke执行时,首先将该任务封装成DispatcherOperation对象,并放置在对应的优先队列中。然后判断是Background还是Foreground,如果是Foreground,则调用PostMessage往Win32消息队列中投递一条消息,然后返回。如果是Background,则检查是否有Timer,如没有则创建一个Timer,Timer会不断循环的投递消息到Win32消息队列,来触发消息的处理。当有Foreground消息是,则删掉Timer。通过这种方式,系统在空闲的时候可以处理Background消息。

当有Win32消息投递到Win32队列时,注册的窗口函数被执行,从优先队列中取出一个DispatcherOperation来执行。完成后,则投递新的Win32消息来触发下次执行,或等待Timer消息。

BeginInvoke的返回值则为DispatcherOperation对象,通过她我们可以取消,等待,或者调整该任务的优先级。在后面的系列中,我们在具体看不同Foreground优先级的使用。

优先队列钩子Hook

与Win32类似,我们也可以对消息的处理添加Hook,可以添加下面的Event Handler:

1 Dispatcher.CurrentDispatcher.Hooks.OperationAborted += new DispatcherHookEventHandler(Hooks_OperationAborted); 
2 Dispatcher.CurrentDispatcher.Hooks.OperationCompleted += 
3 Dispatcher.CurrentDispatcher.Hooks.OperationPosted += 
4 Dispatcher.CurrentDispatcher.Hooks.OperationPriorityChanged +=  

Event Handler的参数中,我们可以获得对应的Dispatcher和DispatcherOperation对象。通过这种办法,我们可以过滤,查询或改变任务的优先级等操作。

DispatcherTimer

DispatcherTimer类似WinForm中的Timer。我们可以在构造函数中指定优先级,Dispatcher实例等等。

总结

总的来说,与Win32比较,如果给WPF中的线程和消息循环的机制打分的话,我觉得可以打4分的高分。WPF解决了Win32中没有优先级,跨线程调用性能的问题,友好的编程接口。如果说不足的话,如果PushFrame可以支持优先级,对Reentrancy的问题,可能能更好的控制;另外,DispatcherOperation无法获得名字,如我们要开发一个队列监视程序的话很不方便。这些我认为可以扣0.5分,那另外0.5分是什么呢?

最后,如果我们看这些类的话,全部都在Windowsbase.dll中,也就是说System.Windows.Threading中的类,如DispatcherObject,Dispatcher也可以用于其他系统中。我们甚至可以利用这些在我们的系统中。

posted @ 2008-08-06 23:47 Nullnoid 阅读(3774) 评论(7) 编辑

2008年8月3日

用Reflector分析WPF时,发现几乎所有的类的继承自DispatcherObject类。而该类非常简单,只有CheckAccess和VerifyAccess两个方法,一个Dispatcher只读属性和一个私有成员变量。正是由于继承了这个类,使WPF中其他类具有了多线程处理的能力。

在分析DispatcherObject前,我们先看看一般Win32或WinForm GUI应用程序中存在的问题和解决办法。

"没有响应":Message Pump被阻塞

使用Windows时,最常碰到的问题就是窗口没有响应的。OS发现一个窗口一定时间内没有处理消息队列时,OS就会显示该窗口"没有响应"。

消息队列是OS为窗口创建的一个线程相关的内部结构。所有对窗口的操作,如鼠标移到,键盘输入等,最终都是在该队列中添加了一条消息。一般我们称创建主窗口的线程为UI线程,该线程创建Message Pump,负责不断的从消息队列中读取消息事件,并执行相应的窗口函数。

在Win32中,是通过调用GetMessage或PeekMessage来读取队列中的消息,并执行窗口处理函数WinProc。WinFom是对Win32 API封装,Application.Run()建立了Message Pump,从队列中读取消息,并调用对应的Event Handler。正是由于Windows这样的设计,如果Event Handler耗时很长时,UI线程无法继续处理消息队列中的其他消息(如WM_Paint),从而窗口没有响应或更新。这就是GUI应用程序中常碰到的Message Pump被阻塞的问题。

如下面的例子,在Button.Click Event Handler中,我们从数据库或文件中读出数据,并将每条记录加入List列表中。由于Message Pump被阻塞,UI线程无法处理WM_Paint消息,因而List只有在所以数据都加入后,才会更新显示。

 1 private void button1_Click(object sender, EventArgs e)
 2 
 3    // Clear the list
 4    listBox1.Items.Clear(); 
 5    
 6    // Get data from database or file 
 7    for (Int32 i = 0; i < 100000; i++)
 8    { 
 9      // Add the data to list 
10      listBox1.Items.Add(i); 
11    }
12 }

解决办法之一:Application.DoEvents

既然问题是由于没有处理消息队列造成的,那么比较直接的一个办法就是调用GetMessage或PeekMessage。WinForm中对于的函数就是Application.DoEvents()方法。

 1 private void button1_Click(object sender, EventArgs e)
 2 
 3    // Clear the list
 4    listBox1.Items.Clear(); 
 5    
 6    // Get data from database or file 
 7    for (Int32 i = 0; i < 100000; i++)
 8    { 
 9      // Add the data to list 
10      listBox1.Items.Add(i); 
11      Application.DoEvents(); 
12    }
13 }

似乎这样就解决问题了,但是上面的方法实际上引入了一个更严重的问题:Reentrancy。

运行上面的程序,我们先点击一次按钮,button1_Click被调用,listBox1.Items.Clear()清空列表,然后开始添加数据到列表。如果这个时候,我们再次点击按钮,一个Button Click消息被加入到消息队列。在没有调用Application.DoEvents时,这条新的消息只有在button1_Click处理完后,才会被调用。这样尽管界面有段时间没有响应,但list中的数据仍能保持完整。当我们加入Application.DoEvents()后,在第一次处理button1_Click过程中,新的消息再次被处理,button1_Click第二次被调用,同样listBox1被清空。但是由于第一次的button1_Click还执行完,后面的Add动作仍会在第二次button1_Click都执行完成后执行,List就变成了1,2,3,4,5,6,4,5,6。而这种行为造成程序不稳定性,有时候数据是好的,有时候又有重复的数据。

造成Reentrancy问题原因是由于DoEvents会处理消息队列中的所有消息,如果DoEvents能够提供只处理WM_Paint事件的话,就不会有这个问题。但是由于消息队列结构的设计不完全支持优先级,所以Win32或WinForm没法解决这个问题。

解决办法之二:拆分复杂的操作

另一个解决办法就是把复杂的操作拆分成很多小的操作,每次只执行一部分。这种办法主要有两个问题:不易拆分或增加了代码的难度,如何调用这些小的操作。如上面的例子中,我们在button1_Click中,我们创建一个System.Windows.Forms.Timer对象,在Tick事件中来添加list的Item。

这种办法有比较大的局限,而且同样会存在Reentrancy的问题。

解决办法之三:多线程

在看多线程前,让我们看看创建一个线程的消耗:

  1. 创建Thread核心对象(128K?),保留1M的Thread Stack地址空间
  2. CPU Task Manager多一个Task
  3. 线程切换非常耗时

是的,如果是单CPU的机器,多线程就意味着性能降低,因为CPU增加了额外的工作。我们需要多线程的原因就是因为UI线程必须能够释放出来去处理消息队列。

在Win32开发中,我们可以调用CreateThread来创建一个线程(Worker线程),来处理耗时的操作,在.Net中对应的就是创建一个Thread对象。这种办法的问题是,每次创建一个Thread,OS都会创建Thread对象,而使用完后该Thread就会被销毁。创建销毁线程对象都是耗时的工作。.Net提供了线程池,可以重复利用线程来提高性能。我们可以用ThreadPool类,或者Delegate的BeginInvoke来使用.Net线程池。

于是,上面的代码很自然的就改为:

 1 private void button1_Click(object sender, EventArgs e)
 2 {
 3    ThreadPool.QueueUserWorkItem(new WaitCallback(DoWork));
 4 }
 5 
 6 private void DoWork(Object state)
 7 {
 8    listBox1.Items.Clear();
 9    for (Int32 i = 0; i < 10000; i++)
10    {
11       listBox1.Items.Add(i);
12    }
13 }

于是编译执行上面的代码,噢,没有问题。但是当你发布给用户时,用户就会抱怨,这个程序有时候突然就没了,于是噩梦就开始了。造成这个问题的原因就是由于Worker线程访问了控件,窗口等GDI对象。为了帮助找到这种问题,在用VS2008调试上面程序时,程序会报Cross-Operation的异常。但是,在Release版本中,程序就会崩溃,并且没有好的工具或方法能够找到这个问题。所以,在设计代码时,就一定注意避免Worker线程访问控件。

为什么Windows会有这样的限制呢?我想这个主要是由于性能的考虑。如果Windows GDI对象要支持多线程的访问,则每次访问时都需要lock来保证数据的完整性,不然界面的行为就会很怪异,每次GDI操作都判断lock的话,界面将会非常非常的慢。如果不用lock的方式,而采用判断Thread.ID的方式,那么每个对象都必须在创建时有个成员变量来保存Thread ID。在Windows 32的时代,内存是很宝贵的资源,所以Windows选择了忽略这个问题也是可以理解的。

那么,我们如何可以解决这个问题呢?在WinForm中,定义了一个ISynchronizeInvoke的接口,Control实现了这个接口。该接口中InvokdRequired属性,用来判断调用线程是否是Control的创建线程,而BeginInvoke和Invoke方法,这是会发消息到Control对应的消息队列,来告诉UI线程来更新Control。于是,上面的代码就变成:

 1 public Form1()
 2 {
 3    InitializeComponent();
 4    updateList = new UpdateUICallBack(UpdateList);
 5 }
 6 
 7 private void button1_Click(object sender, EventArgs e)
 8 {
 9    ThreadPool.QueueUserWorkItem(new WaitCallback(DoWork));
10 }
11 
12 private delegate void UpdateUICallBack(Int32 value);
13 private UpdateUICallBack updateList;
14 private void DoWork(Object state)
15 {
16    for (Int32 i = 0; i < 10000; i++)
17    {
18       updateList(i);
19    }
20 }
21 
22 private void UpdateList(Int32 value)
23 {
24    if (listBox1.InvokeRequired)
25       listBox1.Invoke(updateList, new Object[] { value});
26    else
27       listBox1.Items.Add(value);
28 }

一切似乎很完美了,当在Worker线程上调用updateList时,listBox1.InvokeRequried返回True,调用listBox1.Invoke(),Worker线程会等待Invoke()返回,而Invoke()则会调用Win32 API SendMessage()给UI线程的消息队列,UI线程再次调用UpdateList更新ListBox后返回。

当然,如果我们不想Worker线程被阻塞的话,可以通过调用BeginInvoke()和EndInvoke()的异步方法。不过,不管那种方式,代码都会变得比较难看。而且,尽管InvokeRequired是个属性,实际上,会执行很多操作。InvokeRequired拿到调用线程的ID,然后尝试去得到Control的创建线程ID。而得到Control的创建线程ID会是一个耗时的操作。因为Control创建时并没有保存线程ID到成员变量中。所以InvokeRequired会遍历Control的父Control,一层层,直到找到最外面的父窗口。因而InvokeRequired是一个耗时的操作。

为了解决InvokeRequired的性能问题,我们可以在创建窗口的时候,保存一个Thead的引用,然后再来判断,于是代码变为:

 1 public partial class Form1 : Form
 2 {
 3    private Thread ownerThread;
 4    public Form1()
 5    {
 6       InitializeComponent();
 7       updateList = new UpdateUICallBack(UpdateList);
 8       ownerThread = Thread.CurrentThread;
 9    }
10 
11    private Boolean CheckAccess()
12    {
13       return ownerThread == Thread.CurrentThread;
14    }
15 
16    private void VerifyAccess()
17    {
18       if (!CheckAccess())
19          throw new Exception("Invoke Needed");
20    }
21 
22    private void UpdateList(Int32 value)
23    {
24       if (!CheckAccess())
25          listBox1.Invoke(updateList, new Object[] { value });
26       else
27          listBox1.Items.Add(value);
28    }
29 }

 

好了,上面差不多分析了当前WinForm或Win32 GDI编程的主要问题,下一个Post,让我们来看看WPF是如何解决这些问题的。

 

posted @ 2008-08-03 19:06 Nullnoid 阅读(3810) 评论(6) 编辑

2008年8月2日

摘要: 最近工作需要,开始学习WPF,查看了一些资料,把学习心得写下来希望对大家有帮助。WPF开发于WinForm之后,从技术发展的角度,WPF比WinForm先进是不容置疑的。我觉得WPF相比于WinForm有下面的一些较好的特性:解决Window Handle问题在Windows GDI或WinForm开发中复杂的GUI应用程序,会使用的大量的控件,如Grid等。而每个控件或Grid cell都是一个...阅读全文

posted @ 2008-08-02 22:35 Nullnoid 阅读(8960) 评论(2) 编辑