Meilleures pratiques en général du runtime d’accès concurrentiel
Ce document décrit les meilleures pratiques qui s’appliquent à plusieurs zones du runtime d’accès concurrentiel.
Sections
Ce document contient les sections suivantes :
Utiliser des constructions de synchronisation coopérative lorsque cela est possible
Utiliser oversubscription pour décaler les opérations qui bloquent ou ont une latence élevée
Utiliser des fonctions de gestion de la mémoire simultanée lorsque cela est possible
Utiliser RAII pour gérer la durée de vie des objets concurrentiels
N’utilisez pas d’objets concurrentiels dans des segments de données partagés
Utiliser des constructions de synchronisation coopérative lorsque cela est possible
Le runtime d’accès concurrentiel fournit de nombreuses constructions sécurisées de concurrence qui ne nécessitent pas d’objet de synchronisation externe. Par exemple, la classe concurrency ::concurrent_vector fournit des opérations d’ajout et d’accès à l’élément concurrency-safe. Ici, la concurrence-safe signifie que les pointeurs ou les itérateurs sont toujours valides. Il ne s’agit pas d’une garantie d’initialisation d’élément ou d’un ordre de traversée particulier. Toutefois, dans les cas où vous avez besoin d’un accès exclusif à une ressource, le runtime fournit les classes concurrency ::critical_section, concurrency ::reader_writer_lock et concurrency ::event . Ces types se comportent de manière coopérative ; par conséquent, le planificateur de tâches peut réallouer des ressources de traitement vers un autre contexte, car la première tâche attend les données. Si possible, utilisez ces types de synchronisation au lieu d’autres mécanismes de synchronisation, tels que ceux fournis par l’API Windows, qui ne se comportent pas de manière coopérative. Pour plus d’informations sur ces types de synchronisation et un exemple de code, consultez Structures de données de synchronisation et comparaison des structures de données de synchronisation à l’API Windows.
[Haut]
Éviter les tâches longues qui ne produisent pas
Étant donné que le planificateur de tâches se comporte de manière coopérative, il ne fournit pas d’équité entre les tâches. Par conséquent, une tâche peut empêcher le démarrage d’autres tâches. Bien que cela soit acceptable dans certains cas, dans d’autres cas, cela peut entraîner un blocage ou une faim.
L’exemple suivant effectue plus de tâches que le nombre de ressources de traitement allouées. La première tâche n’est pas renvoyée au planificateur de tâches et, par conséquent, la deuxième tâche ne démarre pas tant que la première tâche n’est pas terminée.
// 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();
}
Cet exemple produit la sortie suivante :
1: 250000000 1: 500000000 1: 750000000 1: 1000000000 2: 250000000 2: 500000000 2: 750000000 2: 1000000000
Il existe plusieurs façons d’activer la coopération entre les deux tâches. Une façon est de donner occasionnellement au planificateur de tâches dans une tâche de longue durée. L’exemple suivant modifie la task
fonction pour appeler la méthode concurrency ::Context ::Yield pour générer l’exécution au planificateur de tâches afin qu’une autre tâche puisse s’exécuter.
// 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();
}
Cet exemple produit la sortie suivante :
1: 250000000
2: 250000000
1: 500000000
2: 500000000
1: 750000000
2: 750000000
1: 1000000000
2: 1000000000
La Context::Yield
méthode génère uniquement un autre thread actif sur le planificateur auquel appartient le thread actuel, une tâche légère ou un autre thread de système d’exploitation. Cette méthode ne génère pas de travail qui est planifiée pour s’exécuter dans un objet concurrency ::task_group ou concurrency ::structured_task_group , mais n’a pas encore démarré.
Il existe d’autres façons d’activer la coopération entre les tâches de longue durée. Vous pouvez interrompre une tâche volumineuse en tâches subordonnées plus petites. Vous pouvez également activer la sursubscription pendant une longue tâche. Le surabonnement vous permet de créer un nombre de threads plus important que le nombre de threads matériels disponibles. Oversubscription est particulièrement utile lorsqu’une tâche longue contient une grande latence, par exemple, la lecture de données à partir d’un disque ou d’une connexion réseau. Pour plus d’informations sur les tâches légères et sursubscription, consultez Planificateur de tâches.
[Haut]
Utiliser oversubscription pour décaler les opérations qui bloquent ou ont une latence élevée
Le runtime d’accès concurrentiel fournit des primitives de synchronisation, telles que concurrency ::critical_section, qui permettent aux tâches de bloquer et de se produire mutuellement. Lorsqu’une tâche bloque ou génère de manière coopérative, le planificateur de tâches peut réallouer des ressources de traitement vers un autre contexte, car la première tâche attend les données.
Il existe des cas dans lesquels vous ne pouvez pas utiliser le mécanisme de blocage coopératif fourni par le runtime d’accès concurrentiel. Par exemple, une bibliothèque externe que vous utilisez peut utiliser un autre mécanisme de synchronisation. Par exemple, lorsque vous effectuez une opération qui peut avoir une grande latence, par exemple lorsque vous utilisez la fonction API ReadFile
Windows pour lire des données à partir d’une connexion réseau. Dans ces cas, la sursubscription peut permettre à d’autres tâches d’être exécutées lorsqu’une autre tâche est inactive. Le surabonnement vous permet de créer un nombre de threads plus important que le nombre de threads matériels disponibles.
Considérez la fonction suivante, download
qui télécharge le fichier à l’URL donnée. Cet exemple utilise la méthode concurrency ::Context ::Oversubscribe pour augmenter temporairement le nombre de threads actifs.
// 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;
}
Étant donné que la GetHttpFile
fonction effectue une opération potentiellement latente, la sursubscription peut permettre à d’autres tâches de s’exécuter en attendant que la tâche actuelle attend les données. Pour obtenir la version complète de cet exemple, consultez How to : Use Oversubscription to Offset Latency.
[Haut]
Utiliser des fonctions de gestion de la mémoire simultanée lorsque cela est possible
Utilisez les fonctions de gestion de la mémoire, concurrency ::Alloc et concurrency ::Free, lorsque vous avez des tâches affinées qui allouent fréquemment de petits objets qui ont une durée de vie relativement courte. Le runtime d’accès concurrentiel contient un cache de mémoire distinct pour chaque thread en cours d’exécution. Les Alloc
fonctions allouent Free
et libèrent de la mémoire à partir de ces caches sans utiliser de verrous ou de barrières de mémoire.
Pour plus d’informations sur ces fonctions de gestion de la mémoire, consultez Planificateur de tâches. Pour obtenir un exemple qui utilise ces fonctions, consultez Guide pratique pour utiliser Alloc et Free pour améliorer les performances de la mémoire.
[Haut]
Utiliser RAII pour gérer la durée de vie des objets concurrentiels
Le runtime d’accès concurrentiel utilise la gestion des exceptions pour implémenter des fonctionnalités telles que l’annulation. Par conséquent, écrivez du code sans risque d’exception lorsque vous appelez le runtime ou appelez une autre bibliothèque qui appelle le runtime.
Le modèle RAII (Resource Acquisition Is Initialization ) est un moyen de gérer en toute sécurité la durée de vie d’un objet d’accès concurrentiel dans une étendue donnée. Sous le modèle RAII, une structure de données est allouée sur la pile. Cette structure de données initialise ou acquiert une ressource lorsqu’elle est créée et détruit ou libère cette ressource lorsque la structure de données est détruite. Le modèle RAII garantit que le destructeur est appelé avant la sortie de l’étendue englobante. Ce modèle est utile lorsqu’une fonction contient plusieurs return
instructions. Ce modèle vous aide également à écrire du code sans risque d’exception. Lorsqu’une throw
instruction provoque le déroulement de la pile, le destructeur de l’objet RAII est appelé ; par conséquent, la ressource est toujours supprimée ou libérée correctement.
Le runtime définit plusieurs classes qui utilisent le modèle RAII, par exemple, concurrency ::critical_section ::scoped_lock et concurrency ::reader_writer_lock ::scoped_lock. Ces classes d’assistance sont appelées verrous délimités. Ces classes offrent plusieurs avantages lorsque vous utilisez des objets concurrency ::critical_section ou concurrency ::reader_writer_lock . Le constructeur de ces classes acquiert l’accès au ou à l’objet fourni critical_section
reader_writer_lock
; le destructeur libère l’accès à cet objet. Étant donné qu’un verrou délimité libère automatiquement l’accès à son objet d’exclusion mutuelle lorsqu’il est détruit, vous ne déverrouillez pas manuellement l’objet sous-jacent.
Considérez la classe suivante, account
qui est définie par une bibliothèque externe et ne peut donc pas être modifiée.
// 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;
};
L’exemple suivant effectue plusieurs transactions sur un account
objet en parallèle. L’exemple utilise un critical_section
objet pour synchroniser l’accès à l’objet account
, car la account
classe n’est pas sécurisée par concurrence. Chaque opération parallèle utilise un critical_section::scoped_lock
objet pour garantir que l’objet critical_section
est déverrouillé lorsque l’opération réussit ou échoue. Lorsque le solde du compte est négatif, l’opération withdraw
échoue en lève une exception.
// 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;
}
}
Cet exemple produit l’exemple de sortie suivant :
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
Pour obtenir d’autres exemples qui utilisent le modèle RAII pour gérer la durée de vie des objets d’accès concurrentiel, consultez Procédure pas à pas : suppression du travail à partir d’un thread d’interface utilisateur, guide pratique pour utiliser la classe de contexte pour implémenter un sémaphore coopératif et comment : utiliser oversubscription pour décaler la latence.
[Haut]
Ne pas créer d’objets concurrentiels à l’étendue globale
Lorsque vous créez un objet d’accès concurrentiel à l’étendue globale, vous pouvez provoquer des problèmes tels que des blocages ou des violations d’accès à la mémoire dans votre application.
Par exemple, lorsque vous créez un objet Concurrency Runtime, le runtime crée un planificateur par défaut pour vous si un objet n’a pas encore été créé. Un objet runtime créé lors de la construction d’objets globaux entraîne en conséquence que le runtime crée ce planificateur par défaut. Toutefois, ce processus prend un verrou interne, qui peut interférer avec l’initialisation d’autres objets qui prennent en charge l’infrastructure du runtime d’accès concurrentiel. Ce verrou interne peut être requis par un autre objet d’infrastructure qui n’a pas encore été initialisé et peut donc entraîner un blocage dans votre application.
L’exemple suivant illustre la création d’un objet concurrency global ::Scheduler . Ce modèle s’applique non seulement à la Scheduler
classe, mais à tous les autres types fournis par le runtime d’accès concurrentiel. Nous vous recommandons de ne pas suivre ce modèle, car il peut provoquer un comportement inattendu dans votre application.
// 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()
{
}
Pour obtenir des exemples de la façon appropriée de créer des Scheduler
objets, consultez Le planificateur de tâches.
[Haut]
N’utilisez pas d’objets concurrentiels dans des segments de données partagés
Le runtime d’accès concurrentiel ne prend pas en charge l’utilisation d’objets d’accès concurrentiel dans une section de données partagées, par exemple, une section de données créée par la directive data_seg#pragma
. Un objet d’accès concurrentiel partagé entre les limites de processus peut placer le runtime dans un état incohérent ou non valide.
[Haut]
Voir aussi
Bonnes pratiques sur le runtime d’accès concurrentiel
Bibliothèque de modèles parallèles
Bibliothèque d’agents asynchrones
Planificateur de tâches
Structures de données de synchronisation
Comparaison des structures de données de synchronisation avec l’API Windows
Guide pratique pour utiliser Alloc et Free pour améliorer les performances de la mémoire
Guide pratique pour utiliser le surabonnement pour compenser la latence
Guide pratique pour utiliser la classe Context pour implémenter un sémaphore coopératif
Procédure pas à pas : suppression de travail d’un thread d’interface utilisateur
Bonnes pratiques de la Bibliothèque de modèles parallèles
Bonnes pratiques pour la bibliothèque d’agents asynchrones