.Net中的异步编程模式 (APM) (三): 如何实现支持APM的设备操作

前一篇中我们看到通过使用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 {  get private   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,  null null null ));
41 
42                          ar.SetAsCompleted( null false );
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

你可能感兴趣的:(.net)