Partager via


Meilleures pratiques en général du runtime d'accès concurrentiel

Ce document décrit les meilleures pratiques à appliquer à plusieurs zones du runtime d'accès concurrentiel.

Sections

Ce document contient les sections suivantes :

  • Utiliser les éléments de synchronisation coopérative, dans la mesure du possible

  • Éviter les tâches longues qui ne cèdent pas

  • Utiliser le surabonnement pour compenser les opérations qui effectuent un blocage ou qui présentent une latence élevée

  • Utiliser les fonctions de gestion de la mémoire simultanée, dans la mesure du possible

  • Utiliser RAII pour gérer la durée de vie des objets d'accès concurrentiel

  • Ne pas créer d'objets d'accès concurrentiel au niveau de la portée globale

  • Ne pas utiliser d'objets d'accès concurrentiel dans les segments de données partagées

Utiliser les éléments de synchronisation coopérative, dans la mesure du possible

Le runtime d'accès concurrentiel fournit un grand nombre d'éléments sécurisés du point de vue de l'accès concurrentiel qui ne requièrent pas d'objet de synchronisation externe.Par exemple, la concurrency::concurrent_vector classe fournit append simultanéité-safe et élément accéder aux opérations.Toutefois, pour les cas où vous avez besoin d'un accès exclusif à une ressource, le runtime fournit le concurrency::critical_section, concurrency::reader_writer_lock, et concurrency::event classes.Ces types se comportent de manière coopérative. Par conséquent, le planificateur de tâches peut réallouer les ressources de traitement à un autre contexte pendant que la première tâche attend des 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 pour obtenir un exemple de code, consultez Structures de données de synchronisation et Comparaison de structures de données de synchronisation avec l'API Windows.

Top

Éviter les tâches longues qui ne cèdent pas

Étant donné que le planificateur de tâches se comporte de manière coopérative, il ne traite pas équitablement les tâches.Par conséquent, une tâche peut empêcher le démarrage d'autres tâches.Dans certains cas, c'est acceptable. Dans d'autres cas, cela peut provoquer un interblocage ou la privation.

L'exemple suivant effectue plus de tâches que le nombre de ressources de traitement allouées.La première tâche ne cède pas au planificateur de tâches. Par conséquent, la deuxième tâche ne démarre pas tant que la première 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.L'une des méthodes consiste à céder occasionnellement au planificateur de tâches lorsqu'une tâche met du temps à s'exécuter.L'exemple suivant modifie la task fonction à appeler le concurrency::Context::Yield méthode pour produire l'exécution du Planificateur de tâches afin qu'une autre tâche peut être exécutée.

// 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 méthode Context::Yield ne cède qu'à un autre thread actif dans le planificateur auquel le thread actuel appartient, à une tâche légère ou à un thread d'un système d'exploitation différent.Cette méthode ne produit pas de travail qui est planifiée pour s'exécuter un concurrency::task_group ou concurrency::structured_task_group d'objet, mais n'a pas encore commencé.

Il existe d'autres façons d'activer la coopération entre des tâches de longue durée.Vous pouvez scinder une grande tâche en sous-tâches plus petites.Vous pouvez également activer le surabonnement pendant une tâche de longue durée.Le surabonnement vous permet de créer un nombre de threads plus important que le nombre de threads matériels disponibles.Le surabonnement peut s'avérer particulièrement utile lorsqu'une tâche de longue durée présente une latence élevée, par exemple, lors de la lecture des données à partir d'un disque ou d'une connexion réseau.Pour plus d'informations sur les tâches légères et le surabonnement, consultez Planificateur de tâches (runtime d'accès concurrentiel).

Top

Utiliser le surabonnement pour compenser les opérations qui effectuent un blocage ou qui présentent une latence élevée

Concurrency Runtime fournit des primitives de synchronisation, telles que concurrency::critical_section, qui permettent de bloquer en coopération et ralentir les uns aux autres des tâches.Lorsqu'une tâche effectue un blocage ou cède de manière coopérative, le planificateur de tâches peut réaffecter les ressources de traitement à un autre contexte pendant que la première tâche attend des données.

Dans certains cas, vous ne pouvez pas utiliser le mécanisme de blocage coopératif qui est fourni par le runtime d'accès concurrentiel.Par exemple, une bibliothèque externe que vous utilisez peut utiliser un mécanisme de synchronisation différent.Ou encore, lorsque vous exécutez une opération qui peut présenter une latence élevée, par exemple lorsque vous utilisez la fonction ReadFile de l'API Windows pour lire des données à partir d'une connexion réseau.Dans ces cas, le surabonnement peut permettre à des tâches de s'exécuter 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.

Prenons la fonction suivante, download, qui télécharge le fichier à l'URL donnée.Cet exemple utilise la concurrency::Context::Oversubscribe méthode 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 fonction GetHttpFile effectue une opération potentiellement latente, le surabonnement peut permettre à d'autres tâches de s'exécuter pendant que la tâche actuelle attend des données.Pour obtenir la version complète de cet exemple, consultez Comment : utiliser le surabonnement pour compenser la latence.

Top

Utiliser les fonctions de gestion de la mémoire simultanée, dans la mesure du possible

Utilisez les fonctions de gestion de mémoire, concurrency::Alloc et concurrency::Free, lorsque vous avez des tâches précis qui allouent fréquemment de petits objets qui ont une durée de vie relativement courte.Le runtime d'accès concurrentiel maintient un cache mémoire séparé pour chaque thread en cours de exécution.Les fonctions Alloc et Free allouent et libèrent la mémoire disponible 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 (runtime d'accès concurrentiel).Pour obtenir un exemple qui utilise ces fonctions, consultez Comment : utiliser Alloc et Free pour améliorer les performances de la mémoire.

Top

Utiliser RAII pour gérer la durée de vie des objets d'accès concurrentiel

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 sécurisé du point de vue des exceptions lorsque vous appelez le runtime ou une autre bibliothèque qui appelle le runtime.

Le modèle RAII (Resource Acquisition Is Initialization) est un moyen de gérer sans risque la durée de vie d'un objet d'accès concurrentiel dans une portée donnée.Selon 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 que la portée englobante ne quitte.Ce modèle est utile lorsqu'une fonction contient plusieurs instructions return.Ce modèle est également utile lorsque vous écrivez du code sécurisé du point de vue des exceptions.Lorsqu'une instruction throw provoque le déroulement de la pile, le destructeur de l'objet RAII est appelé. Par conséquent, la ressource est toujours correctement supprimée ou libérée.

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 portent le nom de verrous à portée limitée.Ces classes offrent plusieurs avantages lorsque vous travaillez avec des concurrency::critical_section ou concurrency::reader_writer_lock objets.Le constructeur de ces classes acquiert l'accès à l'objet critical_section ou reader_writer_lock fourni et le destructeur libère l'accès à cet objet.Étant donné qu'un verrou à portée limitée libère automatiquement l'accès à son objet d'exclusion mutuelle lorsqu'il est détruit, vous ne déverrouillez pas l'objet sous-jacent manuellement.

Prenons l'exemple de 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 objet account en parallèle.L'exemple utilise un objet critical_section pour synchroniser l'accès à l'objet account, car la classe account n'est pas sécurisée du point de vue de l'accès concurrentiel.Chaque opération parallèle utilise un objet critical_section::scoped_lock pour garantir que l'objet critical_section est déverrouillé lorsque l'opération réussit ou échoue.Lorsque le solde de compte est négatif, l'opération withdraw échoue en levant 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 génère 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 des exemples supplémentaires 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 de travail d'un thread d'interface utilisateur, Comment : utiliser la classe Context pour implémenter un sémaphore coopératif et Comment : utiliser le surabonnement pour compenser la latence.

Top

Ne pas créer d'objets d'accès concurrentiel au niveau de la portée globale

Lorsque vous créez un objet d'accès concurrentiel dans une portée globale vous pouvez provoquer des problèmes tels que le blocage ou la mémoire des violations d'accès se produit dans votre application.

Par exemple, lorsque vous créez un objet Concurrency Runtime, le runtime crée un ordonnanceur par défaut pour vous si aucune n'était pas encore créée.Un objet Common Language runtime qui est créé pendant la construction d'un objet global entraîne en conséquence le runtime créer ce Planificateur par défaut.Toutefois, ce processus prend un verrou interne, qui peut interférer avec l'initialisation des autres objets qui prennent en charge l'infrastructure du Runtime de simultanéité.Ce verrou interne peut être requis par un autre objet d'infrastructure qui n'a pas encore été initialisé et peut donc provoquer un blocage se produit dans votre application.

L'exemple suivant illustre la création d'un global concurrency::Scheduler objet.Ce modèle ne s'applique pas uniquement à la Scheduler classe mais tous les autres types sont fournis par le Runtime de simultanéité.Nous recommandons que vous ne suivez pas 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 sur la façon de créer correctement des objets Scheduler, consultez Planificateur de tâches (runtime d'accès concurrentiel).

Top

Ne pas utiliser d'objets d'accès concurrentiel dans les segments de données partagées

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é au delà des limites de processus peut entraîner un état incohérent ou non valide du runtime.

Top

Voir aussi

Tâches

Comment : utiliser Alloc et Free pour améliorer les performances de la mémoire

Comment : utiliser le surabonnement pour compenser la latence

Comment : 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

Concepts

Bibliothèque de modèles parallèles

Bibliothèque d'agents asynchrones

Planificateur de tâches (runtime d'accès concurrentiel)

Structures de données de synchronisation

Comparaison de structures de données de synchronisation avec l'API Windows

Meilleures pratiques de la Bibliothèque de modèles parallèles

Meilleures pratiques de la Bibliothèque d'agents asynchrones

Autres ressources

Meilleures pratiques sur le runtime d'accès concurrentiel