Cet article a fait l'objet d'une traduction automatique.

Windows et C++

Évolution de la synchronisation dans Windows et C++

Kenny Kerr

 

Kenny KerrQuand j'ai commencé à écrire des logiciels concurrents, C++ n'a aucun support pour la synchronisation. Windows lui-même avait seulement une poignée de primitives de synchronisation, qui tous ont été mis en œuvre dans le noyau. Je commence à utiliser une section critique à moins que j'avais besoin de synchroniser à travers des processus, auquel cas j'ai utilisé un mutex. En règle générale, ces deux sont les serrures, ou verrouiller des objets.

Le mutex tire son nom de la notion de « exclusion mutuelle, » un autre nom pour la synchronisation. Il se réfère à la garantie que seul un thread peut accéder à certaines ressources à la fois. La section critique tire son nom de la section de code qui peut accéder à cette ressource. Afin d'assurer l'exactitude, qu'un seul thread peut exécuter cette section critique de code à la fois. Ces deux objets de verrou ont des fonctions différentes, mais il est utile de se rappeler qu'ils se bloque juste, ils ont tous deux fournissent des garanties de l'exclusion mutuelle et tous deux peuvent servir à délimiter des sections critiques de code.

Aujourd'hui le paysage de synchronisation a changé radicalement. Il y a une pléthore de choix pour les programmeurs C++. Windows prend désormais en charge les fonctions de synchronisation plus nombreux, et C++ lui-même fournit enfin une intéressante collection de fonctionnalités d'accès concurrentiel et de synchronisation pour ceux qui utilisent un compilateur supportant le C ++ 11 standard.

Dans article cet, je vais explorer l'état de synchronisation de Windows et le C++. Je vais commencer par examiner les primitives de synchronisation fournies par Windows lui-même et ensuite envisager les alternatives fournies par la bibliothèque C++ Standard. Si la portabilité est votre préoccupation principale, alors les nouveaux ajouts de bibliothèque C++ sera très attrayants. Si, toutefois, la portabilité est moins préoccupante et performance est primordiale, alors se familiariser avec ce que Windows maintenant offres sera importants. Nous allons plonger dans.

Section critique

Première place est l'objet de section critique. Ce verrou est largement utilisé par les applications innombrables, mais a une histoire sordide. Quand j'ai commencé à l'aide de sections critiques, ils étaient vraiment simples. Pour créer une telle serrure, tout ce dont vous aviez besoin devait allouer une structure CRITICAL_SECTION et appeler la fonction InitializeCriticalSection afin de le préparer pour l'utilisation. Cette fonction ne retourne pas une valeur, ce qui implique qu'il ne peut pas échouer. Retour à cette époque, cependant, il fallait que cette fonction créer différentes ressources système, notamment un objet événement du noyau, et il a été possible que dans des situations extrêmement faible mémoire cela échouerait, résultant en une exception structurée se pose. Pourtant, c'était plutôt rare, donc la plupart des développeurs ignoré cette possibilité.

Avec la popularité de COM, l'utilisation de sections critiques ont monté en flèche parce que de nombreuses classes COM utilisé des sections critiques pour la synchronisation, mais dans de nombreux cas, il y avait peu ou aucun conflit réel pour parler de. Lorsque les ordinateurs multiprocesseurs est devenu plus répandus, événement interne de la section critique a vu encore moins son utilisation parce que la section critique serait tourner brièvement en mode utilisateur en attendant d'acquérir le verrou. Un compteur de rotations petites signifiait que beaucoup de brèves périodes de contention pourraient éviter une transition de noyau, améliorant considérablement les performances.

Vers cette époque, certains développeurs du noyau s'est rendu compte qu'ils pourraient améliorer considérablement l'évolutivité de Windows si ils ont reporté la création d'objets event section critique jusqu'à ce qu'il y avait suffisamment de contention pour exiger leur présence. Cela semblait être une bonne idée jusqu'à ce que les développeurs compris que cela signifie que bien que InitializeCriticalSection pourrait échouer maintenant impossible, la fonction EnterCriticalSection (permet d'attendre pour la propriété de verrou) n'était plus fiable. Cela ne pourrait pas aussi facilement être négligé par les développeurs, car il introduit une variété de conditions d'erreur qui aurait fait des sections critiques presque impossible à utiliser correctement et à arrêter des applications innombrables. Pourtant, les victoires d'évolutivité ne pouvait pas être négligés.

Un développeur du noyau est enfin arrivé à une solution sous la forme d'un objet d'événement nouveau et sans papiers, noyau appelé un événement clé. Vous pouvez lire un peu à ce sujet dans le livre, « Windows Internals, » par Mark E. Russinovich, David A. Salomon et Alex Ionescu (Microsoft Press, 2012), mais au fond, au lieu d'exiger un objet event pour chaque section critique, un seul événement à clé pourrait être utilisé pour toutes les sections critiques dans le système. Cela fonctionne parce qu'un objet événement à clé est exactement cela : Elle s'appuie sur une touche, qui est juste un identificateur pointeur taille qui est naturellement l'espace adresse locale.

Il y avait sûrement une tentation pour mettre à jour les sections critiques pour utiliser exclusivement les événements à clé, mais parce que plusieurs débogueurs et autres outils s'appuient sur les données internes de sections critiques, l'événement clé a été utilisé seulement en dernier recours si le noyau n'a pas pu allouer un objet événement régulier.

Ceci peut sembler comme beaucoup d'histoire sans importance mais pour le fait que la performance d'assortie des événements a été significativement amélioré au cours du cycle de développement de Windows Vista, et cela a mené à l'introduction d'un tout nouveau verrou d'objet qui était aussi bien plus simple et plus rapide, mais plus sur cela dans une minute.

Comme l'objet section critique est désormais exempté de l'échec en raison des conditions de mémoire insuffisante, il est vraiment très simple à utiliser. Figure 1 fournit un wrapper simple.

Figure 1 l'écluse de Section critique

class lock
{
  CRITICAL_SECTION h;
  lock(lock const &);
  lock const & operator=(lock const &);
public:
  lock()
  {
    InitializeCriticalSection(&h);
  }
  ~lock()
  {
    DeleteCriticalSection(&h);
  }
  void enter()
  {
    EnterCriticalSection(&h);
  }
  bool try_enter()
  {
    return 0 != TryEnterCriticalSection(&h);
  }
  void exit()
  {
    LeaveCriticalSection(&h);
  }
  CRITICAL_SECTION * handle()
  {
    return &h;
  }
};

La fonction EnterCriticalSection que je l'ai déjà mentionné est complétée par une fonction TryEnterCriticalSection qui fournissent une alternative non bloquant. La fonction LeaveCriticalSection libère le verrou, et DeleteCriticalSection libère toutes les ressources du noyau qui pourraient ont été réparties le long du chemin.

Si la section critique est un choix raisonnable. Il exécute très bien car elle cherche à éviter les transitions de noyau et d'allocation des ressources. Pourtant, il a un peu de bagages qu'il doit transporter en raison de sa compatibilité d'histoire et de l'application.

Mutex

L'objet mutex est un vrai noyau-synchronisation. À la différence des sections critiques, un verrou mutex consomme toujours des ressources allouées du noyau. L'avantage, bien sûr, est que le noyau est alors en mesure de fournir la synchronisation inter-processus en raison de sa prise de conscience de la serrure. Comme un objet de noyau, il fournit les attributs habituels — tel qu'un nom, qui peut être utilisé pour ouvrir l'objet provenant d'autres processus ou tout simplement pour identifier le verrou dans un débogueur. Vous pouvez également spécifier un masque d'accès pour restreindre l'accès à l'objet. C'est comme une serrure intraprocess, d'overkill, un peu plus compliqué à utiliser et beaucoup plus lent. La figure 2 fournit un wrapper simple pour un mutex non nommé qui est efficacement processus local.

Figure 2 le verrou Mutex

#ifdef _DEBUG
  #include <crtdbg.h>
  #define ASSERT(expression) _ASSERTE(expression)
  #define VERIFY(expression) ASSERT(expression)
  #define VERIFY_(expected, expression) ASSERT(expected == expression)
#else
  #define ASSERT(expression) ((void)0)
  #define VERIFY(expression) (expression)
  #define VERIFY_(expected, expression) (expression)
#endif
class lock
{
  HANDLE h;
  lock(lock const &);
  lock const & operator=(lock const &);
public:
  lock() :
    h(CreateMutex(nullptr, false, nullptr))
  {
    ASSERT(h);
  }
  ~lock()
  {
    VERIFY(CloseHandle(h));
  }
  void enter()
  {
    VERIFY_(WAIT_OBJECT_0, WaitForSingleObject(h, INFINITE));
  }
  bool try_enter()
  {
    return WAIT_OBJECT_0 == WaitForSingleObject(h, 0);
   }
  void exit()
  {
    VERIFY(ReleaseMutex(h));
  }
  HANDLE handle()
  {
    return h;
  }
};

La fonction CreateMutex crée le verrou et la fonction CloseHandle commune ferme le processus gérer, qui effectivement décrémente décompte de référence de la serrure dans le noyau. En attendant la propriété verrouillage s'effectue avec la fonction WaitForSingleObject, qui vérifie et attend éventuellement l'état signalé d'une variété d'objets du noyau. Le second paramètre indique combien de temps le thread appelant doit être bloquée en attendant d'acquérir le verrou. La constante infinie est – sans surprise – une attente indéfinie, tandis que la valeur zéro empêche le thread n'attende à tous et sera seulement acquérir le verrou si c'est gratuit. Enfin, la fonction ReleaseMutex libère le verrou.

Le verrou mutex est un gros marteau avec beaucoup de puissance, mais il s'agit pour un coût de performances et de la complexité. Le wrapper dans Figure 2 est jonché d'affirmations pour indiquer les possibilités qu'elle peut échouer, mais c'est l'impact sur les performances qui disqualifient le verrou mutex dans la plupart des cas.

Event

Avant que je parle d'une serrure de haute performance, j'ai besoin d'introduire un objet noyau-synchronisation plus, auquel je faisais allusion déjà. Bien que pas réellement un verrou, en ce qu'elle ne fournit pas une installation d'implémenter directement l'exclusion mutuelle, l'objet événement est essentiel pour la coordination du travail entre les threads. En fait, c'est le même objet utilisé en interne par l'écluse de la section critique — et d'ailleurs, il est très pratique lors de l'implémentation de toutes sortes de modèles d'accès concurrentiel d'une manière efficace et évolutive.

La fonction CreateEvent crée l'événement et — comme le mutex — la fonction CloseHandle ferme le handle, libérant l'objet dans le noyau. Parce qu'il n'est pas réellement un verrou, il n'a aucune sémantique d'acquisition/diffusion. Au contraire, il est l'incarnation même de la fonctionnalité de signalisation fournie par un grand nombre des objets du noyau. Pour comprendre comment les travaux de signalisation, vous devez comprendre qu'un objet event peut être créé dans l'un des deux États. Si vous passez true pour second paramètre de CreateEvent, puis l'objet événement qui en résulte est dit être un événement de réinitialisation manuelle ; Sinon, un événement de réinitialisation automatique est créé. Un événement de réinitialisation manuelle nécessite que vous définissez manuellement et réinitialisez l'état signalé de l'objet. Les fonctions SetEvent et ResetEvent sont prévues à cet effet. Une auto événement de réinitialisation automatique­réinitialise automatiquement (change d'état signalé à non signalé) lorsqu'un thread en attente est libéré. Si un événement de réinitialisation automatique est utile lorsqu'un thread doit assurer la coordination avec les autres threads, alors qu'un événement de réinitialisation manuelle est utile lorsqu'un thread doit coordonner avec n'importe quel nombre de threads. Appeler SetEvent sur un événement de réinitialisation automatique publiera tout au plus un seul thread, alors qu'avec un événement de réinitialisation manuelle, qui appellent à volonté tous les threads en attente de sortie. Comme le mutex, en attente d'un événement soit signalé est obtenue avec la fonction WaitForSingleObject. Figure 3 fournit un wrapper simple pour un événement sans nom qui peut être construit dans les deux modes.

Figure 3 le Signal de l'événement

class event
{
  HANDLE h;
  event(event const &);
  event const & operator=(event const &);
public:
  explicit event(bool manual = false) :
    h(CreateEvent(nullptr, manual, false, nullptr))
  {
    ASSERT(h);
  }
  ~event()
  {
    VERIFY(CloseHandle(h));
  }
  void set()
  {
    VERIFY(SetEvent(h));
  }
  void clear()
  {
    VERIFY(ResetEvent(h));
  }
  void wait()
  {
    VERIFY_(WAIT_OBJECT_0, WaitForSingleObject(h, INFINITE));
  }
};

Verrou SRW

Le nom du verrou de lecteur/graveur Slim (SRW) peut être une bouchée, mais le mot clé est « slim ». Programmeurs pourraient négliger ce verrou en raison de sa capacité à distinguer les lecteurs partagés et exclusifs écrivains, pensant peut-être que c'est exagéré quand tout ce dont ils ont besoin est une section critique. Il s'avère que c'est le verrou plus simple à traiter et aussi de loin le plus rapide et vous ont certainement pas besoin d'avoir partagé lecteurs afin de le pour utiliser. Il a rapidement sa réputation non seulement parce qu'elle s'appuie sur l'objet événement à clé efficace, mais aussi parce qu'il est principalement mis en œuvre en mode utilisateur et seulement descend vers le noyau si le conflit est telle que le thread serait mieux dormir. Encore une fois, les objets mutex et de section critiques fournissent des fonctionnalités supplémentaires, que vous pouvez demander, par exemple récursif ou verrous inter-processus, mais plus souvent qu'autrement, tout ce dont vous avez besoin est un verrouillage rapide et léger pour un usage interne.

Ce verrou s'appuie exclusivement sur les événements à clé que je l'ai mentionné avant, et par conséquent il est extrêmement léger malgré offrant beaucoup de fonctionnalités. Le verrou SRW nécessite uniquement un pointeur -­taille d'espace de stockage, qui est allouée par le processus appelant plutôt que par le noyau. Pour cette raison, la fonction d'initialisation, InitializeSRWLock, ne manquera pas et simplement assure que le verrouillage contient le modèle binaire appropriée avant d'être utilisé.

En attente de verrouillage propriétaire est réalisé en utilisant soit l'acquisition­SRWLockExclusive fonction pour un verrou en écriture dite ou en utilisant la fonction AcquireSRWLockShared pour verrous de lecteur. Toutefois, la terminologie exclusive et partagée est plus appropriée. Il y a libération correspondante et try-acquérir les fonctions qu'on attendrait pour deux exclusive et partagée des modes. Figure 4 fournit un wrapper simple pour un verrou SRW exclusive. Il ne serait pas difficile pour vous d'ajouter les fonctions mode partagé si nécessaire. Notez, cependant, qu'il n'y a aucun destructeur car il n'y a pas de ressources à libérer.

La figure 4 le verrou SRW

class lock
{
  SRWLOCK h;
  lock(lock const &);
  lock const & operator=(lock const &);
public:
  lock()
  {
    InitializeSRWLock(&h);
  }
  void enter()
  {
    AcquireSRWLockExclusive(&h);
  }
  bool try_enter()
  {
    return 0 != TryAcquireSRWLockExclusive(&h);
  }
  void exit()
  {
    ReleaseSRWLockExclusive(&h);
  }
  SRWLOCK * handle()
  {
    return &h;
  }
};

Variable de condition

L'objet de synchronisation finale, que j'ai besoin de mettre en place est la variable conditionnelle. C'est peut-être celle avec laquelle la plupart des programmeurs sera familiers. J'ai, cependant, remarqué un regain d'intérêt pour les variables de condition au cours des derniers mois. Cela pourrait avoir quelque chose à voir avec C ++ 11, mais l'idée n'est pas nouveau et prise en charge de ce concept a été autour depuis un certain temps sur Windows. En effet, Microsoft .NET Framework appuie le modèle de variable de condition depuis la toute première version, même si elle a été fusionnée à la classe Monitor, qui limite son utilité en quelque sorte. Mais ce regain d'intérêt est également grâce à l'incroyables événements à clé permettant aux variables de la condition d'être présenté par Windows Vista, et ils ont seulement amélioré depuis. Bien qu'une variable de condition est simplement un modèle d'accès concurrentiel et, par conséquent, peut être implémentée avec les autres primitives, son inclusion dans le système d'exploitation signifie qu'il peut atteindre des performances incroyables et libère le programmeur de l'obligation de s'assurer de l'exactitude d'un tel code. En effet, si vous utilisez des primitives de synchronisation de système d'exploitation, il est presque impossible d'assurer l'exactitude de certains modes d'accès concurrentiel sans l'aide du système d'exploitation lui-même.

Le modèle de variable de condition est assez courant, si vous pensez cela. Un programme doit attendre une condition à remplir avant de procéder à cela. Évaluation de cette condition consiste à acquérir un verrou pour évaluer certains état partagé. Si, cependant, la condition n'a pas encore été atteints, le verrou doit être libéré pour permettre à un autre thread remplir la condition. Le thread évaluation doit alors attendre jusqu'à ce que la condition soit remplie avant d'acquérir à nouveau le verrou. Une fois que le verrou est acquis de nouveau, la condition doit être évaluée à nouveau, pour éviter la condition évidente. Application de la présente est plus difficile qu'il n'y paraît car il y a, en effet, autres pièges à s'inquiéter, et mise en œuvre de manière efficace est encore plus difficile. Le Pseudo-code suivant illustre le problème :

lock-enter
while (!condition-eval)
{
  lock-exit
  condition-wait
  lock-enter
}
// Do interesting stuff here
lock-exit

Mais même dans cette illustration, il y a un bug subtil. Pour fonctionner correctement, la condition doit être attendue dès avant la sortie de l'écluse, mais faisant donc ne fonctionne pas car la serrure serait alors jamais libérée. La capacité de libérer un objet atomiquement et d'attendre sur un autre est si critique que Windows fournit la fonction SignalObjectAndWait pour faire ainsi pour certains objets du noyau. Mais parce que le verrou SRW vit principalement en mode utilisateur, une solution différente est nécessaire. Entrez les variables conditionnelles.

Comme le verrou SRW, la variable conditionnelle occupe seulement un unique volume taille pointeur de stockage et est initialisée avec la fonction InitializeConditionVariable Fail-Safe. Comme avec les verrous SRW, il n'y a aucune ressource pour libérer, afin que lorsque la variable conditionnelle n'est plus nécessaire la mémoire peut simplement être récupérée.

Parce que la condition elle-même est propre au programme, il incombe à l'appelant d'écrire le modèle comme un certain temps boucle avec le corps en un seul appel à la fonction SleepConditionVariableSRW. Cette fonction libère atomiquement le verrou SRW en attendant d'être réveillé une fois que la condition est remplie. Il est également une fonction SleepConditionVariableCS correspondante si vous souhaitez utiliser à la place des variables de condition avec une serrure de section critique.

La fonction WakeConditionVariable est appelée pour réveiller une attente simple, ou dormir, fil. Le thread rΘveillΘs sera réacquérir le verrou avant de retourner. Alternativement, la fonction WakeAllConditionVariable peut être utilisée pour réveiller tous les threads en attente. Figure 5fournit un wrapper simple avec le nécessaire tout en boucle. Notez qu'il est possible que le thread en sommeil être réveillé de manière imprévisible, et la boucle while s'assure que la condition est toujours vérifiée après que le thread acquiert le verrou à nouveau. Il est également important de noter que le prédicat est toujours évalué tout en maintenant le verrou.

Figure 5 la Variable conditionnelle

class condition_variable
{
  CONDITION_VARIABLE h;
  condition_variable(condition_variable const &);
  condition_variable const & operator=(condition_variable const &);
public:
  condition_variable()
  {
    InitializeConditionVariable(&h);
  }
  template <typename T>
  void wait_while(lock & x, T predicate)
  {
    while (predicate())
    {
      VERIFY(SleepConditionVariableSRW(&h, x.handle(), INFINITE, 0));
    }
  }
  void wake_one()
  {
    WakeConditionVariable(&h);
  }
  void wake_all()
  {
    WakeAllConditionVariable(&h);
  }
};

File d'attente de blocage

Pour donner cette forme de certains, je vais utiliser une file d'attente de blocage par exemple. Permettez-moi de souligner que je ne recommande pas en général les files d'attente de blocage. Vous pouvez mieux servis en utilisant un port d'achèvement e/s ou le pool de threads Windows, qui est simplement une abstraction au-dessus de l'ancienne, ou même la classe concurrent_queue du Runtime d'accès concurrentiel. Quoi que ce soit non bloquant est généralement préféré. Pourtant, une file d'attente bloquante est un concept simple à comprendre et quelque chose que beaucoup de développeurs semble trouver utile. Il est vrai, pas chaque programme doit pouvoir évoluer, mais chaque programme doit être correcte. Une file d'attente bloquante fournit aussi amplement l'occasion d'employer la synchronisation en termes d'exactitude et, bien sûr, amplement l'occasion de se tromper.

Envisagez d'implémenter une file d'attente bloquante avec juste un verrou et un événement. Le verrou protège les files d'attente partagées et l'événement signale au consommateur que le producteur a poussé quelque chose sur la file d'attente. Figure 6 fournit un exemple simple à l'aide d'un événement de réinitialisation automatique. J'ai utilisé ce mode événement parce que la méthode push uniquement un seul élément en file d'attente et, par conséquent, je veux seulement un consommateur d'être réveillé pour pop hors de la file d'attente. La méthode push acquiert le verrou, l'élément en file d'attente et puis signale l'événement à se réveiller à tout consommateur d'attente. La méthode pop acquiert le verrou et puis attend jusqu'à ce que la file d'attente n'est pas vide avant la file d'attente un élément et en le retournant. Les deux méthodes utilisent une classe lock_block. Par souci de concision, il n'a pas été inclus, mais il appelle simplement le verrouillage entrez méthode dans son constructeur et la méthode exit dans son destructeur.

Figure 6 réinitialisation automatique blocage de file d'attente

template <typename T>
class blocking_queue
{
  std::deque<T> q;
  lock x;
  event e;
  blocking_queue(blocking_queue const &);
  blocking_queue const & operator=(blocking_queue const &);
public:
  blocking_queue()
  {
  }
  void push(T const & value)
  {
    lock_block block(x);
    q.push_back(value);
    e.set();
  }
  T pop()
  {
    lock_block block(x);
    while (q.empty())
    {
      x.exit(); e.wait(); // Bug!
x.enter();
    }
    T v = q.front();
    q.pop_front();
    return v;
  }
};

Cependant, remarquez l'impasse probablement parce que les appels de sortie et d'attente ne sont pas atomiques. Si le verrou était un mutex, je pourrais utiliser la fonction SignalObjectAndWait, mais les performances de la file d'attente de blocage en souffrirait.

Une autre option consiste à utiliser un événement de réinitialisation manuelle. Signalisation chaque fois qu'un élément est mis en attente, plutôt que de simplement définir deux États. L'événement puisse être signalé pour tant qu'il y a des éléments dans la file d'attente et non signalé lorsqu'elle est vide. Cela jouera aussi beaucoup mieux car il ya moins d'appels dans le noyau pour signaler l'événement. La figure 7 fournit un exemple de cela. Notez comment la méthode push définit l'événement, si la file d'attente comporte un seul élément. Cela permet d'éviter tout appel superflu à la fonction SetEvent. La méthode pop efface consciencieusement l'événement si elle trouve la file d'attente vide. Tant qu'il y a plusieurs éléments en file d'attente, n'importe quel nombre de consommateurs pouvez extraire des éléments de la file d'attente sans impliquer l'objet événement, améliorant ainsi l'évolutivité.

Figure 7 réinitialisation manuelle blocage de file d'attente

template <typename T>
class blocking_queue
{
  std::deque<T> q;
  lock x;
  event e;
  blocking_queue(blocking_queue const &);
  blocking_queue const & operator=(blocking_queue const &);
public:
  blocking_queue() :
    e(true) // manual
  {
  }
  void push(T const & value)
  {
    lock_block block(x);
    q.push_back(value);
    if (1 == q.size())
    {
      e.set();
    }
  }
  T pop()
  {
    lock_block block(x);
    while (q.empty())
    {
      x.exit();
      e.wait();
      x.enter();
    }
    T v = q.front();
    q.pop_front();
    if (q.empty())
    {
      e.clear();
    }
    return v;
  }
};

Dans ce cas il n'est aucun blocage potentiel dans la séquence de sortie-attente-entrée car un autre consommateur ne peuvent pas voler l'événement, étant donné que c'est un événement de réinitialisation manuelle. Il est difficile de faire mieux qu'en termes de performances. Néanmoins, une solution de rechange (et peut-être plus naturel) solution est d'utiliser une variable conditionnelle au lieu d'un événement. Ceci se fait facilement avec la classe condition_variable dans Figure 5 et est similaire à la file d'attente bloquante de réinitialisation manuelle, bien qu'il soit un peu plus simple. Figure 8 fournit un exemple. Notez comment les intentions de la sémantique et la concurrence deviennent plus claires comme les objets de synchronisation de niveau supérieures sont employées. Cette clarté permet d'éviter les bogues de simultanéité qui frappent souvent plus obscur code.

Condition de blocage Variable figure 8 files d'attente

template <typename T>
class blocking_queue
{
  std::deque<T> q;
  lock x;
  condition_variable cv;
  blocking_queue(blocking_queue const &);
  blocking_queue const & operator=(blocking_queue const &);
public:
  blocking_queue()
  {
  }
  void push(T const & value)
  {
    lock_block block(x);
    q.push_back(value);
    cv.wake_one();
  }
  T pop()
  {
    lock_block block(x);
    cv.wait_while(x, [&]()
    {
      return q.empty();
    });
    T v = q.front();
    q.pop_front();
    return v;
  }
};

Enfin, je dois mentionner que C ++ 11 fournit maintenant un verrou, appelé un mutex, mais aussi un condition_variable. Le C ++ 11 mutex n'a rien à voir avec le mutex Windows. De même, le C ++ 11 condition_variable n'est pas basé sur la variable d'état de Windows. C'est une bonne nouvelle en termes de portabilité. Il peut être utilisé n'importe où vous pouvez trouver un compilateur C++ conforme. En revanche, le C ++ 11 mise en œuvre dans la version Visual C++ 2012 effectue assez mal par rapport à la variable Windows SRW de verrouillage et l'état. Figure 9 fournit un exemple d'une file d'attente de blocage mis en place avec la norme C ++ 11 types de bibliothèques.

Figure 9 ++ 11 bloque la file d'attente

template <typename T>
class blocking_queue
{
  std::deque<T> q;
  std::mutex x;
  std::condition_variable cv;
  blocking_queue(blocking_queue const &);
  blocking_queue const & operator=(blocking_queue const &);
public:
  blocking_queue()
  {
  }
  void push(T const & value)
  {
    std::lock_guard<std::mutex> lock(x);
    q.push_back(value);
    cv.
notify_one();
  }
  T pop()
  {
    std::unique_lock<std::mutex> lock(x);
    cv.wait(lock, [&]()
    {
      return !q.empty();
    });
    T v = q.front();
    q.pop_front();
    return v;
  }
};

La mise en œuvre de la bibliothèque C++ Standard améliorera sans aucun doute dans le temps, comme le sera la prise en charge de la bibliothèque d'accès concurrentiel en général. Le Comité C++ a pris quelques petites mesures conservatrices vers la prise en charge d'accès concurrentiel qui devrait être reconnu, mais le travail n'est pas encore fait. Comme j'ai expliqué dans mes trois dernières colonnes, l'avenir de l'accès concurrentiel C++ est toujours en question. Pour l'instant, la combinaison de quelques excellents primitives de synchronisation dans Windows et le compilateur C++ état-of-the-art fait une trousse à outils convaincant pour produire des émissions de l'accès concurrentiel légers et modulable.

Kenny Kerr fait figure d'artisan informatique passionné par le développement Windows natif. Vous pouvez le contacter à cette adresse : kennykerr.ca.

Merci à l'expert technique suivant d'avoir relu cet article : Mohamed Ameen Ibrahim