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 von Konstrukten für die kooperative Synchronisierung nach Möglichkeit
Vermeiden Sie langwierige Vorgänge, die keine Erträge erzielen
Verwenden von Funktionen für die gleichzeitige Speicherverwaltung nach Möglichkeit
Verwenden von RAII zum Verwalten der Lebensdauer von Parallelitätsobjekten
Erstellen von Parallelitätsobjekten auf globaler Ebene nicht
Verwenden von Parallelitätsobjekten in freigegebenen Datensegmenten nicht
Verwenden von Konstrukten für die kooperative Synchronisierung nach Möglichkeit
Die Concurrency Runtime stellt viele parallelitätssichere Konstrukte bereit, die kein externes Synchronisierungsobjekt erfordern. Die Klasse "concurrency::concurrent_vector" stellt z. B. Parallelitätssichere Anfüge- und Elementzugriffsvorgänge bereit. Hier sind Parallelitätssichere Zeiger oder Iteratoren immer gültig. Es ist keine Garantie für die Elementinitialisierung oder eine bestimmte Traversalreihenfolge. In Fällen, in denen Sie exklusiven Zugriff auf eine Ressource benötigen, stellt die Laufzeit jedoch die Parallelität::critical_section, Parallelität::reader_writer_lock und Parallelitätsklassen::event bereit. Diese Typen weisen kooperatives Verhalten auf. Deshalb kann der Aufgabenplaner 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 einem Codebeispiel finden Sie unter "Synchronisierungsdatenstrukturen" und "Vergleichen von Synchronisierungsdatenstrukturen" mit der Windows-API.
Vermeiden Sie langwierige Vorgänge, die keine Erträge erzielen
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 die 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 Aufgabenplaner abzutreten. Im folgenden Beispiel wird die task
Funktion so geändert, dass die Parallelität::Context::Yield-Methode aufgerufen wird, um die Ausführung für den Aufgabenplaner zu ermöglichen, damit 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();
}
Dieses Beispiel erzeugt die 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 zur Arbeit, die für die Ausführung in einer Parallelität::task_group oder Parallelität::structured_task_group Objekt geplant ist, aber noch nicht gestartet wurde.
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 Vorgängen und Überschreibungen finden Sie unter Task Scheduler.
Verwenden von Oversubscription zum Versatz von Vorgängen, die eine hohe Latenz blockieren oder eine hohe Latenz aufweisen
Die Parallelitäts-Runtime stellt Synchronisierungsgrundtypen bereit, z . B. "concurrency::critical_section", mit denen Aufgaben kooperativ blockiert und miteinander ergibt. 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 Parallelität::Context::Oversubscribe-Methode verwendet, um die Anzahl der aktiven Threads vorübergehend zu 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 How to: Use Oversubscription to Offset Latency.
Verwenden von Funktionen für die gleichzeitige Speicherverwaltung nach Möglichkeit
Verwenden Sie die Speicherverwaltungsfunktionen, Parallelität::Alloc und Parallelität::Frei, wenn Sie feinkörnige Aufgaben haben, die häufig kleine Objekte zuweisen, die eine relativ kurze Lebensdauer aufweisen. 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 Task Scheduler. Ein Beispiel, das diese Funktionen verwendet, finden Sie unter How to: Use Alloc and Free to Improve Memory Performance.
Verwenden von 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 Muster "Resource Acquisition Is Initialization " (RAII) ist eine Möglichkeit, die Lebensdauer eines Parallelitätsobjekts unter einem bestimmten 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. Parallelität::critical_section::scoped_lock und Parallelität::reader_writer_lock::scoped_lock. Diese Hilfsklassen werden als bereichsbezogene Sperren bezeichnet. Diese Klassen bieten mehrere Vorteile, wenn Sie mit Parallelität::critical_section oder parallelen Objekten::reader_writer_lock 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:
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, die das RAII-Muster zum Verwalten der Lebensdauer von Parallelitätsobjekten verwenden, finden Sie unter Walkthrough: Removing Work from a User-Interface Thread, How to: Use the Context Class to Implement a Cooperative Semaphor, and How to: Use Oversubscription to Offset Latency.
Erstellen von Parallelitätsobjekten auf globaler Ebene nicht
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 Parallelitätsobjekts::Scheduler 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 richtige Methode zum Erstellen Scheduler
von Objekten finden Sie unter Task Scheduler.
Verwenden von Parallelitätsobjekten in freigegebenen Datensegmenten nicht
Die Parallelitätslaufzeit unterstützt nicht die Verwendung von Parallelitätsobjekten in einem freigegebenen Datenabschnitt, z. B. einen Datenabschnitt, der von der data_seg-Direktive#pragma
erstellt wird. Ein über Prozessgrenzen hinweg gemeinsam genutztes Parallelitätsobjekt kann einen inkonsistenten oder ungültigen Zustand der Laufzeit verursachen.
Siehe auch
Bewährte Methoden im Zusammenhang mit der Concurrency Runtime
Parallel Patterns Library (PPL)
Asynchrone Agents Library
Aufgabenplanung
Synchronisierungsdatenstrukturen
Vergleich der Synchronisierungsdatenstrukturen mit der Windows-API
Vorgehensweise: Verbessern der Arbeitsspeicherleistung mithilfe von Alloc und Free
Vorgehensweise: Verwenden der Überzeichnung zum Versetzen der Latenz
Vorgehensweise: Implementieren einer kooperativen Semaphore mithilfe der Context-Klasse
Exemplarische Vorgehensweise: Entfernen von Arbeit aus einem Benutzeroberflächenthread
Bewährte Methoden in der Parallel Patterns Library
Bewährte Methoden in der asynchronen Agents Library