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. Beispielsweise stellt die concurrency::concurrent_vector-Klasse parallelitätssichere Anfügevorgänge und parallelitätssicheren Elementzugriff bereit. Für Fälle, in denen Sie exklusiven Zugriff auf eine Ressource benötigen, stellt die Laufzeit jedoch die Klassen concurrency::critical_section, concurrency::reader_writer_lock und concurrency::event bereit. 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.

[Nach oben]

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();
}

In diesem Beispiel wird die folgende Ausgabe erzeugt:

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. Im folgenden Beispiel wird die task-Funktion geändert, um die concurrency::Context::Yield-Methode aufzurufen, damit die Ausführung an den Taskplaner abgetreten wird und eine andere Aufgabe ausgeführt werden 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();
}

In diesem Beispiel wird die folgende Ausgabe erzeugt:

  

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 hält keine Arbeitsvorgänge zugunsten von Arbeitsvorgängen zurück, die für die Ausführung in einem concurrency::task_group-Objekt oder einem concurrency::structured_task_group-Objekt geplant sind, jedoch noch nicht gestartet wurden.

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).

[Nach oben]

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

Die Concurrency Runtime stellt Synchronisierungsprimitive, z. B. concurrency::critical_section, bereit, die das kooperative Blockieren und Zurückhalten von Aufgaben 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 mit der concurrency::Context::Oversubscribe-Methode die Anzahl aktiver Threads vorübergehend erhöht.

// 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.

[Nach oben]

Verwenden Sie nach Möglichkeit parallele Speicherverwaltungsfunktionen

Verwenden Sie die Speicherverwaltungsfunktionen concurrency::Alloc und concurrency::Free, wenn differenzierte Aufgaben häufig Speicher für kleine Objekte mit relativ kurzer Lebensdauer reservieren. 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.

[Nach oben]

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 Laufzeit definiert mehrere Klassen, die das RAII-Muster verwenden, 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, wenn Sie mit concurrency::critical_section-Objekten oder concurrency::reader_writer_lock-Objekten arbeiten. 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:

  

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.

[Nach oben]

Erstellen Sie keine Parallelitätsobjekte im globalen Gültigkeitsbereich

Wenn Sie ein Parallelitätsobjekt im globalen Gültigkeitsbereich erstellen, kann dies zu Problemen wie Deadlocks oder Arbeitsspeicher-Zugriffsverletzungen in der Anwendung führen.

Wenn Sie z. B. ein Concurrency Runtime-Objekt erstellen, erstellt die Laufzeit einen Standardplaner, sofern noch kein Planer erstellt wurde. Ein Laufzeitobjekt, das während der globalen Objekterstellung erstellt wird, führt dementsprechend dazu, dass die Laufzeit diesen Standardplaner erstellt. Dieser Vorgang verwendet jedoch eine interne Sperre, die die Initialisierung anderer Objekte behindern kann, die die Concurrency Runtime-Infrastruktur unterstützen. Diese interne Sperre wird eventuell von einem anderen Infrastrukturobjekt benötigt, das noch nicht initialisiert wurde, und daher tritt möglicherweise ein Deadlock in der Anwendung auf.

Im folgenden Beispiel wird die Erstellung eines globalen concurrency::Scheduler-Objekts veranschaulicht. Dieses Muster gilt nicht nur für die Scheduler-Klasse, sondern auch für alle anderen Typen, die von der Concurrency Runtime bereitgestellt werden. Es wird empfohlen, dieses Muster nicht anzuwenden, da es zu einem unerwarteten Verhalten in der 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).

[Nach oben]

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.

[Nach oben]

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