Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Dieses Dokument beschreibt empfohlene Vorgehensweisen, die für mehrere Bereiche der Concurrency Runtime gelten.
Abschnitte
Dieses Dokument enthält folgende Abschnitte:
Verwendung kooperativer Synchronisationskonstrukte, wenn möglich
Vermeidung von langwierigen Aufgaben, die keinen Mehrwert erzeugen
Verwendung konkurrierender Speicherverwaltungsfunktionen, wenn möglich
Verwendung von RAII zum Verwalten der Lebensdauer von Gleichzeitigkeitsobjekten
Keine Erstellung von Gleichzeitigkeitsobjekten im globalen Umfang
Keine Verwendung von Gleichzeitigkeitsobjekten in gemeinsam genutzten Datensegmenten
Verwendung kooperativer Synchronisationskonstrukte, wenn möglich
Die Concurrency Runtime stellt viele parallelitätssichere Konstrukte bereit, die kein externes Synchronisierungsobjekt erfordern. Die Klasse concurrency::concurrent_vector bietet zum Beispiel nebenläufige Operationen für das Anhängen und den Zugriff auf Elemente. Gleichzeitigkeitssicher bedeutet hier, dass Pointer oder Iteratoren immer gültig sind. Es ist keine Garantie für die Initialisierung von Elementen oder für eine bestimmte Traversalreihenfolge. Für Fälle, in denen Sie einen exklusiven Zugriff auf eine Ressource benötigen, bietet die Runtime jedoch die Klassen concurrency::critical_section, concurrency::reader_writer_lock und concurrency::event. 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 über diese Synchronisationsarten und ein Codebeispiel finden Sie unter Datenstrukturen für die Synchronisierung und cVergleich von Synchronisationsdatenstrukturen mit der Windows-API.
Vermeidung von langwierigen Aufgaben, die keinen Mehrwert erzeugen
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 Funktion task so geändert, dass sie die Methode concurrency::Context::Yield anfragt, um die Ausführung an den Taskplaner abzugeben, damit ein anderer Task 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 wirkt sich nicht auf Arbeiten aus, die in einem concurrency::task_group oder concurrency::structured_task_group-Objekt geplant sind, aber 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 über vereinfachte Aufgaben und Oversubscription finden Sie unter Task Scheduler.
Verwendung von Oversubscription, um Operationen auszugleichen, die blockieren oder eine hohe Latenz haben
Die Concurrency Runtime bietet Synchronisierungsprimitive wie concurrency::critical_section, die es Aufgaben ermöglichen, sich gegenseitig zu blockieren und anzuhalten. 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. Dieses Beispiel verwendet die Methode concurrency::Context::Oversubscribe, 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 Vorgehensweise: Oversubscription verwenden, um Latenz auszugleichen.
Verwendung konkurrierender Speicherverwaltungsfunktionen, wenn möglich
Verwenden Sie die Funktionen concurrency::Alloc und concurrency::Free zur Speicherverwaltung, wenn Sie detaillierte Aufgaben haben, die häufig kleine Objekte zuweisen, 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 über diese Speicherverwaltungsfunktionen finden Sie unter Task Scheduler. Ein Beispiel, das diese Funktionen verwendet, finden Sie unter Vorgehensweise: Alloc und Free verwenden, um die Speicherleistung zu verbessern.
Verwendung von RAII zum Verwalten der Lebensdauer von Gleichzeitigkeitsobjekten
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-Muster (Resource Acquisition Is Initialization) ist eine Möglichkeit, die Lebensdauer eines Gleichzeitigkeitsobjekts in 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 Runtime definiert mehrere Klassen, die das RAII-Muster verwenden, zum Beispiel concurrency::critical_section::scoped_lock und concurrency::reader_writer_lock::scoped_lock. Diese Hilfsklassen werden als Scoped Locks bezeichnet. Diese Klassen bieten mehrere Vorteile, wenn Sie mit concurrency::critical_section 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:
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 für die Verwendung des RAII-Musters zur Verwaltung der Lebensdauer von Gleichzeitigkeitsobjekten finden Sie unter Exemplarische Vorgehensweise: Entfernen von Arbeit aus einem Benutzer-Interface-Thread, Vorgehensweise: Verwendung von Context-Klasse zur Implementierung eines kooperativen Semaphors und Vorgehensweise: Verwendung von Oversubscriptions, um Latenz auszugleichen.
Keine Erstellung von Gleichzeitigkeitsobjekten im globalen Umfang
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.
Das folgende Beispiel demonstriert die Erstellung eines globalen concurrency::Scheduler-Objekts. 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 korrekte Erstellung von Scheduler-Objekten finden Sie unter Task Scheduler.
Keine Verwendung von Gleichzeitigkeitsobjekten in gemeinsam genutzten Datensegmenten
Die Concurrency Runtime unterstützt nicht die Verwendung von Gleichzeitigkeitsobjekten in einem gemeinsam genutzten Datenabschnitt, z. B. einem Datenabschnitt, der mit der data_seg#pragma-Direktive erstellt wurde. Ein über Prozessgrenzen hinweg gemeinsam genutztes Parallelitätsobjekt kann einen inkonsistenten oder ungültigen Zustand der Laufzeit verursachen.
Weitere Informationen
Bewährte Methoden im Zusammenhang mit der Concurrency Runtime
Parallel Patterns Library (PPL)
Asynchrone Agents Library
Task Scheduler
Synchronisierungsdatenstrukturen
Vergleich der Synchronisierungsdatenstrukturen mit der Windows-API
Vorgehensweise: Verbessern der Arbeitsspeicherleistung mithilfe von Alloc und Free
Vorgehensweise: Verwendung von Oversubscription 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