Abbruch in der PPL

In diesem Thema wird die Rolle des Abbruchs in der Parallel Patterns Library (PPL) erläutert und beschrieben, wie Sie die parallele Verarbeitung abbrechen und wie Sie ermitteln, wann eine Aufgabengruppe abgebrochen wird.

Abschnitte

  • Parallele Arbeitsstrukturen

  • Abbrechen von parallelen Aufgaben

  • Abbrechen von parallelen Algorithmen

  • Wann ein Abbruch nicht verwendet werden sollte

Parallele Arbeitsstrukturen

Differenzierte Aufgaben und Berechnungen werden in der PPL mithilfe von Aufgabengruppen verwaltet. Sie können Aufgabengruppen schachteln, um Strukturen paralleler Arbeitsvorgänge zu bilden. Die folgende Abbildung zeigt eine parallele Arbeitsstruktur. In dieser Abbildung stellen tg1 und tg2 Aufgabengruppen und t1, t2, t3, t4 und t5 Aufgaben dar.

Parallele Arbeitsstruktur

Das folgende Beispiel zeigt den Code, der zum Erstellen der Struktur in der Abbildung erforderlich ist. tg1 und tg2 sind in diesem Beispiel Concurrency::structured_task_group-Objekte, und t1, t2, t3, t4 und t5 sind Concurrency::task_handle-Objekte.

// task-tree.cpp
// compile with: /c /EHsc
#include <ppl.h>
#include <sstream>
#include <iostream>
#include <sstream>

using namespace Concurrency;
using namespace std;

void create_task_tree()
{   
   // Create a task group that serves as the root of the tree.
   structured_task_group tg1;

   // Create a task that contains a nested task group.
   auto t1 = make_task([&] {
      structured_task_group tg2;

      // Create a child task.
      auto t4 = make_task([&] {
         // TODO: Perform work here.
      });

      // Create a child task.
      auto t5 = make_task([&] {
         // TODO: Perform work here.
      });

      // Run the child tasks and wait for them to finish.
      tg2.run(t4);
      tg2.run(t5);
      tg2.wait();
   });

   // Create a child task.
   auto t2 = make_task([&] {
      // TODO: Perform work here.
   });

   // Create a child task.
   auto t3 = make_task([&] {
      // TODO: Perform work here.
   });

   // Run the child tasks and wait for them to finish.
   tg1.run(t1);
   tg1.run(t2);
   tg1.run(t3);
   tg1.wait();   
}

[Nach oben]

Abbrechen von parallelen Aufgaben

Es gibt zwei Möglichkeiten, parallele Arbeitsvorgänge abzubrechen. Sie können die Concurrency::task_group::cancel-Methode oder die Concurrency::structured_task_group::cancel-Methode aufrufen. Oder Sie lösen im Text einer Arbeitsfunktion eine Ausnahme aus.

Die Verwendung einer cancel-Methode ist hier effizienter als die Ausnahmebehandlung. Mit der cancel-Methode wird eine Aufgabengruppe mit allen untergeordneten Aufgabengruppen abgebrochen, wobei mit der obersten Ebene begonnen wird (Top-Down-Ansatz). Bei der Ausnahmebehandlung wird dagegen die umgekehrte Reihenfolge verwendet (Bottom-Up-Ansatz), sodass jede untergeordnete Aufgabengruppe einzeln abgebrochen werden muss.

In den folgenden Abschnitten wird beschrieben, wie Sie parallele Arbeitsvorgänge mit der cancel-Methode und mit Ausnahmebehandlung abbrechen. Weitere Beispiele zum Abbrechen paralleler Aufgaben finden Sie unter Gewusst wie: Verwenden eines Abbruchs zum Verlassen einer Parallel-Schleife und Gewusst wie: Verwenden der Ausnahmebehandlung zum Verlassen einer Parallel-Schleife.

Abbrechen paralleler Aufgaben mit der cancel-Methode

Mit den Methoden Concurrency::task_group::cancel und Concurrency::structured_task_group::cancel wird der Zustand Canceled für eine Aufgabengruppe festgelegt.

Tipp

Die Laufzeit implementiert Abbrüche mithilfe der Ausnahmebehandlung. Diese Ausnahmen dürfen im eigenen Code nicht abgefangen oder behandelt werden. Außerdem empfiehlt es sich, ausnahmesicheren Code in den Funktionsrümpfen der Aufgaben zu schreiben. Zum Beispiel können Sie das RAII (Resource Acquisition Is Initialization)-Muster verwenden, um sicherzustellen, dass Ressourcen ordnungsgemäß behandelt werden, wenn eine Ausnahme im Rumpf einer Aufgabe ausgelöst wird. Ein vollständiges Beispiel, in dem das RAII-Muster verwendet wird, um in einer abbrechbaren Aufgabe eine Ressource zu bereinigen, finden Sie unter Exemplarische Vorgehensweise: Entfernen von Arbeit aus einem Benutzeroberflächenthread.

Nach dem Aufruf der cancel-Methode startet die Aufgabengruppe keine neuen Aufgaben mehr. Die cancel-Methoden können von mehreren untergeordneten Aufgaben aufgerufen werden. Wenn eine Aufgabe abgebrochen wurde, geben die Concurrency::task_group::wait-Methode und die Concurrency::structured_task_group::wait-Methode Concurrency::canceled zurück.

Die cancel-Methode wird nur auf die jeweils untergeordneten Aufgaben angewendet. Wenn Sie z. B. die Aufgabengruppe tg1 in der Abbildung der parallelen Arbeitsstruktur abbrechen, sind alle Aufgaben in der Struktur (t1, t2, t3, t4 und t5) betroffen. Wenn Sie die geschachtelte Aufgabengruppe tg2 abbrechen, sind dagegen nur die Aufgaben t4 und t5 betroffen.

Wenn Sie die cancel-Methode aufrufen, werden auch alle untergeordneten Aufgabengruppen abgebrochen. Dies gilt jedoch nicht für die übergeordneten Elemente der Aufgabengruppe in der parallelen Arbeitsstruktur. In den folgenden Beispielen wird dies auf Grundlage der Abbildung oben veranschaulicht.

Im ersten Beispiel wird eine Arbeitsfunktion für die Aufgabe t4 erstellt, die der Arbeitsgruppe tg2 untergeordnet ist. Die Arbeitsfunktion ruft die Funktion work in einer Schleife auf. Wenn ein Aufruf von work fehlschlägt, wird die übergeordnete Aufgabengruppe der Aufgabe abgebrochen. Hierdurch geht die Aufgabengruppe tg2 in den Zustand "abgebrochen" über, die Aufgabengruppe tg1 wird jedoch nicht abgebrochen.

auto t4 = make_task([&] {
   // Perform work in a loop.
   for (int i = 0; i < 1000; ++i)
   {
      // Call a function to perform work.
      // If the work function fails, cancel the parent task
      // and break from the loop.
      bool succeeded = work(i);
      if (!succeeded)
      {
         tg2.cancel();
         break;
      }
   }         
});

Dieses zweite Beispiel ähnelt dem ersten, mit dem Unterschied, dass die Aufgabe hier die Aufgabengruppe tg1 abbricht. Dadurch sind alle Aufgaben in der Struktur betroffen (t1, t2, t3, t4 und t5).

auto t4 = make_task([&] {
   // Perform work in a loop.
   for (int i = 0; i < 1000; ++i)
   {
      // Call a function to perform work.
      // If the work function fails, cancel all tasks in the tree.
      bool succeeded = work(i);
      if (!succeeded)
      {
         tg1.cancel();
         break;
      }
   }   
});

Die structured_task_group-Klasse ist nicht threadsicher. Daher erzeugt eine untergeordnete Aufgabe, die eine Methode des übergeordneten structured_task_group-Objekts aufruft, ein nicht spezifiziertes Verhalten. Die structured_task_group::cancel-Methode und die Concurrency::structured_task_group::is_canceling-Methode sind Ausnahmen von dieser Regel. Eine untergeordnete Aufgabe kann diese Methoden aufrufen, um die übergeordnete Aufgabe abzubrechen und nach einem Abbruch zu suchen.

Abbrechen paralleler Aufgaben mit Ausnahmen

Im Thema Ausnahmebehandlung in der Concurrency Runtime wird erläutert, wie die Concurrency Runtime über Ausnahmen Fehler meldet. Nicht alle Ausnahmen geben jedoch einen Fehler an. Ein Suchalgorithmus kann z. B. die zugeordnete Aufgabengruppe abbrechen, wenn das Ergebnis gefunden wurde. Wie jedoch bereits erwähnt ist die Ausnahmebehandlung im Vergleich zur cancel-Methode die weniger effiziente Möglichkeit zum Abbrechen paralleler Aufgaben.

Wenn Sie im Text einer Arbeitsfunktion, die Sie an eine Aufgabengruppe übergeben, eine Ausnahme auslösen, speichert die Runtime diese Ausnahme und marshallt sie an den Kontext, der auf das Beenden der Aufgabengruppe wartet. Wie auch bei der cancel-Methode verwirft die Runtime alle Aufgaben, die noch nicht gestartet wurden, und akzeptiert keine neuen Aufgaben.

Dieses dritte Beispiel ähnelt dem zweiten, mit dem Unterschied, dass die Aufgabe t4 eine Ausnahme auslöst, um die Aufgabengruppe tg2 abzubrechen. In diesem Beispiel wird ein try-catch-Block verwendet, um nach einem Abbruch zu suchen, wenn die Aufgabengruppe tg2 auf das Beenden der untergeordneten Aufgaben wartet. Wie im ersten Beispiel geht die Aufgabengruppe tg2 in den Zustand "abgebrochen" über, die Aufgabengruppe tg1 ist jedoch nicht betroffen.

structured_task_group tg2;

// Create a child task.      
auto t4 = make_task([&] {
   // Perform work in a loop.
   for (int i = 0; i < 1000; ++i)
   {
      // Call a function to perform work.
      // If the work function fails, throw an exception to 
      // cancel the parent task.
      bool succeeded = work(i);
      if (!succeeded)
      {
         throw exception("The task failed");
      }
   }         
});

// Create a child task.
auto t5 = make_task([&] {
   // TODO: Perform work here.
});

// Run the child tasks.
tg2.run(t4);
tg2.run(t5);

// Wait for the tasks to finish. The runtime marshals any exception
// that occurs to the call to wait.
try
{
   tg2.wait();
}
catch (const exception& e)
{
   wcout << e.what() << endl;
}

In diesem vierten Beispiel wird mithilfe der Ausnahmebehandlung die gesamte Arbeitsstruktur abgebrochen. Im Beispiel wird die Ausnahme abgefangen, wenn Aufgabengruppe tg1 auf das Beenden der untergeordneten Aufgaben wartet, nicht tg2. Wie im zweiten Beispiel gehen so beide Aufgabengruppen in der Struktur (tg1 und tg2) in den Zustand "abgebrochenen" über.

// Run the child tasks.
tg1.run(t1);
tg1.run(t2);
tg1.run(t3);   

// Wait for the tasks to finish. The runtime marshals any exception
// that occurs to the call to wait.
try
{
   tg1.wait();
}
catch (const exception& e)
{
   wcout << e.what() << endl;
}

Da die task_group::wait-Methode und die structured_task_group::wait-Methode ausgelöst werden, wenn eine untergeordnete Aufgabe eine Ausnahme auslöst, geben diese keinen Rückgabewert aus.

Bestimmen des Zeitpunkts für das Abbrechen

Abbruchvorgänge sind kooperativ. Daher erfolgen sie nicht unmittelbar. Wenn eine Aufgabengruppe abgebrochen wird, kann jeder Laufzeitaufruf durch eine untergeordnete Aufgabe einen Unterbrechungspunkt auslösen, der die Runtime veranlasst, einen internen Ausnahmetyp zum Abbrechen aktiver Aufgaben auszulösen und abzufangen. Die Concurrency Runtime definiert keine bestimmten Unterbrechungspunkte. Diese können in jedem Aufruf der Runtime auftreten. Die Runtime muss die ausgelösten Ausnahmen behandeln, um den Abbruch durchzuführen. Behandeln Sie daher keine unbekannten Ausnahmen im Text einer Aufgabe.

Wenn eine untergeordnete Aufgabe einen zeitaufwändigen Vorgang ausführt und die Runtime nicht aufruft, muss sie regelmäßig nach einem Abbruch suchen und rechtzeitig beendet werden können. Das folgende Beispiel zeigt eine Möglichkeit zur Bestimmung des Abbruchzeitpunkts. Die Aufgabe t4 bricht die übergeordnete Aufgabengruppe ab, wenn sie auf einen Fehler stößt. Die Aufgabe t5 ruft regelmäßig die structured_task_group::is_canceling-Methode auf, um nach einem Abbruch zu suchen. Wenn die übergeordnete Aufgabengruppe abgebrochen wird, druckt die Aufgabe t5 eine Meldung und wird beendet.

structured_task_group tg2;

// Create a child task.
auto t4 = make_task([&] {
   // Perform work in a loop.
   for (int i = 0; i < 1000; ++i)
   {
      // Call a function to perform work.
      // If the work function fails, cancel the parent task
      // and break from the loop.
      bool succeeded = work(i);
      if (!succeeded)
      {
         tg2.cancel();
         break;
      }
   }
});

// Create a child task.
auto t5 = make_task([&] {
   // Perform work in a loop.
   for (int i = 0; i < 1000; ++i)
   {
      // To reduce overhead, occasionally check for 
      // cancelation.
      if ((i%100) == 0)
      {
         if (tg2.is_canceling())
         {
            wcout << L"The task was canceled." << endl;
            break;
         }
      }

      // TODO: Perform work here.
   }
});

// Run the child tasks and wait for them to finish.
tg2.run(t4);
tg2.run(t5);
tg2.wait();

In diesem Beispiel wird bei jedem hundertsten Durchlauf der Aufgabenschleife nach einem Abbruchvorgang gesucht. Die Häufigkeit, mit der nach einem Abbruch gesucht wird, hängt vom Arbeitsaufwand der Aufgabe ab und davon, wie schnell die Reaktion der Aufgaben auf den Abbruch sein soll.

Wenn Sie keinen Zugriff auf die übergeordnete Aufgabengruppe haben, rufen Sie die Concurrency::is_current_task_group_canceling-Funktion auf, um zu ermitteln, ob die übergeordnete Aufgabengruppe abgebrochen wird.

[Nach oben]

Abbrechen von parallelen Algorithmen

Parallele Algorithmen in der PPL, z. B. Concurrency::parallel_for, basieren auf Aufgabengruppen. Daher können Sie die meisten Techniken für Aufgabengruppen auch zum Abbrechen paralleler Algorithmen verwenden.

In den folgenden Beispielen werden mehrere Möglichkeiten zum Abbrechen eines parallelen Algorithmus gezeigt.

Im folgenden Beispiel wird die Concurrency::structured_task_group::run_and_wait-Methode verwendet, um den parallel_for-Algorithmus aufzurufen. Die structured_task_group::run_and_wait-Methode wartet auf das Beenden der angegebenen Aufgabe. Das structured_task_group-Objekt aktiviert die Arbeitsfunktion zum Abbrechen der Aufgabe.

// To enable cancelation, call parallel_for in a task group.
structured_task_group tg;

task_group_status status = tg.run_and_wait([&] {
   parallel_for(0, 100, [&](int i) {
      // Cancel the task when i is 50.
      if (i == 50)
      {
         tg.cancel();
      }
      else
      {
         // TODO: Perform work here.
      }
   });
});

// Print the task group status.
wcout << L"The task group status is: ";
switch (status)
{
case not_complete:
   wcout << L"not complete." << endl;
   break;
case completed:
   wcout << L"completed." << endl;
   break;
case canceled:
   wcout << L"canceled." << endl;
   break;
default:
   wcout << L"unknown." << endl;
   break;
}

Folgende Ergebnisse werden zurückgegeben:

The task group status is: canceled.

Im folgenden Beispiel wird eine parallel_for-Schleife mithilfe der Ausnahmebehandlung abgebrochen. Die Runtime marshallt die Ausnahme an den aufrufenden Kontext.

try
{
   parallel_for(0, 100, [&](int i) {
      // Throw an exception to cancel the task when i is 50.
      if (i == 50)
      {
         throw i;
      }
      else
      {
         // TODO: Perform work here.
      }
   });
}
catch (int n)
{
   wcout << L"Caught " << n << endl;
}

Folgende Ergebnisse werden zurückgegeben:

Caught 50

Im folgenden Beispiel wird der Abbruch in einer parallel_for-Schleife mit einem booleschen Flag koordiniert. Jede Aufgabe wird ausgeführt, da in diesem Beispiel nicht die übergeordnete Aufgabengruppe mit der cancel-Methode oder mit Ausnahmebehandlung abgebrochen wird. Diese Methode kann daher mehr Rechenleistung erfordern als die anderen Methoden.

// Create a Boolean flag to coordinate cancelation.
bool canceled = false;

parallel_for(0, 100, [&](int i) {
   // For illustration, set the flag to cancel the task when i is 50.
   if (i == 50)
   {
      canceled = true;
   }

   // Perform work if the task is not canceled.
   if (!canceled)
   {
      // TODO: Perform work here.
   }
});

Jede Abbruchmethode hat andere Vorteile. Wählen Sie die Methode, die Ihren Anforderungen am besten entspricht.

[Nach oben]

Wann ein Abbruch nicht verwendet werden sollte

Die Verwendung eines Abbruchs ist sinnvoll, wenn jeder Member einer Gruppe zusammenhängender Aufgaben rechtzeitig beendet werden kann. In einigen Fällen ist ein Abbruch jedoch für die Anwendung nicht sinnvoll. Da der Aufgabenabbruch kooperativ ist, wird die übergeordnete Aufgabengruppe beispielsweise nicht abgebrochen, wenn eine einzelne Aufgabe blockiert wird. Wenn z. B. eine Aufgabe, mit der die Blockierung einer anderen aktiven Aufgabe aufgehoben wird, noch nicht gestartet wurde, wird diese bei Abbruch der Aufgabengruppe nicht gestartet. Dies kann zu einem Deadlock-Fehler in der Anwendung führen. Ein Abbruch ist ebenfalls nicht sinnvoll, wenn eine Aufgabe abgebrochen wird, die untergeordnete Aufgabe jedoch einen wichtigen Vorgang, z. B. das Freigeben einer Ressource, ausführt. Da mit dem Abbruch der übergeordneten Aufgabe der gesamte Satz von Aufgaben abgebrochen wird, wird der Vorgang nicht ausgeführt. Ein Beispiel, in dem dieser Aspekt veranschaulicht wird, finden Sie im Abschnitt Informieren Sie sich über die Auswirkungen von Abbruch und Ausnahmebehandlung auf die Zerstörung von Objekten der empfohlenen Vorgehensweisen im Thema zur Parallel Patterns Library.

[Nach oben]

Verwandte Themen

Referenz

task_group-Klasse

structured_task_group-Klasse

parallel_for-Funktion

Änderungsprotokoll

Datum

Versionsgeschichte

Grund

März 2011

Dem Abschnitt "Wann ein Abbruch nicht verwendet werden sollte" einen weiteren Fall hinzugefügt.

Informationsergänzung.