Freigeben über


Allgemein empfohlene Vorgehensweisen in der Concurrency Runtime

Dieses Dokument beschreibt empfohlene Vorgehensweisen, die für mehrere Bereiche der Concurrency Runtime gelten.

Abschnitte

Dieses Dokument enthält folgende Abschnitte:

  • Verwenden Sie nach Möglichkeit Konstrukte für die kooperative Synchronisierung

  • Vermeiden Sie langwierige Aufgaben, die nicht zurückgehalten werden

  • Verwenden Sie die Überzeichnung für den Offset von blockierenden Vorgängen oder von Vorgängen mit langer Wartezeit

  • Verwenden Sie nach Möglichkeit parallele Speicherverwaltungsfunktionen

  • Verwenden Sie RAII zum Verwalten der Lebensdauer von Parallelitätsobjekten

  • Erstellen Sie keine Parallelitätsobjekte im globalen Gültigkeitsbereich

  • Verwenden Sie keine Parallelitätsobjekte in freigegebenen Datensegmenten

Verwenden Sie nach Möglichkeit Konstrukte für die kooperative Synchronisierung

Die Concurrency Runtime stellt viele parallelitätssichere Konstrukte bereit, die kein externes Synchronisierungsobjekt erfordern.Zum Beispiel die concurrency::concurrent_vector -Klasse stellt sicher anfügen und Element Operationen zugreifen.Für Fälle, wo exklusiven Zugriff auf eine Ressource benötigt, die Common Language Runtime bietet jedoch die concurrency::critical_section, concurrency::reader_writer_lock, und concurrency::event Klassen.Diese Typen weisen kooperatives Verhalten auf. Deshalb kann der Taskplaner Verarbeitungsressourcen neu einem anderen Kontext zuteilen, während die erste Aufgabe auf Daten wartet.Verwenden Sie nach Möglichkeit diese Synchronisierungstypen statt anderer Synchronisierungsmechanismen, z. B. die von der Windows-API bereitgestellten Synchronisierungsmechanismen, die kein kooperatives Verhalten aufweisen.Weitere Informationen zu diesen Synchronisierungstypen und ein Codebeispiel finden Sie unter Synchronisierungsdatenstrukturen und Vergleich der Synchronisierungsdatenstrukturen mit der Windows-API.

Top

Vermeiden Sie langwierige Aufgaben, die nicht zurückgehalten werden

Da sich der Taskplaner kooperativ verhält, stellt er keine Fairness zwischen Aufgaben bereit.Daher kann eine Aufgabe das Starten anderer Aufgaben verhindern.Dies ist zwar in manchen Fällen akzeptabel, kann jedoch in anderen Fällen Deadlocks oder Ressourcenmangel verursachen.

Im folgenden Beispiel übersteigt die Anzahl der ausgeführten Aufgaben die Anzahl der zugeteilten Verarbeitungsressourcen.Die erste Aufgabe wird nicht an den Taskplaner abgetreten, und daher wird die zweite Aufgabe erst gestartet, nachdem die erste Aufgabe beendet wurde.

// cooperative-tasks.cpp
// compile with: /EHsc
#include <ppl.h>
#include <iostream>
#include <sstream>

using namespace concurrency;
using namespace std;

// Data that the application passes to lightweight tasks.
struct task_data_t
{
   int id;  // a unique task identifier.
   event e; // signals that the task has finished.
};

// A lightweight task that performs a lengthy operation.
void task(void* data)
{   
   task_data_t* task_data = reinterpret_cast<task_data_t*>(data);

   // Create a large loop that occasionally prints a value to the console.
   int i;
   for (i = 0; i < 1000000000; ++i)
   {
      if (i > 0 && (i % 250000000) == 0)
      {
         wstringstream ss;
         ss << task_data->id << L": " << i << endl;
         wcout << ss.str();
      }
   }
   wstringstream ss;
   ss << task_data->id << L": " << i << endl;
   wcout << ss.str();

   // Signal to the caller that the thread is finished.
   task_data->e.set();
}

int wmain()
{
   // For illustration, limit the number of concurrent 
   // tasks to one.
   Scheduler::SetDefaultSchedulerPolicy(SchedulerPolicy(2, 
      MinConcurrency, 1, MaxConcurrency, 1));

   // Schedule two tasks.

   task_data_t t1;
   t1.id = 0;
   CurrentScheduler::ScheduleTask(task, &t1);

   task_data_t t2;
   t2.id = 1;
   CurrentScheduler::ScheduleTask(task, &t2);

   // Wait for the tasks to finish.

   t1.e.wait();
   t2.e.wait();
}

Dieses Beispiel erzeugt folgende Ausgabe:

1: 250000000
1: 500000000
1: 750000000
1: 1000000000
2: 250000000
2: 500000000
2: 750000000
2: 1000000000

Es gibt mehrere Möglichkeiten, die Zusammenarbeit zwischen den beiden Aufgaben zu ermöglichen.Eine Möglichkeit besteht darin, eine Aufgabe mit langer Ausführungszeit gelegentlich an den Taskplaner abzutreten.Das folgende Beispiel ändert die task -Funktion, die Concurrency::Context::Yield -Methode, um in den Taskplaner Ausführungskontrolle, so dass eine andere Aufgabe ausführen kann.

// A lightweight task that performs a lengthy operation.
void task(void* data)
{   
   task_data_t* task_data = reinterpret_cast<task_data_t*>(data);

   // Create a large loop that occasionally prints a value to the console.
   int i;
   for (i = 0; i < 1000000000; ++i)
   {
      if (i > 0 && (i % 250000000) == 0)
      {
         wstringstream ss;
         ss << task_data->id << L": " << i << endl;
         wcout << ss.str();

         // Yield control back to the task scheduler.
         Context::Yield();
      }
   }
   wstringstream ss;
   ss << task_data->id << L": " << i << endl;
   wcout << ss.str();

   // Signal to the caller that the thread is finished.
   task_data->e.set();
}

Dieses Beispiel erzeugt folgende Ausgabe:

1: 250000000
2: 250000000
1: 500000000
2: 500000000
1: 750000000
2: 750000000
1: 1000000000
2: 1000000000

Die Context::Yield-Methode gibt nur an einen anderen aktiven Thread des Planers, zu dem der aktuelle Thread gehört, an eine einfache Aufgabe oder an einen anderen Betriebssystemthread zurück.Diese Methode führt nicht zu arbeiten, die für die Ausführung geplant ist ein concurrency::task_group oder concurrency::structured_task_group Objekt, aber noch nicht begonnen.

Es gibt weitere Verfahren, die Zusammenarbeit zwischen Aufgaben mit langer Ausführungsdauer zu ermöglichen.Sie können eine große Aufgabe in kleinere Unteraufgaben aufteilen.Sie können auch Überzeichnung während einer langwierigen Aufgabe aktivieren.Durch Überzeichnung können Sie mehr Threads als die Anzahl der verfügbaren Hardwarethreads erstellen.Überzeichnung ist von besonderem Nutzen, wenn eine langwierige Aufgabe einen hohen Betrag an Wartezeit beinhaltet, z. B. das Lesen von Daten von einem Datenträger oder über eine Netzwerkverbindung.Weitere Informationen zu einfachen Aufgaben und Überzeichnung finden Sie unter Taskplaner (Concurrency Runtime).

Top

Verwenden Sie die Überzeichnung für den Offset von blockierenden Vorgängen oder von Vorgängen mit langer Wartezeit

Die Parallelität Runtime stellt Synchronisierungsprimitive, z. B. concurrency::critical_section, Aufgaben Kooperativ blockieren und miteinander zu ermöglichen.Wenn eine Aufgabe kooperativ blockiert oder zurückgehalten wird, kann der Taskplaner Verarbeitungsressourcen neu einem anderen Kontext zuteilen, während die erste Aufgabe auf Daten wartet.

Es gibt Fälle, in denen Sie den von der Concurrency Runtime bereitgestellten kooperativen Blockierungsmechanismus nicht verwenden können.Zum Beispiel verwendet eine externe Bibliothek möglicherweise einen anderen Synchronisierungsmechanismus.Ein weiteres Beispiel ist das Ausführen eines Vorgangs, der einen hohen Betrag an Wartezeit beinhalten kann, wenn Sie z. B. die ReadFile-Funktion der Windows-API zum Lesen von Daten über eine Netzwerkverbindung verwenden.In diesen Fällen kann Überzeichnung die Ausführung anderer Aufgaben ermöglichen, wenn sich eine andere Aufgabe im Leerlauf befindet.Durch Überzeichnung können Sie mehr Threads als die Anzahl der verfügbaren Hardwarethreads erstellen.

Betrachten Sie die folgende Funktion download, mit der die Datei an der angegebenen URL heruntergeladen wird.In diesem Beispiel wird die Concurrency::Context::Oversubscribe -Methode, um die Anzahl der aktiven Threads vorübergehend erhöhen.

// Downloads the file at the given URL.
string download(const string& url)
{
   // Enable oversubscription.
   Context::Oversubscribe(true);

   // Download the file.
   string content = GetHttpFile(_session, url.c_str());

   // Disable oversubscription.
   Context::Oversubscribe(false);

   return content;
}

Da die GetHttpFile-Funktion einen Vorgang mit potenzieller Wartezeit ausführt, kann Überzeichnung die Ausführung anderer Aufgaben ermöglichen, während die aktuelle Aufgabe auf Daten wartet.Die vollständige Version dieses Beispiels finden Sie unter Gewusst wie: Verwenden der Überzeichnung zum Kompensieren der Latenz.

Top

Verwenden Sie nach Möglichkeit parallele Speicherverwaltungsfunktionen

Verwenden Sie die Speicher-Management-Funktionen, concurrency::Alloc und concurrency::Free, bei abgestimmte Aufgaben, die häufig auf kleine Objekte zuordnen, die eine relativ kurze Lebensdauer haben.Die Concurrency Runtime verwaltet für jeden ausgeführten Thread einen eigenen Arbeitsspeichercache.Die Alloc-Funktion und die Free-Funktion reservieren Arbeitsspeicher in diesen Caches und geben Arbeitsspeicher in den Caches frei, ohne Sperren oder Arbeitsspeicherbarrieren zu verwenden.

Weitere Informationen zu diesen Speicherverwaltungsfunktionen finden Sie unter Taskplaner (Concurrency Runtime).Ein Beispiel, das diese Funktionen veranschaulicht, finden Sie unter Gewusst wie: Verbessern der Arbeitsspeicherleistung mithilfe von Alloc und Free.

Top

Verwenden Sie RAII zum Verwalten der Lebensdauer von Parallelitätsobjekten

Die Concurrency Runtime verwendet die Ausnahmebehandlung zum Implementieren von Funktionen, z. B. Abbruch.Schreiben Sie daher ausnahmesicheren Code, wenn Sie die Laufzeit oder eine andere Bibliothek aufrufen, die die Laufzeit aufruft.

Das RAII (Resource Acquisition Is Initialization)-Muster bietet eine Möglichkeit, die Lebensdauer eines Parallelitätsobjekts in einem angegebenen Bereich sicher zu verwalten.Unter dem RAII-Muster wird dem Stapel eine Datenstruktur zugeordnet.Diese Datenstruktur initialisiert oder ruft eine Ressource ab, wenn sie erstellt wird, und zerstört oder gibt diese Ressource frei, wenn die Datenstruktur zerstört wird.Das RAII-Muster garantiert, dass der Destruktor aufgerufen wird, bevor der einschließende Bereich beendet wird.Dieses Muster ist hilfreich, wenn eine Funktion mehrere return-Anweisungen enthält.Das Muster erleichtert Ihnen außerdem das Schreiben von ausnahmesicherem Code.Wenn eine throw-Anweisung das Entladen des Stapels verursacht, wird der Destruktor für das RAII-Objekt aufgerufen. Daher wird die Ressource immer ordnungsgemäß gelöscht oder freigegeben.

Die Common Language Runtime definiert mehrere Klassen, mit denen das Muster RAII, z. B. Concurrency::critical_section::scoped_lock und Concurrency::reader_writer_lock::scoped_lock.Diese Hilfsklassen werden als bewertete Sperren bezeichnet.Diese Klassen bieten mehrere Vorteile beim Arbeiten mit concurrency::critical_section oder concurrency::reader_writer_lock Objekte.Der Konstruktor dieser Klassen erhält Zugriff auf das bereitgestellte critical_section-Objekt bzw. reader_writer_lock-Objekt, und der Destruktor gibt den Zugriff auf das Objekt frei.Da eine bewertete Sperre den Zugriff auf das gegenseitige Ausschlussobjekt automatisch freigibt, wenn es zerstört wird, muss das zugrunde liegende Objekt nicht manuell entsperrt werden.

Betrachten Sie die folgende Klasse account, die durch eine externe Bibliothek definiert ist und deshalb nicht geändert werden kann.

// account.h
#pragma once
#include <exception>
#include <sstream>

// Represents a bank account.
class account
{
public:
   explicit account(int initial_balance = 0)
      : _balance(initial_balance)
   {
   }

   // Retrieves the current balance.
   int balance() const
   {
      return _balance;
   }

   // Deposits the specified amount into the account.
   int deposit(int amount)
   {
      _balance += amount;
      return _balance;
   }

   // Withdraws the specified amount from the account.
   int withdraw(int amount)
   {
      if (_balance < 0)
      {
         std::stringstream ss;
         ss << "negative balance: " << _balance << std::endl;
         throw std::exception((ss.str().c_str()));
      }

      _balance -= amount;
      return _balance;
   }

private:
   // The current balance.
   int _balance;
};

Im folgenden Beispiel werden mehrere Transaktionen für ein account-Objekt parallel ausgeführt.Im Beispiel wird ein critical_section-Objekt zum Synchronisieren des Zugriffs auf das account-Objekt verwendet, da die account-Klasse nicht parallelitätssicher ist.Für jeden parallelen Vorgang wird ein critical_section::scoped_lock-Objekt verwendet, um sicherzustellen, dass das critical_section-Objekt entsperrt wird, wenn der Vorgang erfolgreich ausgeführt wird oder fehlschlägt.Wenn der Kontostand negativ ist, löst der withdraw-Vorgang eine Ausnahme aus und schlägt fehl.

// account-transactions.cpp
// compile with: /EHsc
#include "account.h"
#include <ppl.h>
#include <iostream>
#include <sstream>

using namespace concurrency;
using namespace std;

int wmain()
{
   // Create an account that has an initial balance of 1924.
   account acc(1924);

   // Synchronizes access to the account object because the account class is 
   // not concurrency-safe.
   critical_section cs;

   // Perform multiple transactions on the account in parallel.   
   try
   {
      parallel_invoke(
         [&acc, &cs] {
            critical_section::scoped_lock lock(cs);
            wcout << L"Balance before deposit: " << acc.balance() << endl;
            acc.deposit(1000);
            wcout << L"Balance after deposit: " << acc.balance() << endl;
         },
         [&acc, &cs] {
            critical_section::scoped_lock lock(cs);
            wcout << L"Balance before withdrawal: " << acc.balance() << endl;
            acc.withdraw(50);
            wcout << L"Balance after withdrawal: " << acc.balance() << endl;
         },
         [&acc, &cs] {
            critical_section::scoped_lock lock(cs);
            wcout << L"Balance before withdrawal: " << acc.balance() << endl;
            acc.withdraw(3000);
            wcout << L"Balance after withdrawal: " << acc.balance() << endl;
         }
      );
   }
   catch (const exception& e)
   {
      wcout << L"Error details:" << endl << L"\t" << e.what() << endl;
   }
}

Dieses Beispiel erzeugt die folgende Beispielausgabe:

Balance before deposit: 1924
Balance after deposit: 2924
Balance before withdrawal: 2924
Balance after withdrawal: -76
Balance before withdrawal: -76
Error details:
        negative balance: -76

Weitere Beispiele, in denen das RAII-Muster zum Verwalten der Lebensdauer von Parallelitätsobjekten verwendet wird, finden Sie unter Exemplarische Vorgehensweise: Entfernen von Arbeit aus einem Benutzeroberflächenthread, Gewusst wie: Implementieren einer kooperativen Semaphore mithilfe der Context-Klasse und Gewusst wie: Verwenden der Überzeichnung zum Kompensieren der Latenz.

Top

Erstellen Sie keine Parallelitätsobjekte im globalen Gültigkeitsbereich

Beim Erstellen eines Objekts Parallelität im globalen Gültigkeitsbereich können Sie Themen wie Deadlocks oder Speicher in Ihrer Anwendung auftreten führen zu Zugriffsverletzungen.

Beispielsweise wenn Sie eine Parallelität Runtime-Objekt erstellen, erstellt die Laufzeit einen Standard-Planer für Sie wenn noch nicht erstellt wurde.Ein Common Language Runtime-Objekt, das während der Konstruktion eines globalen Objekts erstellt wird bewirkt entsprechend die Laufzeit dieses Standard-Scheduler zu erstellen.Dieser Vorgang benötigt jedoch eine interne Sperre, die die Initialisierung von anderen Objekten, die Unterstützung der Parallelität Runtime-Infrastruktur beeinträchtigen kann.Diese internen Sperren möglicherweise von einem anderen Objekt der Infrastruktur erforderlich, die noch nicht initialisiert und kann somit Deadlock auftreten in der Anwendung.

Das folgende Beispiel veranschaulicht die Erstellung eines globalen concurrency::Scheduler Objekt.Dieses Muster gilt nicht nur für die Scheduler -Klasse, aber alle anderen Typen, die von der Runtime Parallelität bereitgestellt werden.Wir empfehlen, die nicht diesem Muster folgen, da dies ein unerwartetes Verhalten in Ihrer Anwendung führen kann.

// global-scheduler.cpp
// compile with: /EHsc
#include <concrt.h>

using namespace concurrency;

static_assert(false, "This example illustrates a non-recommended practice.");

// Create a Scheduler object at global scope.
// BUG: This practice is not recommended because it can cause deadlock.
Scheduler* globalScheduler = Scheduler::Create(SchedulerPolicy(2,
   MinConcurrency, 2, MaxConcurrency, 4));

int wmain() 
{   
}

Beispiele für die ordnungsgemäße Vorgehensweise zum Erstellen von Scheduler-Objekten finden Sie unter Taskplaner (Concurrency Runtime).

Top

Verwenden Sie keine Parallelitätsobjekte in freigegebenen Datensegmenten

Die Concurrency Runtime unterstützt nicht die Verwendung von Parallelitätsobjekten in einem freigegebenen Datenabschnitt, z. B. in einem mit der data_seg #pragma-Direktive erstellten Datenabschnitt.Ein über Prozessgrenzen hinweg gemeinsam genutztes Parallelitätsobjekt kann einen inkonsistenten oder ungültigen Zustand der Laufzeit verursachen.

Top

Siehe auch

Aufgaben

Gewusst wie: Verbessern der Arbeitsspeicherleistung mithilfe von Alloc und Free

Gewusst wie: Verwenden der Überzeichnung zum Kompensieren der Latenz

Gewusst wie: Implementieren einer kooperativen Semaphore mithilfe der Context-Klasse

Exemplarische Vorgehensweise: Entfernen von Arbeit aus einem Benutzeroberflächenthread

Konzepte

Parallel Patterns Library (PPL)

Asynchronous Agents Library

Taskplaner (Concurrency Runtime)

Synchronisierungsdatenstrukturen

Vergleich der Synchronisierungsdatenstrukturen mit der Windows-API

Empfohlene Vorgehensweisen in der Parallel Patterns Library

Empfohlene Vorgehensweisen in der Asynchronous Agents Library

Weitere Ressourcen

Empfohlene Vorgehensweisen im Zusammenhang mit der Concurrency Runtime