Dieser Artikel wurde maschinell übersetzt.

Windows mit C++

Threadpool-Zeitgeber und E/A

Kenny Kerr

Kenny KerrIn dieser Hinsicht meine letzte Rate im Windows 7-Threadpool, werde ich zur Deckung der beiden verbleibenden Rückruf generierenden Objekten von der API bereitgestellt. Es gibt noch mehr, ich könnte schreiben, Informationen über den Threadpool, aber nach fünf Artikel, die praktisch alle Funktionen abdecken, sollten Sie bequem es benutzen, um Ihre Anwendungen effektiv und effizient macht sein.

In meine August (msdn.microsoft.com/magazine/hh335066) und November (msdn.microsoft.com/magazine/hh547107) Spalten beschriebenen arbeiten und Objekte bzw. warten. Ein Arbeitsobjekt erlaubt Ihnen, Arbeit in Form einer Funktion direkt an den Threadpool zur Ausführung einzugeben. Die Funktion wird zum frühestmöglichen Zeitpunkt auszuführen. Ein Warteobjekts erzählt den Threadpool warten ein Synchronisierungsobjekt für Kernel in Ihrem Namen, und eine Funktion in eine Warteschlange, wenn es signalisiert wird. Dies ist eine skalierbare Alternative zu traditionellen Synchronisierungsprimitiven und eine effiziente Alternative zum Abrufen. Allerdings gibt es viele Fälle, in denen Timer benötigt, um einige Code nach einem bestimmten Intervall oder einige regelmäßige Zeitpunkt ausgeführt. Dies möglicherweise wegen mangelnder ein "Push" Unterstützung in einigen Web-Protokoll oder vielleicht, weil Sie ein Kommunikationsprotokoll UDP-Stil implementieren und Sie Retransmissionen behandeln müssen. Glücklicherweise bietet der Threadpool-API ein Timer-Objekt mit all diesen Szenarios eine effiziente und jetzt bekannte umzugehen.

Zeitgeberobjekte

Die CreateThreadpoolTimer-Funktion erstellt ein Timer-Objekt. Wenn die Funktion erfolgreich ist, gibt es einen undurchsichtigen-Zeiger, der das Timer-Objekt darstellt. Wenn dies fehlschlägt, gibt einen null-Zeiger-Wert zurück und enthält weitere Informationen über die GetLastError-Funktion. Angesichts ein Timer-Objekts, informiert die CloseThreadpoolTimer Funktion dem Threadpool, dass das Objekt freigegeben werden kann. Wenn Sie in der Serie zusammen verfolgt haben, sollte dies alles ziemlich vertraut klingen. Hier ist eine Eigenschaften-Klasse, die mit der handlichen Unique_handle Klassenvorlage verwendet werden kann, ich in meinem Artikel vom Juli 2011 eingeführt (msdn.microsoft.com/magazine/hh288076):

struct timer_traits
{
  static PTP_TIMER invalid() throw()
  {
    return nullptr;
  }
  static void close(PTP_TIMER value) throw()
  {
    CloseThreadpoolTimer(value);
  }
};
typedef unique_handle<PTP_TIMER, timer_traits> timer;

Ich kann jetzt die Typedef verwenden und erstellen Sie ein Timer-Objekt wie folgt:

void * context = ...
timer t(CreateThreadpoolTimer(its_time, context, nullptr));
check_bool(t);

Wie üblich, der letzte Parameter optional akzeptiert einen Zeiger auf eine Umgebung so dass Sie das Timer-Objekt, mit einer Umgebung zuordnen können, wie ich in meinem Artikel vom September 2011 beschrieben (msdn.microsoft.com/­Magazin/hh416747). Der erste Parameter ist die Callback-Funktion, die in den ThreadPool eingereiht wird jedes Mal, wenn der Zeitgeber abläuft. Der Timer-Rückruf wird wie folgt deklariert:

void CALLBACK its_time(PTP_CALLBACK_INSTANCE, void * context, PTP_TIMER);

Um zu steuern, wann und wie oft der Zeitgeber abläuft, verwenden Sie die SetThreadpoolTimer-Funktion. Natürlich, seine erste Parameter liefert das Timer-Objekt, aber der zweite Parameter gibt den zu gegebener Zeit an dem der Zeitgeber ablaufen soll. Es verwendet eine FILETIME-Struktur, um absolute oder relative Zeit beschreiben. Wenn Sie nicht ganz sicher, wie dies funktioniert, empfehle ich Ihnen, Artikel des letzten Monats, zu lesen, wo ich die Semantik rund um die FILETIME-Struktur im Detail beschrieben. Hier ist ein einfaches Beispiel, wo ich der Zeitgeber ablaufen in fünf Sekunden festgelegt:

union FILETIME64
{
  INT64 quad;
  FILETIME ft;
};
FILETIME relative_time(DWORD milliseconds)
{
  FILETIME64 ft = { -static_cast<INT64>(milliseconds) * 10000 };
  return ft.ft;
}
auto due_time = relative_time(5 * 1000);
SetThreadpoolTimer(t.get(), &due_time, 0, 0);

Wieder, wenn Sie nicht sicher sind sind, wie die Relative_time-Funktion funktioniert, lesen Sie bitte meine November 2011-Spalte. In diesem Beispiel läuft der Zeitgeber nach fünf Sekunden, an welcher Stelle der Threadpool eine Instanz der Its_time-Rückruffunktion Warteschlange wird. Wenn nicht gehandelt wird, werden keine weitere Rückrufe in der Warteschlange werden.

SetThreadpoolTimer können Sie auch um einen periodischen Zeitgeber zu erstellen, der einen Rückruf auf einige regelmäßigen Abständen Warteschlange wird. Hier ist ein Beispiel:

auto due_time = relative_time(5 * 1000);
SetThreadpoolTimer(t.get(), &due_time, 500, 0);

In diesem Beispiel wird die Timer-Rückruf zuerst nach fünf Sekunden und anschließend jede halbe Sekunde danach Warteschlange bis das Timer-Objekt zurücksetzen oder geschlossen ist. Im Gegensatz zu der Zeit ist einfach die Zeit in Millisekunden angegeben. Denken Sie daran, die ein periodischer Zeitgeber nach der festgelegten Frist verstreicht, unabhängig davon, wie lange es, den Rückruf dauert ausgeführt die einen Rückruf Warteschlange wird. Dies bedeutet es ist möglich, dass mehrere Rückrufe gleichzeitig ausgeführt, oder überlappen, wenn das Intervall klein genug ist oder die Rückrufe nehmen eine lange genug Zeit für die Ausführung.

Wenn Sie sicherstellen müssen Rückrufe nicht überlappen, und die genaue Startzeit für jede Periode ist nicht so wichtig, dann ein anderen Ansatz für die Erstellung von einen periodischen Zeitgeber möglicherweise geeignet. Anstatt einen Zeitraum im Aufruf von SetThreadpoolTimer einfach Rücksetzen Sie den Timer in des Rückrufs selbst . Auf diese Weise können Sie sicherstellen, dass die Rückrufe nie überlappen. Wenn nichts anderes, vereinfacht dies die Debuggen. Stellen Sie sich ein Timer-Rückruf in den Debugger nur feststellen, dass der Threadpool bereits in der ein paar weitere Instanzen Warteschlange hat während Sie waren analysieren Ihren Code (oder Ihren Kaffee nachfüllen) durchlaufen. Bei diesem Ansatz wird das nie passieren. Hier ist, was es aussieht:

void CALLBACK its_time(PTP_CALLBACK_INSTANCE, void *, PTP_TIMER timer)
{
  // Your code goes here
  auto due_time = relative_time(500);
  SetThreadpoolTimer(timer, &due_time, 0, 0);
}
auto due_time = relative_time(5 * 1000);
SetThreadpoolTimer(t.get(), &due_time, 0, 0);

Wie Sie sehen können, den ersten fälligen beträgt fünf Sekunden und dann ich den zu gegebener Zeit auf 500 ms am Ende des Rückrufs zurückgesetzt. Ich habe genommen Vorteile aus der Tatsache, dass die Rückruf-Signatur stellt einen Zeiger auf das ursprüngliche Timer-Objekt, was die Arbeit beim Zurücksetzen des Zeitgebers sehr einfach. Sie können auch RAII verwenden, um sicherzustellen, dass der Aufruf von SetThreadpoolTimer zuverlässig aufgerufen wird, bevor der Rückruf zurückgegeben.

Sie können SetThreadpoolTimer aufrufen, mit einem null-Zeiger-Wert für die zu gegebener Zeit alle Ablaufzeiten des künftigen Zeitgeber beenden, die weitere Rückrufe führen kann. Sie müssen auch aufrufen, WaitForThreadpool­TimerCallbacks zu vermeiden Racebedingungen. Natürlich Zeitgeberobjekte funktionieren genauso gut mit Bereinigungsgruppen, wie in meinem Artikel vom Oktober 2011 beschrieben.

SetThreadpoolTimer der letzte Parameter kann ein wenig verwirrend sein, weil die Dokumentation bezieht sich auf eine "Fensterlänge" sowie eine Verzögerung. Was ist das alles? Dies ist tatsächlich eine Funktion, die Energieeffizienz betrifft und Gesamtleistung reduziert Verbrauch. Es basiert auf einer Technik namens Zeitgeber Koaleszenz. Natürlich ist die beste Lösung vermeiden ganz Timer Ereignisse und verwendet stattdessen. Dies ermöglicht die Systemprozessoren die größte Menge von Leerlaufzeit, damit sie zu ermutigen ihre Niederleistungs-Leerlaufs so weit wie möglich eingeben. Noch, wenn Timer erforderlich sind, Zeitgeber Koaleszenz reduzieren die gesamte Leistungsaufnahme durch Verringerung der Zahl der Zeitgeber-Interrupts, die erforderlich sind. Koaleszenz Zeitgeber basiert auf der Idee einer "tolerierbare Verzögerung" für den Zeitgeber Ablaufzeiten. Angesichts einiger erträglich Verzögerung, kann der Windows-Kernel die tatsächlichen Ablaufzeit zeitgleich mit einer vorhandenen Zeitgeber anpassen. Eine gute Faustregel ist, legen Sie die Verzögerung auf ein Zehntel des Programmplanungszeitraums verwendet. Beispielsweise, wenn der Zeitgeber in 10 Sekunden ablaufen soll, verwenden Sie eine Verzögerung von einer Sekunde, je nachdem, was für Ihre Anwendung geeignet ist. Je größer die Verzögerung, unterbricht die weitere Möglichkeit, die der Kernel hat der Zeitgeber zu optimieren. Auf der anderen Seite wird nichts weniger als 50 ms nicht von großem Nutzen, da er beginnt, auf den Kernel Uhr Standardintervall eingreifen.

E/a-Abschlussobjekte

Jetzt es Zeit für mich ist, das Juwel des Threads einzuführen bündeln API: die Eingabe/Ausgabe (e/A)-Abschlussobjekt, oder einfach das I/O-Objekt. Damals, als ich zuerst den Threadpool-API eingeführt, erwähnte ich, dass der Threadpool auf der I/O Abschluss Port API aufbaut. Traditionell war implementieren die am besten skalierbare i/O unter Windows möglich nur mit der I/O Abschluss Port-API. Ich habe in der Vergangenheit über diese API geschrieben. Obwohl nicht besonders schwer zu verwenden, war es nicht immer, dass einfache Integration mit einer Anwendungs andere threading benötigt. Dank der Thread Pool API aber haben Sie das Beste aus beiden Welten mit einer einzigen API für Arbeit, Synchronisation, Timer und jetzt i/O, zu. Ein weiterer Vorteil ist, dass überlappende I/O Abschluss mit dem Threadpool tatsächlich intuitiver ist als mithilfe der I/O Abschluss Port API, vor allem, wenn es darum geht, mehrere Dateihandles und mehrere überlappende Vorgänge gleichzeitig behandeln.

Wie Sie schon erraten haben vielleicht, die CreateThreadpoolIo-Funktion erstellt ein I/O-Objekt und die CloseThreadpoolIo Funktion dem Threadpool informiert, dass das Objekt freigegeben werden kann. Hier ist eine Eigenschaften-Klasse für die Unique_handle-Klassenvorlage:

struct io_traits
{
  static PTP_IO invalid() throw()
  {
    return nullptr;
  }
  static void close(PTP_IO value) throw()
  {
    CloseThreadpoolIo(value);
  }
};
typedef unique_handle<PTP_IO, io_traits> io;

Die CreateThreadpoolIo Funktion akzeptiert ein Dateihandle, was bedeutet, dass ein I/O-Objekt in der Lage, die e/A für ein einzelnes Objekt zu steuern. Natürlich, dass muss das Objekt überlappende e/A zu unterstützen, aber dazu beliebte Resource-Typen wie z. B. Dateisystemdateien gehören, named Pipes, Sockets und so weiter. Lassen Sie mich mit einem einfachen Beispiel warten, um ein UDP-Paket mit einen Socket empfangen zu demonstrieren. Um den Socket zu verwalten, wird Unique_handle mit der folgenden Merkmale-Klasse verwendet:

struct socket_traits
{
  static SOCKET invalid() throw()
  {
    return INVALID_SOCKET;
  }
  static void close(SOCKET value) throw()
  {
    closesocket(value);
  }
};
typedef unique_handle<SOCKET, socket_traits> socket;

Im Gegensatz zu den Eigenschaften-Klassen, die ich bisher gezeigt habe, zurück nicht in diesem Fall die ungültige Funktion einen null-Zeiger-Wert. Dies ist da die WSASocket-Funktion, wie der CreateFile-Funktion, einen ungewöhnlichen Wert verwendet, um ein ungültiges Handle anzugeben. Angesichts dieser Merkmale Klasse und Typedef, kann ich ein Socket und I/O Objekt ganz einfach erstellen:

socket s(WSASocket( ...
, WSA_FLAG_OVERLAPPED));
check_bool(s);
void * context = ...
io i(CreateThreadpoolIo(reinterpret_cast<HANDLE>(s.get()), io_completion, context, nullptr));
check_bool(i);

Die Callback-Funktion, die signalisiert den Abschluss einer i/O-Operation wird wie folgt deklariert:

void CALLBACK io_completion(PTP_CALLBACK_INSTANCE, void * context, void * overlapped,
  ULONG result, ULONG_PTR bytes_copied, PTP_IO)

Die eindeutigen Parameter für diesen Rückruf sollte vertraut, wenn Sie überlappende e/A vor verwendet haben. Da überlappenden e/A von Natur aus asynchron ist und überlappende i/O Operationen – daher der Name überschnitten ich / O — muss es eine Möglichkeit, die bestimmte i/O-Operation zu identifizieren, die abgeschlossen wurde. Dies ist der Zweck des überlappenden-Parameters. Dieser Parameter ermöglicht, dass ein Zeiger auf die OVERLAPPED oder WSAOVERLAPPED Struktur, die wurde angegeben, wenn eine bestimmte i/O-Operation zuerst initiiert wurde. Der traditionelle Ansatz der Verpackung einer OVERLAPPED-Struktur in einer größeren Struktur, mehr Daten aus dieser Parameter hängen kann weiterhin verwendet werden. Der überlappende-Parameter stellt eine Möglichkeit zum Identifizieren der bestimmten i/O-Operation, die abgeschlossen wurde, während der Context-Parameter – wie üblich – stellt einen Kontext für den I/O-Endpunkt, unabhängig von einer bestimmten Operation. Da diese beiden Parameter, sollten Sie keine Probleme Koordinierung des Datenflusses über Ihre Anwendung haben. Der Result-Parameter erfahren Sie, ob die überlappende Operation mit der üblichen ERROR_SUCCESS, oder 0 (null), die Erfolg erfolgreich war. Schließlich erfahren Sie der Bytes_copied-Parameter natürlich wie viele Bytes gelesen tatsächlich geschrieben oder waren. Ein häufiger Fehler ist anzunehmen, dass die Anzahl der angeforderten Bytes tatsächlich kopiert wurde. Machen Sie nicht diesen Fehler: Es ist der Grund für diesen Parameter bestehen.

Das einzige Teil der Threadpool I/O Unterstützung, die etwas problematisch ist ist die Handhabung der I/O Anforderung selbst. Es kümmert sich, um dies richtig zu codieren. Vor dem Aufrufen einer Funktion, einige asynchrone i/O-Operation, wie z. B. ReadFile oder WSARecvFrom, initiieren müssen Sie aufrufen die StartThreadpoolIo-Funktion lassen den Threadpool wissen, dass eine i/O-Operation zu starten. Der Trick ist, wenn die i/O-Operation geschieht synchron abgeschlossen, dann Sie den Threadpool dies mitteilen müssen, durch Aufrufen der CancelThreadpoolIo-Funktion. Denken Sie daran, die I/O Abschluss nicht unbedingt bis zum erfolgreichen Abschluss gleichzusetzen. Eine i/O-Operation möglicherweise erfolgreich sein oder fehlschlagen beide synchron oder asynchron. So oder so, wenn die i/O-Operation nicht Abschlussport seiner Fertigstellung wird müssen Sie den Threadpool wissen lassen. Hier ist, wie dies im Rahmen der Empfang ein UDP-Paket aussehen könnte:

StartThreadpoolIo(i.get());
auto result = WSARecvFrom(s.get(), ...
if (!result)
{
  result = WSA_IO_PENDING;
}
else
{
  result = WSAGetLastError();
}
if (WSA_IO_PENDING != result)
{
  CancelThreadpoolIo(i.get());
}

Wie Sie sehen können, beginne ich den Prozess durch Aufrufen von StartThreadpoolIo an den Threadpool sagen, dass eine i/O-Operation zu beginnen. Anschließend rufen Sie die WSARecvFrom um Dinge geht. Interpretieren das Ergebnis ist die entscheidende Rolle. Die WSARecvFrom-Funktion gibt 0 (null), wenn der Vorgang wurde erfolgreich abgeschlossen, aber den Completion Port weiterhin benachrichtigt werden so dass ich das Ergebnis in WSA_IO_PENDING ändern. Ein anderes Ergebnis von WSARecvFrom gibt Fehler, mit der Ausnahme, natürlich, der WSA_IO_PENDING selbst, was bedeutet, dass der Vorgang wurde erfolgreich initiiert, aber es später abgeschlossen sein wird. Jetzt fordere ich einfach CancelThreadpoolIo, wenn das Ergebnis nicht zu den Threadpool zu beschleunigen aussteht. Verschiedene I/O Endpunkte können unterschiedliche Semantik vorsehen. Datei e/A kann beispielsweise zu vermeiden, den Completion Port auf synchrone Abschluss Benachrichtigung konfiguriert werden. Sie müssten dann CancelThreadpoolIo nach Bedarf aufrufen.

Wie die anderen Rückruf generierenden Objekte in die Threadpool-API können ausstehende Rückrufe für i/O Objekte abgebrochen werden mithilfe der WaitForThreadpoolIoCallbacks-Funktion. Denken Sie daran, dass dies wird alle ausstehenden Rückrufe abzubrechen, jedoch nicht abgebrochen haben alle ausstehenden i/O-Operationen selbst. Sie müssen noch die entsprechende Funktion verwenden Sie zum Abbrechen des Vorgangs Racebedingungen zu vermeiden. Dadurch können Sie sicher keine OVERLAPPED-Strukturen, und so weiter.

Und das ist es für den Threadpool-API. Wie gesagt, es gibt noch mehr ich könnte über dieses leistungsstarke API schreiben, aber angesichts der ausführlichen exemplarischen Vorgehensweise ich habe bisher bereitgestellt, ich bin sicher, dass Sie auch auf Ihrem Weg zu benutzen, um Ihre nächste Anwendung macht. Begleiten Sie mich nächsten Monat, als ich weiterhin Windows mit C++ prüfen.

Kenny Kerr ist ein Software-Handwerker mit einer Leidenschaft für systemeigene Windows-Entwicklung. Sie erreichen ihn in kennykerr.ca.