Dieser Artikel wurde maschinell übersetzt.

Windows mit C++

Zurück in die Zukunft mit fortsetzbaren Funktionen

Kenny Kerr

 

Kenny KerrSchloss ich meine letzte Spalte (msdn.microsoft.com/magazine/jj618294) durch die Hervorhebung einige möglichen Verbesserungen C ++ 11 Futures und Versprechungen, die sie nicht weitgehend akademische und vereinfachend, praktische und nützliche für den Bau von effizienten und kombinationsfähige asynchrone Systeme verwandeln würde. Größtenteils wurde dies durch die Arbeit von Niklas Gustafsson und Artur Laksberg aus dem Visual C++-Team inspiriert.

Als Darstellung der Zukunft der Futures gab ich ein Beispiel in dieser Richtung:

 

int main()
{
  uint8 b[1024];
  auto f = storage_read(b, sizeof(b), 0).then([&]()
  {
    return storage_write(b, sizeof(b), 1024);
  });
  f.wait();
}

Die Storage_read und die Storage_write zurück eine Zukunft, die die entsprechenden i/o-Operationen, die irgendwann in der Zukunft abgeschlossen werden könnte. Diese Funktionen Modell einige Speicher-Subsystem mit 1-KB-Seiten. Das Programm als Ganzes liest die erste Seite aus dem Speicher in den Puffer und kopiert es dann zurück in die zweite Seite des Speichers. Die Neuheit in diesem Beispiel ist die Verwendung der hypothetischen "dann" Methode hinzugefügt, um zukünftige Klasse, sodass die Lese- und Vorgänge in einem einzigen logischen i/o-Operation bestehen, die dann nahtlos auf gewartet werden können.

Dies ist eine große Verbesserung gegenüber der Welt der Stapel zu rippen, die ich in meinem letzten Artikel beschrieben, aber an sich ist noch nicht ganz die Utopie einer Koroutine-ähnliche Einrichtung unterstützt durch die Sprache, die ich in meinem Artikel vom August 2012 "Lightweight kooperative Multitasking mit C++" beschrieben (msdn.microsoft.com/magazine/jj553509). In dieser Spalte ich erfolgreich demonstriert, wie eine solche Anlage mit paar dramatische Makro-Tricks erzielt werden kann — aber nicht ohne erhebliche Nachteile, die hauptsächlich im Zusammenhang mit der Unfähigkeit, lokale Variablen verwenden. Diesen Monat möchte ich teilen einige Gedanken darüber, wie dies in der C++-Sprache selbst erreicht werden könnte.

Ich begann unbedingt diese Serie von Artikeln, die Erforschung alternativer Verfahren zur Erreichung von Parallelität mit eine praktische Lösung, denn die Realität ist, dass wir Lösungen, die heute arbeiten. Wir tun, jedoch muss in die Zukunft blicken und schieben die C++ Gemeinschaft übermitteln fordern mehr Unterstützung für das Schreiben von I/O-intensiven Anwendungen in einer natürlichen und produktiver Weise. Sicherlich sollte hochskalierbare Schriftsysteme der ausschließlichen Zuständigkeit des JavaScript und c#-Programmierer und dem seltenen C++-Programmierer mit genug Willenskraft nicht. Außerdem sollten Sie beachten, dass dies nicht nur um Komfort und Eleganz in der Programmierung, Syntax und Stil. Die Fähigkeit, mehrere I/O Anforderungen zu einem bestimmten Zeitpunkt aktiv hat das Potenzial, die Leistung erheblich verbessern. Speicher- und Netzwerk-Treiber sollen skalieren sowie weitere I/O Anforderungen im Flug sind. Im Falle von Speichertreibern, die Anfragen können kombiniert werden, um Verbesserung der Hardware Pufferung und Reduzierung Suchzeiten. Im Fall der Netzwerktreiber bedeuten mehr Anfragen größer Netzwerkpakete, optimierte gleitende Fensteroperationen und vieles mehr.

Ich werde wechseln Getriebe leicht zu veranschaulichen, wie schnell die Komplexität sein hässliches Haupt erhebt. Anstatt einfach lesen und schreiben zu und von einem Speichergerät, wie etwa serviert Inhalt einer Datei über eine Netzwerkverbindung? Nach wie vor werde ich beginnen mit einem synchronen Ansatz und von dort aus arbeiten. Computer könnten grundsätzlich asynchron sein, aber wir Sterbliche sind sicherlich nicht. Ich weiß nicht wie es euch geht, aber ich habe nie viel von einem Multitalent gewesen. Betrachten Sie folgenden Klassen:

class file { uint32 read(void * b, uint32 s); };
class net { void write(void * b, uint32 s); };

Nutzen Sie Ihre Fantasie den Rest ausfüllen. Ich brauche nur eine Datei-Klasse, die eine bestimmte Anzahl von Bytes aus einer Datei gelesen werden kann. Ich gehe weiter davon aus, dass das Dateiobjekt nachvollziehen den Offset wird. Ebenso könnte die Netto-Klasse einen TCP-Datenstrom Modell, wo der Daten-Offset von TCP per ihre gleitenden Fenster Umsetzung erfolgt, die unbedingt vom Aufrufer versteckt ist. Für eine Vielzahl von Gründen, vielleicht im Zusammenhang mit Zwischenspeichern oder Konflikte lesen die Datei, dass die Methode nicht immer die Anzahl der Bytes, die eigentlich angefragt zurückgeben könnte. Es gibt, jedoch nur 0 zurück, wenn das Ende der Datei erreicht wurde. Net Write-Methode ist einfacher, da die TCP-Implementierung von Design, zum Glück eine riesige Menge an Arbeit, dies für den Anrufer einfach zu halten ist. Dies ist ein einfaches imaginären Szenario aber ziemlich repräsentativ für OS i/o. Ich kann jetzt das folgende einfache Programm schreiben:

int main()
{
  file f = ...; net n = ...; uint8 b[4096];
  while (auto actual = f.read(b, sizeof(b)))
  {
    n.write(b, actual);
  }
}

Angesichts eine 10 KB-Datei, können Sie sich vorstellen die folgende Sequenz von Ereignissen, bevor die Schleife erschöpft ist:

read 4096 bytes -> write 4096 bytes ->
read 4096 bytes -> write 4096 bytes ->
read 2048 bytes -> write 2048 bytes ->
read 0 bytes

Wie das synchrone Beispiel in meinem letzten Artikel ist es nicht schwer herauszufinden, was hier, dank der sequenziellen Natur von C++ geht. Damit der Wechsel zu asynchronen Komposition ist ein bisschen schwieriger. Der erste Schritt ist die Datei und Netzklassen zurückzugebenden Future zu verwandeln:

class file { future<uint32> read(void * b, uint32 s); };
class net { future<void> write(void * b, uint32 s); };

Das war der einfache Teil. Umschreiben der main-Funktion um eine asynchrone in diesen Methoden nutzen, präsentiert ein paar Herausforderungen. Es genügt nicht mehr die Zukunft hypothetische "dann"-Methode, da bin ich nicht mehr einfach den Umgang mit sequentiellen Komposition. Ja, es ist wahr, dass das Schreiben Lesen, aber nur folgt, wenn das Lesen tatsächlich etwas liest. Um die Dinge zu komplizieren folgt eine lesen weiter, auch einen Schreibvorgang in allen Fällen. Sie könnten versucht sein, in Bezug auf die Verschlüsse zu denken, aber dieser Begriff umfasst die Zusammensetzung der Zustand und das Verhalten auch nicht die Zusammensetzung des Verhaltens mit anderen Verhalten.

Ich könnte beginnen mit der Erstellung von Schließungen nur für den Lese- und Schreibvorgänge:

auto read = [&]() { return f.read(b, sizeof(b)); };
auto write = [&](uint32 actual) { n.write(b, actual); };

Natürlich funktioniert das ganze nicht weil die Zukunft dann Methode nicht weiß, was an die Write-Funktion übergeben:

read().then(write);

Um dieses Problem anzugehen, brauche ich eine Art des Übereinkommens, die Zukunft Staat weiterleiten ermöglichen. Eine offensichtliche Wahl ist (vielleicht), die Zukunft selbst zu übermitteln. Die damalige Methode wird dann erwarten, dass einen Ausdruck, die einen zukünftigen Parameter des entsprechenden Typs, dass Sie mir zu schreiben:

auto read = [&]() { return f.read(b, sizeof(b)); };
auto write = [&](future<uint32> previous) { n.write(b, 
  previous.get()); };
read().then(write);

Dies funktioniert, und ich möchten vielleicht sogar Zusammensetzbarkeit weiter verbessern, indem Sie definieren, dass der Ausdruck, den die damalige-Methode erwartet eine Zukunft auch zurückgeben soll. Das Problem bleibt jedoch wie die bedingte Schleife zum Ausdruck zu bringen. Letztlich beweist es einfacher, die ursprüngliche Schleife als Do...while Schleife stattdessen zu überdenken sein, weil dies einfacher, in einem iterativen Weise auszudrücken. Ich könnte dann einen Do_while-Algorithmus, um dieses Muster zu imitieren, asynchron, erarbeiten, bedingt Verkettung Future und bringen die iterative Zusammensetzung zu einem Ende, basierend auf dem Ergebnis eines künftigen <bool> Wert, zum Beispiel:

future<void> do_while(function<future<bool>()> body)
{
  auto done = make_shared<promise<void>>();
  iteration(body, done);
  return done->get_future();  
}

Die Do_while-Funktion erstellt zunächst ein Verweiszählung Versprechen deren ultimative Zukunft die Beendigung der Schleife signalisiert. Dies ist der Iteration-Funktion zusammen mit der Funktion, die innerhalb der Schleife darstellt übergeben:

void iteration(function<future<bool>()> body, 
  shared_ptr<promise<void>> done)
{
  body().then([=](future<bool> previous)
  {
    if (previous.get()) { iteration(body, done); }
    else { done->set_value(); }
  });
}

Diese Iteration-Funktion ist das Herz des Do_while-Algorithmus, Bereitstellung der Verkettung von einem Aufruf zum nächsten sowie die Fähigkeit, ausbrechen und Fertigstellung zu signalisieren. Obwohl es rekursive aussehen könnte, denken Sie daran, dass der springende Punkt ist die asynchronen Operationen aus dem Stapel zu trennen, und so die Schleife nicht tatsächlich im Stapel wächst. Mit dem Do_while-Algorithmus ist relativ einfach, und ich kann jetzt schreiben, das Programm unter Abbildung 1.

Abbildung 1 mit einem Do_while Algorithmus

int main()
{
  file f = ...; net n = ...; uint8 b[4096];
  auto loop = do_while([&]()
  {
    return f.read(b, sizeof(b)).then([&](future<uint32> previous)
    {
      return n.write(b, previous.get());
    }).then([&]()
    {
      promise<bool> p;
      p.set_value(!f.eof);
      return p.get_future();
    });
  });
  loop.wait();
}

Die Do_while-Funktion gibt natürlich eine Zukunft und in diesem Fall ist es gewartet, aber dies könnte auch einfach hätte vermieden werden durch das Speichern von lokalen Variablen der main-Funktion auf dem Heap mit Shared_ptrs. Innerhalb der Lambda-Ausdruck an die Do_while-Funktion übergeben wird beginnt der Lesevorgang, gefolgt von den Schreibvorgang. Um dieses Beispiel einfach zu halten, nehme ich an, die schreiben sofort zurückgegeben wird, wenn es aufgefordert, Null Byte schreiben. Wenn der Schreibvorgang abgeschlossen ist, ich die Datei EOF-Status zu überprüfen und eine Zukunft, die den Schleife-Bedingung-Wert zurückzugeben. Dadurch wird sichergestellt, dass der Körper der Schleife wiederholt wird, bis die den Inhalt der Datei leer ist.

Obwohl dieser Code nicht besonders widerlich ist — und in der Tat ist wohl viel sauberer als Stapel Rippen — ein wenig Unterstützung von der Sprache wäre ein langer Weg zu gehen. Niklas Gustafsson hat bereits solch eine Design vorgeschlagen und nannte es "fortsetzbare Funktionen." Aufbauend auf die Verbesserungen vorgeschlagen für Futures und Versprechungen und ein wenig syntaktischen Zucker hinzufügen, ich könnte Schreiben einer abgebrochenen Funktion überraschend komplexe asynchrone Operation wie folgt zu Kapseln:

future<void> file_to_net(shared_ptr<file> f, 
  shared_ptr<net> n) resumable
{
  uint8 b[4096];
  while (auto actual = await f->read(b, sizeof(b)))
  {
    await n->write(b, actual);
  }
}

Die Schönheit dieses Entwurfs ist, dass der Code hat eine verblüffende Ähnlichkeit mit der Originalversion synchron, und das ist, was ich Suche, nachdem alle. Beachten Sie das "Wiederaufnahme"-Kontextschlüsselwort hinter der Funktion Parameterliste. Dies entspricht dem hypothetischen "Async"-Schlüsselwort, die, das ich in meinem Artikel vom August 2012 beschrieben. Im Gegensatz zu dem, was ich in dieser Spalte gezeigt würde jedoch das vom Compiler selbst implementiert werden. So gäbe es keine Komplikationen und Einschränkungen wie die, die ich mit der Makro-Umsetzung konfrontiert. Switch-Anweisungen und lokale Variablen können — und Konstruktoren und Destruktoren wie erwartet funktionieren würde – aber Ihre Funktionen könnten jetzt anhalten und fortsetzen ähnlich, was ich einen Prototyp mit Makros. Nicht nur das, sondern Sie würde vom die Fallstricke für den Fang von lokaler Variablen nur, um sie außerhalb des Bereichs, ein häufiger Fehler bei der Verwendung von Lambda-Ausdrücken gehen haben befreit werden. Der Compiler würde kümmern Bereitstellen von Speicher für lokale Variablen innerhalb fortsetzbare Funktionen auf dem Heap.

In dem früheren Beispiel Sie auch bemerken das "abwarten"-Schlüsselwort vor das Lesen und Schreiben von Methodenaufrufen. Dieses Schlüsselwort definiert einen Punkt Wiederaufnahme und erwartet einen Ausdruck, wodurch eine Zukunft-wie-Objekt, das sie verwenden kann, um zu bestimmen, ob er anhalten und später fortsetzen oder einfach Ausführung fortführen, wenn der asynchrone Vorgang geschah synchron abgeschlossen. Um die beste Leistung zu erzielen, muss ich natürlich die allzu häufigen Szenario der asynchronen Vorgänge abschließen synchron, vielleicht aufgrund von caching oder schnelle Fehlerfolgen Szenarien behandelt.

Beachten Sie, dass ich sagte, dass das Schlüsselwort Await eine Zukunft-wie-Objekt erwartet. Genaugenommen gibt es keinen Grund, warum es ein tatsächliches zukünftige Objekt sein muss. Es muss nur das notwendige Verhalten um unterstützt die Erkennung von asynchronen Abschluss und Signalisierung bereitzustellen. Dies ist analog zu den Weg heute Vorlagen arbeiten. Dieses Objekt der Zukunft-wie müsste die damalige Methode, die ich in meinem letzten Artikel gezeigt sowie die vorhandenen Get-Methode unterstützen. Zur Verbesserung der Leistung in Fällen wo das Ergebnis sofort verfügbar ist wäre die vorgeschlagene Try_get und Is_done Methoden auch nützlich. Natürlich kann der Compiler optimiert basierend auf der Verfügbarkeit solcher Methoden.

Dies ist nicht so weit hergeholt wie es scheinen mag. C# hat bereits eine nahezu identische Anlage in Form von Async-Methoden, das moralische Äquivalent fortsetzbare Funktionen. Es sieht sogar ein Await-Schlüsselwort, das auf die gleiche Weise funktioniert, wie ich gezeigt habe. Meine Hoffnung ist, dass der C++-Community fortsetzbare Funktionen, oder so ähnlich, umarmen wird, so dass wir alle in der Lage, effizient und kombinationsfähige asynchrone Systeme natürlich und leicht zu schreiben sein werde.

Für eine detaillierte Analyse der abgebrochenen Funktionen, einschließlich ein anschauen, wie sie umgesetzt werden könnte, lesen Sie bitte Niklas Gustafsson Papier, "Resumable Funktionen," am bit.ly/zvPr0a.

Kenny Kerr ist Softwarespezialist mit dem Schwerpunkt auf der systemeigenen Windows-Entwicklung. Sie erreichen ihn unter kennykerr.ca.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Artur Laksberg