MSDN Magazin > Home > Ausgaben > 2007 > June >  Concurrent Affairs: Asynchrone Gerätevorg&...
Concurrent Affairs
Asynchrone Gerätevorgänge
Jeffrey Richter

Codedownload verfügbar unter: ConcurrentAffairs2007_06.exe (162 KB)
Browse the Code Online
In meinem letzten Artikel habe ich demonstriert, wie zwei Basisklassen, AsyncResultNoResult und AsyncResult<TResult>, implementiert werden. Beide Klassen implementieren die IAsyncResult-Schnittstelle, die sich im Kern des CLR-(Common Language Runtime-)APM (Asynchronous Programming Model) befindet. In diesem Artikel werde ich die AsyncResult<TResult>-Klasse verwenden, um das APM auf eine Weise zu implementieren, die es Ihnen ermöglicht, Hardwaregerätevorgänge asynchron durchzuführen. Zusätzlich wird Ihnen dieser Artikel einen Einblick geben, wie Microsoft® .NET Framework selbst asynchrone Datei- und Netzwerk-E/A-Vorgänge durchführt. Der gesamte Code dafür ist in meiner Power Threading-Bibliothek enthalten, die auf der Website wintellect.com zur Verfügung steht.

CreateFile und DeviceIoControl
Wenn Sie in Win32®-basiertem Code mit einer Datei arbeiten möchten, öffnen Sie sie, indem Sie die Win32-Funktion CreateFile aufrufen, und führen danach an dieser Datei Vorgänge durch, indem Sie Funktionen wie beispielsweise WriteFile und ReadFile aufrufen. Wenn Sie mit dem Bearbeiten der Datei fertig sind, schließen Sie die Datei, indem Sie die Funktion „CloseHandle“ aufrufen.
In .NET Framework wird durch das Erstellen eines FileStream-Objekts intern CreateFile aufgerufen. Durch das Aufrufen der FileStream-Methoden Read und Write werden intern die Win32-Funktionen „ReadFile“ bzw. „WriteFile“ aufgerufen. Ähnlich wird durch das Aufrufen der FileStream-Methoden „Close“ oder „Dispose“ intern die Win32-Funktion „CloseHandle“ aufgerufen. Wenn Sie mit Dateien arbeiten, müssen die meisten Anwendungen lediglich Daten in die Datei schreiben oder aus der Datei einlesen, sodass die FileStream-Klasse die meisten Vorgänge bereitstellt, die Sie dafür benötigen.
Windows® bietet jedoch weitaus mehr Vorgänge, die an einer Datei ausgeführt werden können. Für die allgemeineren Vorgänge stellt Windows spezielle Win32-Funktionen wie z. B. WriteFile, ReadFile, FlushFileBuffers und GetFileSize zur Verfügung. Für nur selten verwendete Vorgänge bietet Win32 allerdings keine eigenen Funktionen, sondern stellt stattdessen eine einzige Funktion namens „DeviceIoControl“ bereit, die es einer Anwendung ermöglicht, direkt mit dem Gerätetreiber (z. B. dem NTFS-Festplattentreiber) zu kommunizieren, der für die Handhabung der Datei zuständig ist. Beispiele für selten verwendete Dateivorgänge sind opportunistische Sperren (microsoft.com/msj/0100/win32/win320100.aspx), Manipulationen des Änderungsjournals (microsoft.com/msj/0999/journal/journal.aspx), das Komprimieren von Datenträgervolumes/Dateien, das Erstellen von Verknüpfungspunkten, das Formatieren/Neupartitionieren von Datenträgern und das Arbeiten mit ausgewählten Dateien.
Darüber hinaus kann DeviceIoControl von jeder Anwendung zur Kommunikation mit jedem Hardwaregerätetreiber verwendet werden. Mithilfe von DeviceIoControl kann eine Anwendung den Batteriestatus des Computers abfragen, die Helligkeit der LCD abfragen oder ändern, einen CD- oder DVD-Wechsler manipulieren, USB-Geräte abfragen und auswerfen und vieles mehr.

Win32-Gerätekommunikation
Im Folgenden wird kurz untersucht, wie Win32 einer Anwendung ermöglicht, mit einem Gerät zu kommunizieren und diese Funktionsweise anschließend zu wrappen, damit sie über verwalteten Code verwendet werden kann.
In Win32 wird ein Gerät geöffnet, indem die Funktion „CreateFile“ aufgerufen wird. Das erste Argument von CreateFile ist eine Zeichenfolge, die das Gerät identifiziert, das geöffnet werden soll. Normalerweise wird als Zeichenfolge, die CreateFile zum Öffnen einer Datei verwendet, ein Pfadname angegeben. Sie können an CreateFile aber auch spezielle Zeichenfolgen zum Öffnen eines Geräts übergeben. Abbildung 1 zeigt einige mögliche Zeichenfolgen und beschreibt, welche Arten von Geräten CreateFile mit diesen Zeichenfolgen öffnet. Bedenken Sie dabei, dass einige dieser Geräte auf das System oder auf Mitglieder der Administratorengruppe beschränkt sind und daher bei einigen dieser Geräte der Versuch, sie zu öffnen, scheitern könnte, es sei denn, Ihre Anwendung wird mit den erforderlichen Zugriffsberechtigungen ausgeführt.

An CreateFile übergebene Zeichenfolge Beschreibung
@“\\.\PhysicalDrive1” Öffnet Festplatte 1 und gewährt Ihnen Zugriff auf alle Sektoren dieser Festplatte.
@“\\.\C:” Öffnet das Festplattenvolume C: und ermöglicht Ihnen, das Änderungsjournal dieses Volumes zu manipulieren.
@“\\.\Changer0” Öffnet Datenträgerwechsler 0, um Ihnen zu ermöglichen, Datenträger zu verschieben und weitere Vorgänge durchzuführen.
@“\\.\Tape0” Öffnet Bandlaufwerk 0, um Ihnen zu ermöglichen, Dateien zu sichern und wiederherzustellen.
@“\\.\COM2” Öffnet Kommunikationsport 2, um Ihnen zu ermöglichen, Byte zu senden und zu empfangen.
@“\\.\LCD” Öffnet das LCD-Gerät, um Ihnen zu ermöglichen, es abzufragen und die Helligkeit einzustellen.
Jetzt wissen Sie, wie ein Gerät geöffnet wird. Als Nächstes soll die Win32-Funktion „DeviceIoControl“ genauer betrachtet werden:
BOOL DeviceIoControl(
   HANDLE hDevice,           // Handle returned from CreateFile
   DWORD  dwIoControlCode,   // Operation control code
   PVOID  pInBuffer,         // Address of input buffer
   DWORD  nInBufferSize,     // Size, in bytes, of input buffer
   PVOID  pOutBuffer,        // Address of output buffer
   DWORD  nOutBufferSize,    // Size, in bytes, of output buffer
   PDWORD pBytesReturned,    // Gets number of bytes written to 
                             // pOutBuffer
   POVERLAPPED pOverlapped); // For asynchronous operation
Wenn Sie diese Funktion aufrufen, verweist der Handle-Parameter auf eine Datei, ein Verzeichnis oder einen durch Aufrufen von CreateFile erhaltenen Gerätetreiber. Der Parameter „dwIoControlCode“ gibt an, welcher Vorgang an dem Gerät durchgeführt werden soll. Jeder Vorgang wird einfach durch diesen 32-Bit-Ganzzahlcode identifiziert. Wenn ein Vorgang Argumente erfordert, werden diese Argumente in Feldern einer Datenstruktur abgelegt, und die Adresse dieser Struktur wird im Parameter „pInBuffer“ übergeben. Die Größe der Struktur wird im Parameter „nInBufferSize“ übergeben. Wenn der Vorgang einige Daten zurückgibt, werden in ähnlicher Weise die Ausgabedaten in einem Puffer abgelegt, der vom Aufrufer zugewiesen werden muss. Die Adresse dieses Puffers wird im Parameter „pOutBuffer“ übergeben. Die Größe dieses Puffers wird im Parameter „nOutBufferSize“ übergeben. Wenn DeviceIoControl zurückkehrt, schreibt es die Anzahl der Byte, die tatsächlich in den Ausgabepuffer geschrieben wurden, in den Parameter „pBytesReturned“, der als Verweis übergeben wird. Beim Durchführen synchroner Vorgänge wird NULL im Parameter „pOverlapped“ übergeben. Beim Durchführen eines asynchronen Vorgangs dagegen muss die Adresse einer OVERLAPPED-Struktur übergeben werden. Auf diesen Punkt werde ich in diesem Artikel an späterer Stelle ausführlich eingehen.
Nun, da Sie wissen, wie Sie über Win32 mit Geräten kommunizieren können, werden wir beginnen, einen Wrapper für diese Win32-Funktionen zu schreiben, damit Sie in einer .NET-Sprache verwalteten Code schreiben können, um direkt mit Hardwaregeräten zu kommunizieren.

Synchrone Geräte-E/A in verwaltetem Code
Konzentrieren wir uns zunächst darauf, wie synchrone Gerätevorgänge durchgeführt werden. Später werde ich den Code hinzufügen, der zum Durchführen asynchroner Vorgänge benötigt wird. In C# habe ich eine statische DeviceIO-Klasse definiert, die einen benutzerfreundlichen Wrapper für die Win32-Funktionen „CreateFile“ und „DeviceIoControl“ darstellt. Abbildung 2 zeigt das öffentliche Objektmodell für diese Klasse.
public static class DeviceIO {
   public static SafeFileHandle OpenDevice(String deviceName, 
      FileAccess access, FileShare share, Boolean useAsync);

   public static void Control(SafeFileHandle device, 
      DeviceControlCode deviceControlCode);

   public static void Control(SafeFileHandle device, 
      DeviceControlCode deviceControlCode, Object inBuffer);

   public static TResult GetObject<TResult>(SafeFileHandle device, 
      DeviceControlCode deviceControlCode) where TResult: new();

   public static TResult GetObject<TResult>(SafeFileHandle device,
      DeviceControlCode deviceControlCode, Object inBuffer) 
          where TResult : new();

   public static TElement[] GetArray<TElement>(SafeFileHandle device, 
      DeviceControlCode deviceControlCode, Object inBuffer, 
      Int32 maxElements) where TElement : struct;
}
In einer verwalteten Anwendung können Sie OpenDevice aufrufen, eine ähnliche Zeichenfolge wie die in Abbildung 1 gezeigte Zeichenfolge übergeben sowie den gewünschten Zugriff und die gewünschte Freigabe aufrufen. OpenDevice ruft intern CreateFile auf, um den Handle zum Gerät zurückzugeben. Der Handle wird in ein SafeFileHandle-Objekt gewrappt zurückgegeben, um sicherzustellen, dass er am Ende geschlossen wird, und um die anderen Vorteile zu nutzen, die jede der von SafeHandle abgeleiteten Klassen bietet. Dieses SafeFileHandle-Objekt kann als erstes Argument an jede der anderen statischen Methoden von DeviceIO übergeben werden und wird schließlich als erstes Argument übergeben, wenn die Win32-Funktion „DeviceIoControl“ aufgerufen wird.
Das zweite Argument für alle anderen Methoden ist ein DeviceControlCode, eine einfache Struktur (Werttyp), die nur ein einziges privates Instanzfeld (Int32) enthält, das einen Befehlscode angibt, den Sie einem Gerät senden möchten. Ich finde, das Wrappen des Int32-Befehlscodes in seinen eigenen Typ hat den Vorteil, dass der Code leichter zu lesen und typensicherer ist und von IntelliSense® in Visual Studio® profitiert. Intern wird der in einer DeviceControlCode-Instanz enthaltene Int32-Wert als zweites Argument der Win32-Funktion „DeviceIoControl“ übergeben.
Wenn wir jetzt Gerätesteuercodes untersuchen, werden Sie feststellen, dass es einige gemeinsame Muster gibt, und ich habe beschlossen, benutzerfreundliche Methoden anzubieten, die das Arbeiten mit gemeinsamen Mustern praktisch gestalten. Alle diese Methoden führen den Vorgang synchron aus, d. h., der aufrufende Thread wird erst zurückgegeben, wenn der Vorgang abgeschlossen ist. Zusätzlich müssen Sie beim Aufrufen einer dieser Methoden für das Argument „useAsync“ den Wert „false“ festlegen, wenn Sie OpenDevice aufrufen.
Die Control-Methode kapselt ein Muster ein, bei dem der Steuercode, den Sie an ein Gerät senden, dieses Gerät anweist, einen Vorgang durchzuführen, und das Gerät keine Ergebnisdaten zum Zurückgeben hat. Es gibt eine Control-Methode, die nur einen SafeFileHandle und einen Steuercode verwendet, sowie eine Überladung, die ein zusätzliches Argument „Object“ verwendet. Das zusätzliche Object-Argument ermöglicht Ihnen, dem Gerät einige zusätzliche Daten zu übergeben, die das Gerät für einen Vorgang verwendet. Wenn Sie in der Win32-SDK-Dokumentation Steuercodes nachschlagen, wird in der Dokumentation angegeben, welche zusätzlichen Informationen (falls notwendig) Sie für jeden Steuercode übergeben müssen. Wenn Sie zusätzliche Daten benötigen, müssen Sie einen dem Win32-Datentyp entsprechenden verwalteten Typ definieren, eine Instanz dieses Win32-Datentyps erstellen, ihre Felder initialisieren und im Parameter „inBuffer“ einen Verweis auf die Instanz übergeben.
Die Methode „GetObject<TResult>“ kapselt ein Muster ein, in dem der Steuercode, den Sie an ein Gerät senden, dieses Gerät anweist, Ihrer Anwendung einige Daten zurückzugeben. Um diese Rückgabedaten abzurufen, müssen Sie einen dem Win32-Datentyp entsprechenden verwalteten Typ definieren und diesen Typ beim Aufrufen von GetObject als den generischen Typ „TResult“ angeben. Intern wird GetObject eine Instanz dieses Typs erstellen (aus diesem Grund ist mit dem generischen Typ die neue Beschränkung verknüpft), das Gerät wird die Felder innerhalb des Aufrufs an DeviceIoControl initialisieren, und GetObject wird das initialisierte Objekt wieder an Sie zurückgeben. Beim Aufrufen von GetObject gibt es zwei Überladungen, die es Ihnen ermöglichen, optional über den Parameter „inBuffer“ dem Gerät zusätzliche Daten zu übergeben.
Die Methode GetArray<TElement> kapselt das letzte Muster ein, bei dem der Steuercode, den Sie an ein Gerät senden, dieses Gerät anweist, Ihrer Anwendung ein Array von Elementen zurückzugeben. Um diese Elemente abzurufen, müssen Sie einen dem Win32-Element entsprechenden verwalteten Typ definieren und diesen Typ beim Aufrufen von GetArray als den generischen Typ „TElement“ angeben. Der verwaltete Datentyp muss als Struktur (Werttyp) definiert werden, damit der Speicher des Arrays linear angelegt wird, wie dies DeviceIoControl erfordert. Daher ist TElement auf eine Struktur beschränkt. Zusätzlich müssen Sie beim Aufrufen von GetArray über den Parameter „maxElements“ die maximale Anzahl der Elemente angeben, die vom Gerät zurückgegeben werden sollen.
Intern erstellt GetObject aus diesen Elementen ein Array und übergibt die Adresse des Arrays an DeviceIoControl, das dann die einzelnen Arrayelemente initialisiert. Wenn DeviceIoControl zurückkehrt, gibt es die Anzahl der Byte zurück, die tatsächlich in das Array gestellt wurden. GetArray verwendet diesen Wert, um nötigenfalls das Array auf die exakte Größe zu schrumpfen, damit die Anzahl der Elemente im Array der Anzahl der initialisierten Elemente entspricht. Dies ist sehr praktisch, da jeder Code, der dieses von GetArray zurückgegebene Array verwendet, ganz einfach die Length-Eigenschaft des Arrays abfragen kann, um die Anzahl der Elemente zu erhalten, und diesen Wert in seiner Schleife verwenden kann, um die zurückgegebenen Elemente zu verarbeiten.
Abbildung 3 zeigt das verwaltete Äquivalent zur Win32-Struktur „DisplayBrightness“ und seinem Hilfsobjekt „DisplayPowerFlags“ (enumerierter Typ). Abbildung 4 zeigt eine Methode namens „AdjustBrightness“, die meine statische DeviceIO-Klasse dazu verwendet, die Helligkeit Ihrer LCD konstant anzupassen. Dies ist zugegebenermaßen nicht sehr praktisch in der Anwendung, sondern nur ein einfaches Beispiel, um die Grundkonzepte zu veranschaulichen.
public static void AdjustBrightness(6) {
   // The code for setting LCD brightness; obtained by looking up 
   // IOCTL_VIDEO_SET_DISPLAY_BRIGHTNESS in Platform SDK documentation
   DeviceControlCode s_SetBrightness =
      new DeviceControlCode(DeviceType.Video, 0x127, 
         DeviceMethod.Buffered, DeviceAccess.Any);

   // Open the device
   using (SafeFileHandle lcd = DeviceIO.OpenDevice(@”\\.\LCD”, 
      FileAccess.ReadWrite, FileShare.ReadWrite, false)) {

      for (Int32 times = 0; times < 10; times++) {
         for (Int32 dim = 0; dim <= 100; dim += 20) {
            // Initialize the equivalent of a Win32 DISPLAY_BRIGHTNESS 
            // structure
            DisplayBrightness db = 
               new DisplayBrightness(DisplayPowerFlags.ACDC, 
                  (Byte)((times % 2 == 0) ? dim : 100 - dim));

            // Tell the LCD device to adjust its brightness
            DeviceIO.Control(lcd, s_SetBrightness, db);

            // Sleep for a bit and then adjust it again
            Thread.Sleep(150);
         }
      }
   }
}
[Flags] 
internal enum DisplayPowerFlags : byte {
   None = 0x00000000, AC = 0x00000001, DC = 0x00000002, ACDC = AC | DC
}

internal struct DisplayBrightness {
   public DisplayPowerFlags m_Power;
   public Byte m_ACBrightness; // 0-100
   public Byte m_DCBrightness; // 0-100

   public Win32DisplayBrightnessStructure(
         DisplayPowerFlags power, Byte level) {
      m_Power = power;
      m_ACBrightness = m_DCBrightness = level;
   }
}

Asynchrone Geräte-E/A in verwaltetem Code
Da einige Gerätevorgänge, wie beispielsweise das Ändern der LCD-Helligkeit, keine echten E/A-Vorgänge sind, ist es sinnvoll, für diese Arten von Vorgängen DeviceIoControl synchron aufzurufen. Die meisten Aufrufe an DeviceIoControl haben jedoch E/A-Vorgänge (daher der Name dieser Funktion) zur Folge, und deshalb ist es sinnvoll, diese Vorgänge asynchron durchzuführen. In diesem Abschnitt wird erläutert, wie in verwaltetem Code P/Invoke für die Win32-Funktion „DeviceIoControl“ verwendet wird, um asynchrone E/A-Vorgänge an Dateien, Datenträgern und anderen Hardwaregeräten durchzuführen.
Meine statische DeviceIO-Klasse stellt mithilfe des APM von CLR asynchrone Versionen der Methoden „Control“, „GetObject“ und „GetArray“ bereit. Abbildung 5 zeigt die bisher noch nicht vorgestellten öffentlichen Methoden dieser Klasse, die APM unterstützen.
public static class DeviceIO {
   public static IAsyncResult BeginControl(SafeFileHandle device,
      DeviceControlCode deviceControlCode, Object inBuffer,
      AsyncCallback asyncCallback, Object state);

   public static void EndControl(IAsyncResult result);

   public static IAsyncResult BeginGetObject<TResult>(
      SafeFileHandle device, DeviceControlCode deviceControlCode, 
      Object inBuffer, AsyncCallback asyncCallback, Object state) 
      where TResult: new();

   public static TResult EndGetObject<TResult>(IAsyncResult result)
      where TResult: new();

   public static IAsyncResult BeginGetArray<TElement>(
      SafeFileHandle device, DeviceControlCode deviceControlCode, 
      Object inBuffer, Int32 maxElements, AsyncCallback asyncCallback, 
      Object state) where TElement: struct;

   public static TElement[] EndGetArray<TElement>(IAsyncResult result) 
      where TElement: struct;
}
Wie Sie sehen können, folgen alle BeginXxx-Methoden dem Muster des APM von CLR, d. h., alle diese Methoden geben ein IAsyncResult zurück, und die beiden letzten Parameter sind ein AsyncCallback-Parameter und ein Object-Parameter. Die zusätzlichen Parameter entsprechen den Parametern der synchronen Entsprechung jeder Methode. In ähnlicher Weise akzeptieren alle EndXxx-Methoden einen einzelnen Parameter (IAsyncResult), und jede Methode gibt denselben Datentyp wie ihre synchrone Entsprechung zurück.
Um eine dieser asynchronen Methoden aufzurufen, müssen zwei Schritte erfolgen. Zunächst müssen Sie Windows mitteilen, dass Sie Vorgänge an dem Gerät asynchron durchführen möchten. Dies erreichen Sie, indem Sie der Funktion „CreateFile“ das Kennzeichen „FILE_FLAG_OVERLAPPED“ übergeben, das den numerischen Wert 0x40000000 hat. Die verwaltete Entsprechung dieses Kennzeichens ist FileOptions.Asynchronous (und hat natürlich ebenfalls den Wert 0x40000000). Danach müssen Sie den Gerätetreiber anweisen, Vorgangsabschlusseinträge in den Threadpool von CLR zu einzufügen. Dies wird durch Aufrufen der statischen ThreadPool-Methode „BindHandle“ erreicht, die einen einzigen Parameter annimmt, bei dem es sich um einen Verweis auf ein von Safehandle abgeleitetes Objekt handelt.
Die DeviceIO-Methode „OpenDevice“ führt beide dieser Aktionen durch, wenn Sie in ihrem Parameter „useAsync“ den Wert „true“ übergeben. Meine Methode „OpenDevice“ wird folgendermaßen implementiert:
public static SafeFileHandle OpenDevice(String deviceName, 
   FileAccess access, FileShare share, Boolean useAsync) {

   SafeFileHandle device = CreateFile(deviceName, access, share, 
      IntPtr.Zero, FileMode.Open, 
      useAsync ? FileOptions.Asynchronous : FileOptions.None, 
      IntPtr.Zero);

   if (device.IsInvalid) throw new Win32Exception();
   if (useAsync) ThreadPool.BindHandle(device);

   return device;
}
Sehen wir uns jetzt an, wie BeginGetObject und EndGetObject intern funktionieren. Die anderen asynchronen Methoden funktionieren ähnlich und Sie werden ihre Funktionsweise leicht verstehen, sobald Sie die Funktionsweise von BeginGetObject und EndGetObject gesehen haben. Alle BeginXxx-Methoden sind sehr einfach: sie erstellen ein Objekt oder ein Array, wenn von dem Vorgang ein Wert oder ein Array von Elementen zurückgegeben wird, und rufen dann sofort eine interne Hilfsmethode (AsyncControl) auf, die wie in Abbildung 6 gezeigt implementiert wird.
private static DeviceAsyncResult<T> AsyncControl<T>(
    SafeFileHandle device, 
   DeviceControlCode deviceControlCode, Object inBuffer, T outBuffer, 
   AsyncCallback asyncCallback, Object state) {

   SafePinnedObject inDeviceBuffer = new SafePinnedObject(inBuffer);
   SafePinnedObject outDeviceBuffer = new SafePinnedObject(outBuffer);
   DeviceAsyncResult<T> asyncResult = new DeviceAsyncResult<T>(
      inDeviceBuffer, outDeviceBuffer, asyncCallback, state);

   unsafe {
      Int32 bytesReturned;
      NativeControl(device, deviceControlCode, inDeviceBuffer, 
          outDeviceBuffer, out bytesReturned, 
          asyncResult.GetNativeOverlapped());
   }
   return asyncResult;
}
Beim Durchführen eines Vorgangs können Sie an DeviceIoControl die Adresse eines Puffers übergeben, in dem zusätzliche Eingabedaten enthalten sind. Wenn DeviceIoControl Daten zurückgibt, muss der Speicher für diese Daten zugewiesen werden, bevor DeviceIoControl aufgerufen wird, damit DeviceIoControl die Daten initialisieren kann. Danach werden diese Daten untersucht, wenn der Vorgang abgeschlossen wird. Das Problem besteht darin, dass es mehrere Stunden dauern könnte, bis der Vorgang abgeschlossen ist, und während dieser Zeiten Freispeichersammlungen auftreten könnten, die die Daten verschieben würden. Dies hätte zur Folge, dass die an DeviceIoControl übergebenen Adressen nicht mehr auf die gewünschten Puffer verweisen würden und daher eine Speicherbeschädigung eintreten würde.
Sie können GC (Garbage Collection) anweisen, ein Objekt nicht zu verschieben, indem Sie das Objekt befestigen. Meine SafePinnedObject-Klasse ist eine Hilfsklasse, die ein Objekt im Speicher befestigt, um zu verhindern, dass es bei der GC verschoben wird. Da jedoch die SafePinnedObject-Klasse von SafeHandle abgeleitet ist, bietet sie alle Vorteile, die von Safehandle abgeleitete Typen normalerweise mit sich bringen, einschließlich der Gewissheit, dass die Befestigung des Objekts später wieder aufgehoben wird. Abbildung 7 vermittelt Ihnen eine Vorstellung davon, wie die SafePinnedObject-Klasse implementiert wird (ich habe einen Teil des Überprüfungscodes entfernt, um Platz zu sparen). Weitere Informationen zu von SafeHandle abgeleiteten Typen und zum Befestigen von Objekten im Speicher finden Sie in meinem Buch „CLR via C#“ (Microsoft Press®, 2006).
public sealed class SafePinnedObject : SafeHandleZeroOrMinusOneIsInvalid {
   private GCHandle m_gcHandle;  // Handle of pinned object (or 0)

   public SafePinnedObject(Object obj) : base(true) {
      // If obj is null, we create this object but it pins nothing
      if (obj == null) return;

      // If specified, pin the buffer and save its memory address
      m_gcHandle = GCHandle.Alloc(obj, GCHandleType.Pinned);
      SetHandle(m_gcHandle.AddrOfPinnedObject());
   }

   protected override Boolean ReleaseHandle() {
      SetHandle(IntPtr.Zero); // Just for safety, set the address to null
      m_gcHandle.Free();      // Unpin the object
      return true;
   }

   // Returns the object of a pinned buffer or null if not specified
   public Object Target {
      get { return IsInvalid ? null : m_gcHandle.Target; }
   }

   // Returns the number of bytes in a pinned object or 0 if not specified
   public Int32 Size {
      get {
         Object target = Target;

         // If obj was null, return 0 bytes
         if (target == null) return 0;

         // If obj is not an array, return the size of it
         if (!target.GetType().IsArray) return Marshal.SizeOf(target);

         // obj is an array, return the total size of the array
         Array a = (Array)target;
         return a.Length * Marshal.SizeOf(a.GetType().GetElementType());
      }
   }
}
Nach dem Befestigen des Eingabe- und des Ausgabepuffers erstellt AsyncControl ein DeviceAsyncResult<TResult>-Objekt und übergibt ihm die beiden SafePinnedObject-Objekte „AsyncCallback“ und „Object“. DeviceAsyncResult<TResult> ist ein weiterer interner Typ meiner Implementierung. Dieser Typ ist, wie in meinem letzten Artikel beschrieben, von AsyncResult<TResult> abgeleitet, was bedeutet, dass dieser Typ die IAsyncResult-Schnittstelle von APM implementiert. Sobald es erstellt wurde, speichert das DeviceAsyncResult-Objekt einfach alle ihm übergebenen Argumente in privaten Feldern und kehrt danach zurück.

Overlapped-Parameter von DeviceIoControl
Nachdem nun diese ganze Vorbereitungsarbeit abgeschlossen ist, ist AsyncControl bereit, DeviceIoControl aufzurufen. Zu diesem Zweck ruft es NativeControl auf und übergibt ihm den Gerätehandle, den Steuercode, den Eingabepuffer, den Ausgabepuffer und einen Verweis zu einer Int32-Variable namens „bytesReturned“, die von der Methode praktisch ignoriert wird, da die Methode zurückkehrt, bevor der Vorgang abgeschlossen ist. Das wichtigste Argument ist das letzte: Beim Durchführen eines asynchronen Vorgangs muss an DeviceIoControl die Adresse einer NativeOverlapped-Struktur (die Entsprechung einer OVERLAPPED-Struktur von Win32) übergeben werden. AsyncControl erreicht dies durch den Aufruf einer Hilfsmethode namens „GetNativeOverlapped“, die in meiner DeviceAsyncResult-Klasse definiert ist. Diese Hilfsmethode wird folgendermaßen implementiert:
// Create and return a NativeOverlapped structure to be passed 
// to native code
public unsafe NativeOverlapped* GetNativeOverlapped() {
   // Create a managed Overlapped structure that refers to our 
   // IAsyncResult (this)
   Overlapped o = new Overlapped(0, 0, IntPtr.Zero, this);

   // Pack the managed Overlapped structure into a NativeOverlapped 
   // structure
   return o.Pack(CompletionCallback, 
      new Object[] { m_inBuffer.Target, m_outBuffer.Target });
}
Diese Methode erstellt eine Instanz der System.Threading.Overlapped-Klasse und initialisiert sie. Dies ist eine verwaltete Hilfsklasse, die Ihnen ermöglicht, eine überlappte Struktur einzurichten und zu manipulieren. Es ist jedoch nicht möglich, an systemeigenen Code einen Verweis auf dieses Objekt zu übergeben. Stattdessen müssen Sie zuerst das Overlapped-Objekt in ein NativeOverlapped-Objekt packen. Anschließend kann ein Verweis auf das resultierende NativeOverlapped-Objekt an systemeigenen Code übergeben werden. Wenn Sie Pack aufrufen, übergeben Sie ihm einen Delegaten, der auf eine Rückrufmethode verweist, die von einem Thread eines Threadpools ausgeführt wird, wenn der Vorgang abgeschlossen wird. In meinem Code handelt es sich dabei um die Methode „CompletionCallback“.
Das Aufrufen der Pack-Methode der Overlapped-Klasse bewirkt mehrere Dinge. Es wird Speicher für eine systemeigene OVERLAPPED-Struktur vom verwalteten Heapspeicher zugewiesen und befestigt, wodurch gewährleistet wird, dass der Speicher nicht verschoben wird, falls eine GC erfolgen sollte. Danach werden die Felder des Feldes der NativeOverlapped-Struktur von den im verwalteten Overlapped-Objekt eingestellten Feldern initialisiert. Dies umfasst das Erstellen eines normalen GCHandle für das IAsyncResult-Objekt, auf das das Overlapped-Objekt verweist, um sicherzustellen, dass das IAsyncResult-Objekt während des gesamten Vorgangs aktiv bleibt.
Pack befestigt anschließend den Delegaten der Rückrufmethode (CompletionCallback), damit die feste Adresse an systemeigenen Code übergeben werden kann, was dem systemeigenen Code den Rückruf an verwalteten Code ermöglicht, sobald dieser Vorgang abgeschlossen ist. Zusätzlich befestigt Pack auch alle weiteren Objekte, auf die durch seinen zweiten Parameter (ein Object-Array) verwiesen wird. In meinem Beispiel sind die Objekte „inBuffer“ und „outBuffer“ bereits befestigt, weil ich sie mithilfe meiner SafePinnedObject-Klasse gewrappt habe. Ich muss sie selbst befestigen, damit ich ihre Adresse abrufen kann, indem ich die Methode „AddrOfPinnedObject“ von GCHandle aufrufe.
Die Methode „Pack“ befestigt sie jedoch erneut, allerdings auf eine spezielle Weise. Normalerweise wird beim Entladen einer AppDomain von CLR automatisch die Befestigung aller Objekte aufgehoben, damit sie gesammelt werden können, um einen Speicherverlust zu vermeiden. Pack jedoch befestigt die Objekte, bis der asynchrone Vorgang abgeschlossen ist. Daher wird beim Starten eines asynchronen Vorgangs und beim anschließenden Entladen der AppDomain die Befestigung der von Pack befestigten Objekte nicht aufgehoben. Sobald der Vorgang abgeschlossen ist, wird die Befestigung der Objekte aufgehoben, damit sie gesammelt werden können. Dadurch wird eine Speicherbeschädigung verhindert.
Pack zeichnet auch auf, von welcher AppDomain Pack aufgerufen wurde, um sicherzustellen, dass der Threadpool-Thread, der die Methode „CompletionCallback“ aufruft, in derselben AppDomain ausgeführt wird. Zum Schluss erfasst Pack den Stapel der Codezugriffssicherheitsberechtigungen, damit die Rückrufmethode unter denselben Sicherheitsberechtigungen ausgeführt wird, unter denen der Code, der den asynchronen Vorgang initiiert hat, ausgeführt wurde. Sie können die Methode „UnsafePack“ der Overlapped-Klasse aufrufen, wenn Sie es vorziehen, den Stapel der Sicherheitsberechtigungen nicht zu verbreiten.
Pack gibt die Adresse der befestigten NativeOverlapped-Struktur zurück, die an jede systemeigene Funktion übergeben werden kann, von der die Adresse zu einer OVERLAPPED-Struktur von Win32 erwartet wird. In meiner Methode „AsyncControl“ wird die Adresse der NativeOverlapped-Struktur an NativeControl übergeben (siehe Abbildung 8), und NativeControl übergibt sie der Win32-Funktion „DeviceIoControl“.
private static unsafe void NativeControl(SafeFileHandle device, 
   DeviceControlCode deviceControlCode, 
   SafePinnedObject inBuffer, SafePinnedObject outBuffer, 
   out Int32 bytesReturned, NativeOverlapped* nativeOverlapped) {

   Boolean ok = DeviceIoControl(device, deviceControlCode.Code,
      inBuffer, inBuffer.Size, outBuffer, outBuffer.Size, 
      out bytesReturned, nativeOverlapped);
   if (ok) return;

   Int32 error = Marshal.GetLastWin32Error();
   const Int32 c_ErrorIOPending = 997;
   if (error == c_ErrorIOPending) return;
   throw new InvalidOperationException(
      String.Format(“Control failed (code={0})”, error));
}
Beim Durchführen eines asynchronen Vorgangs kehrt DeviceIoControl sofort zurück, und zum Schluss wird von der BeginObject<TResult>-Methode von DeviceIO das DeviceAsyncResult-Objekt zurückgegeben, das die IAsyncResult-Schnittstelle implementiert.
Wenn der Vorgang abgeschlossen ist, fügt der Gerätetreiber einen Eintrag im Threadpool von CLR ein. Um es sich kurz ins Gedächtnis zu rufen: Der Treiber kann dies ausführen, weil die Methode „OpenDevice“ von DeviceIO intern die Methode „BindHandle“ des ThreadPools aufruft, wenn das Gerät für asynchronen Zugriff geöffnet wird. Zum Schluss wird ein Threadpool-Thread diesen in die Warteschlange gestellten Eintrag extrahieren, alle gespeicherten Berechtigungssätze übernehmen, in die richtige AppDomain springen und die Methode „CompletionCallback“ aufrufen (siehe Abbildung 9). Denken Sie daran, dass die Methode „CompletionCallback“ innerhalb der DeviceAsyncResult<TResult>-Klasse definiert ist, die von der AsyncResult<TResult>-Klasse abgeleitet ist.
// Called by a thread pool thread when native overlapped I/O completes
private unsafe void CompletionCallback(UInt32 errorCode, UInt32 numBytes,
   NativeOverlapped* nativeOverlapped) {
   // Release the native OVERLAPPED structure and 
   // let the IAsyncResult object (this) be collectable.
   Overlapped.Free(nativeOverlapped);

   try {
      if (errorCode != 0) {
         // An error occurred, record the Win32 error code
         base.SetAsCompleted(new Win32Exception((Int32)errorCode), false);
      } else {
         // No error occurred, the output buffer contains the result
         TResult result = (TResult)m_outBuffer.Target;

         // If the result is an array of values, resize the array 
         // to the exact size so that the Length property is accurate
         if ((result != null) && result.GetType().IsArray) {
            // Only resize if the number of elements initialized in the 
            // array is less than the size of the array itself
            Type elementType = result.GetType().GetElementType();
            Int64 numElements = numBytes / Marshal.SizeOf(elementType);
            Array origArray = (Array)(Object)result;
            if (numElements < origArray.Length) {
               // Create new array (size equals number of initialized 
               // elements)
               Array newArray = Array.CreateInstance(
                   elementType, numElements);

               // Copy initialized elements from original array to new 
               // array
               Array.Copy(origArray, newArray, numElements);
               result = (TResult)(Object)newArray;
            }
         }
         // Record result and call AsyncCallback method passed to BeginXxx 
         // method
         base.SetAsCompleted(result, false);
      }
   }
   finally {
      // Make sure that the input and output buffers are unpinned
      m_inBuffer.Dispose();
      m_outBuffer.Dispose();
      m_inBuffer = m_outBuffer = null;   // Allow early GC
   }
}
Die erste Aktion, die von der Methode „CompletionCallback“ ausgeführt wird, besteht darin, das Objekt „nativeOverlapped“ freizusetzen. Dies ist äußerst wichtig, da es das GCHandle für das Objekt „IAsyncResult“ freigibt, die Befestigung aller Objekte aufhebt, die der Methode „Pack“ der Overlapped-Klasse übergeben wurden, und auch die Befestigung des Objekts „NativeOverlapped“ selbst aufhebt, wodurch dieses Objekt von einer Garbage Collection verarbeitet werden kann. Ein Versäumnis, das Objekt „NativeOverlapped“ freizusetzen, hätte eine Beschädigung verschiedener Objekte zur Folge.
Der Rest des Codes von CompletionCallback ist recht einfach. Es geht dabei nur darum, im Status der AsyncResult<T>-Basisklasse aufzuzeichnen, ob der Vorgang fehlgeschlagen ist, oder das Ergebnis des Vorgangs aufzuzeichnen, wobei die Array-Größe eingestellt wird, falls zum Initiieren des Vorgangs der BeginGetArray von DeviceIO aufgerufen wurde. Im abschließenden Block von CompletionCallback ist Code zum Entsorgen der SafePinnedObject-Objekte enthalten, um sicherzustellen, dass der Eingabepuffer und der Ausgabepuffer nicht mehr befestigt sind, und um zu ermöglichen, dass diese Puffer von der GC komprimiert werden können und der von ihnen eingenommene Speicherplatz schließlich von der GC wieder freigegeben werden kann, wenn die Anwendung diese Puffer nicht mehr benötigt.
An einem bestimmten Punkt wird der Anwendungscode die Methode „EndControl“, „EndGetObject“ oder „EndGetArray“ von DeviceIO aufrufen und das von der entsprechenden Begin-Methode zurückgegebene DeviceAsyncResult<TResult>-Objekt übergeben. Intern rufen alle diese End-Methoden lediglich die Methode „EndInvoke“ von AsyncResult<TResult> auf, die entweder die in ihr eingestellte Ausnahme innerhalb von CompletionCallback auslöst oder den in ihr eingestellten Wert zurückgibt.

Opportunistische Sperren
Eine opportunistische Sperre bildet ein einfaches Beispiel, das Ihnen einen asynchronen Gerätevorgang veranschaulicht. Abbildung 10 zeigt eine statische OpLockDemo-Klasse, deren Methode „Main“ (mithilfe des Kennzeichens „FileOptions.Asynchronous“) eine Datei erstellt. Danach wird das innerhalb von FileStream eingebettete SafeFileHandle an BeginFilter übergeben, das intern die Methode „BeginControl“ von DeviceIO aufruft und den Code übergibt, der für die Datei eine opportunistische Anforderungsfiltersperre einrichtet. Jetzt achtet der Dateisystemgerätetreiber darauf, ob eine andere Anwendung versucht, die Datei zu öffnen, und falls dies der Fall ist, fügt der Gerätetreiber einen Eintrag im Threadpool von CLR ein. Dadurch wird wiederum die anonyme Methode aufgerufen, die das Feld „s_earlyEnd“ auf den Wert 1 einstellt, der angibt, dass eine andere Anwendung auf die Datei zugreifen will. Daraufhin schließt die erste Anwendung die Datei und ermöglicht dadurch der anderen Anwendung, weiterhin mit Zugriff auf die Datei ausgeführt zu werden.
internal static class OpLockDemo {
   private static Byte s_endEarly = 0;

   public static void Main() {
      String filename = Path.Combine(
         Environment.GetFolderPath(Environment.SpecialFolder.Desktop), 
         @”FilterOpLock.dat”);

      // Attempt to open/create the file for processing (must be 
      // asynchronous)
      using (FileStream fs = new FileStream(
             filename, FileMode.OpenOrCreate,
         FileAccess.ReadWrite, FileShare.ReadWrite, 8096,
         FileOptions.Asynchronous)) {

         // Request the Filter Oplock on the file.
         // When another process attempts to access the file, the system 
         // will 
         //    1. Block the other process until we close the file
         //    2. Call us back notifying us that another process wants to 
         // access the file
         BeginFilter(fs.SafeFileHandle, 
            delegate(IAsyncResult result) {
               EndFilter(result);
               Console.WriteLine(“Another process wants to access “ +
                                “the file or the file closed”);
               Thread.VolatileWrite(ref s_endEarly, 1);  // Tell Main 
                                                         // thread to end 
                                                         // early
            }, null);

         // Pretend we’re accessing the file here
         for (Int32 count = 0; count < 100; count++) {
            Console.WriteLine(“Accessing the file ({0})...”, count);

            // If the user hits a key or if another application 
            // wants to access the file, close the file.
            if (Console.KeyAvailable || 
               (Thread.VolatileRead(ref s_endEarly) == 1)) break;
            Thread.Sleep(150);
         }
      }  // Close the file here allows the other process to continue 
         // running
   }

   // Establish a Request filter opportunistic lock
   private static IAsyncResult BeginFilter(SafeFileHandle file, 
      AsyncCallback asyncCallback, Object state) {

      // See FSCTL_REQUEST_FILTER_OPLOCK in Platform SDK’s WinIoCtl.h file
      DeviceControlCode RequestFilterOpLock =
         new DeviceControlCode(DeviceType.FileSystem, 23, 
            DeviceMethod.Buffered, DeviceAccess.Any);

      return DeviceIO.BeginControl(file, RequestFilterOpLock, null, 
         asyncCallback, state);
   }

   private static void EndFilter(IAsyncResult result) { 
      DeviceIO.EndControl(result); 
   }
}
Um all dies zu testen, starten Sie die Anwendung „OpLockDemo“. Gehen Sie dann zu Ihrem Desktop, und versuchen Sie, die von der Anwendung erstellte Datei „FilterOpLock.dat“ zu löschen. Dadurch wird die anonyme Methode aufgerufen und die Beispielanwendung beendet.

Schlussbemerkung
Windows und seine Gerätetreiber stellen viele Funktionen bereit, die von der Klassenbibliothek von .NET Framework nicht gewrappt werden können. Glücklicherweise stellt .NET Framework jedoch geeignete Mechanismen bereit, die es Ihnen ermöglichen, über P/Invoke auf diese nützlichen Funktionen zuzugreifen. Die Tatsache, dass Sie diese Vorgänge asynchron durchführen können, bedeutet, dass Sie solide, zuverlässige und skalierbare Anwendungen entwickeln können, die diese Funktionen nutzen.

Senden Sie Ihre Fragen und Kommentare für Jeffrey Richter an  mmsync@microsoft.com.


Jeffrey Richter ist Mitbegründer von Wintellect, einer Firma zur Prüfung von Softwarearchitekturen sowie für Beratung und Schulung. Er ist Autor verschiedener Bücher, einschließlich CLR via C# (Microsoft Press, 2006). Jeffrey Richter schreibt auch redaktionelle Beiträge für das MSDN Magazine und ist seit 1990 als Berater für Microsoft tätig.

Page view tracker