Delen via


Algemene aanbevolen procedures in de Gelijktijdigheidsruntime

In dit document worden aanbevolen procedures beschreven die van toepassing zijn op meerdere gebieden van de Gelijktijdigheidsruntime.

Afdelingen

Dit document bevat de volgende secties:

Coöperatieve synchronisatieconstructies gebruiken indien mogelijk

De Gelijktijdigheidsruntime biedt veel gelijktijdigheidsveilige constructies waarvoor geen extern synchronisatieobject is vereist. De klasse gelijktijdigheid::concurrent_vector biedt bijvoorbeeld gelijktijdigheidsveilige toevoeg- en elementtoegangsbewerkingen. Hier zijn gelijktijdigheidsveilige aanwijzers of iterators altijd geldig. Het is geen garantie voor het initialiseren van elementen of van een bepaalde doorkruisingsvolgorde. Voor gevallen waarin u echter exclusieve toegang tot een resource nodig hebt, biedt de runtime de concurrency::critical_section, concurrency::reader_writer_lock en concurrency::event classes. Deze typen gedragen zich coöperatief; Daarom kan de taakplanner verwerkingsbronnen opnieuw toewijzen aan een andere context wanneer de eerste taak wacht op gegevens. Gebruik indien mogelijk deze synchronisatietypen in plaats van andere synchronisatiemechanismen, zoals die worden geleverd door de Windows-API, die niet samenwerken. Zie Synchronisatiegegevensstructuren en synchronisatiegegevensstructuren vergelijken met de Windows-API voor meer informatie over deze synchronisatietypen en een codevoorbeeld.

[Boven]

Lange taken voorkomen die niet opleveren

Omdat de taakplanner zich coöperatief gedraagt, biedt deze geen billijkheid tussen taken. Daarom kan een taak voorkomen dat andere taken worden gestart. Hoewel dit in sommige gevallen acceptabel is, kan dit in andere gevallen een impasse of verhongering veroorzaken.

In het volgende voorbeeld worden meer taken uitgevoerd dan het aantal toegewezen verwerkingsresources. De eerste taak levert niet op aan de taakplanner en daarom wordt de tweede taak pas gestart als de eerste taak is voltooid.

// 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();
}

In dit voorbeeld wordt de volgende uitvoer gegenereerd:

1: 250000000 1: 500000000 1: 750000000 1: 1000000000 2: 250000000 2: 500000000 2: 750000000 2: 1000000000

Er zijn verschillende manieren om samenwerking tussen de twee taken mogelijk te maken. Een manier is om bij een langlopende taak af en toe controle aan de taakplanner over te dragen. In het volgende voorbeeld wordt de task functie aangepast om de concurrency::Context::Yield methode aan te roepen, waarmee de uitvoering aan de taakplanner wordt overgedragen zodat een andere taak kan worden uitgevoerd.

// 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();
}

In dit voorbeeld wordt de volgende uitvoer gegenereerd:

1: 250000000
2: 250000000
1: 500000000
2: 500000000
1: 750000000
2: 750000000
1: 1000000000
2: 1000000000

De Context::Yield methode levert slechts een andere actieve thread op de scheduler op waartoe de huidige thread behoort, een lichtgewicht taak of een andere besturingssysteemthread. Deze methode levert geen werk op dat volgens de planning moet worden uitgevoerd in een concurrency::task_group of concurrency::structured_task_group object, maar nog niet is gestart.

Er zijn andere manieren om samenwerking tussen langlopende taken mogelijk te maken. U kunt een grote taak opsplitsen in kleinere subtaken. U kunt ook oversubscriptie inschakelen tijdens een lange taak. Met oversubscription kunt u meer threads maken dan het beschikbare aantal hardwarethreads. Oversubscriptie is vooral handig wanneer een lange taak een hoge hoeveelheid latentie bevat, bijvoorbeeld het lezen van gegevens van schijf of van een netwerkverbinding. Zie Task Scheduler voor meer informatie over lichtgewicht taken en oversubscriptie.

[Boven]

Gebruik oversubscription om bewerkingen te compenseren die blokkeren of een hoge latentie hebben

De Concurrency Runtime biedt synchronisatieprimitieven, zoals concurrency::critical_section, waarmee taken elkaar op een coöperatieve manier kunnen blokkeren en aan elkaar kunnen toegeven. Wanneer een taak blokkeert of pauzeert, kan de taakplanner verwerkingsbronnen opnieuw toewijzen aan een andere context terwijl de eerste taak op gegevens wacht.

Er zijn gevallen waarin u het samenwerkende blokkeringmechanisme dat wordt geleverd door de Gelijktijdigheidsruntime niet kunt gebruiken. Een externe bibliotheek die u gebruikt, kan bijvoorbeeld een ander synchronisatiemechanisme gebruiken. Een ander voorbeeld is wanneer u een bewerking uitvoert die een hoge hoeveelheid latentie kan hebben, bijvoorbeeld wanneer u de Functie Windows API ReadFile gebruikt om gegevens uit een netwerkverbinding te lezen. In dergelijke gevallen kan oversubscriptie andere taken inschakelen om uit te voeren wanneer een andere taak niet actief is. Met oversubscription kunt u meer threads maken dan het beschikbare aantal hardwarethreads.

Houd rekening met de volgende functie, downloadwaarmee het bestand op de opgegeven URL wordt gedownload. In dit voorbeeld wordt de concurrency::Context::Oversubscribe-methode gebruikt om het aantal actieve threads tijdelijk te verhogen.

// 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;
}

Omdat de GetHttpFile functie een mogelijk latente bewerking uitvoert, kan overinschrijving andere taken toestaan te worden uitgevoerd terwijl de huidige taak wacht op gegevens. Zie voor de volledige versie van dit voorbeeld: Oversubscription gebruiken om latentie te compenseren.

[Boven]

Gelijktijdige geheugenbeheerfuncties gebruiken, indien mogelijk

Gebruik de geheugenbeheerfuncties, gelijktijdigheid:::Alloc en gelijktijdigheid:::Gratis wanneer u fijnmazige taken hebt die vaak kleine objecten toewijzen die een relatief korte levensduur hebben. De Gelijktijdigheidsruntime bevat een afzonderlijke geheugencache voor elke actieve thread. De functies Alloc en Free wijzen geheugen toe en maken geheugen vrij uit deze caches, zonder gebruik te maken van vergrendelingen of geheugenbarrières.

Zie Task Scheduler voor meer informatie over deze geheugenbeheerfuncties. Zie Hoe: Geheugenprestaties verbeteren met Alloc en Free voor een voorbeeld waarin deze functies worden gebruikt.

[Boven]

RAII gebruiken om de levensduur van gelijktijdigheidsobjecten te beheren

Concurrency Runtime maakt gebruik van uitzonderingsafhandeling om functies zoals annulering te implementeren. Schrijf daarom uitzonderingsveilige code wanneer u de runtime aanroept of een andere bibliotheek aanroept die in de runtime wordt aangeroepen.

Het RAII-patroon ( Resource Acquisition Is Initialization ) is een manier om de levensduur van een gelijktijdigheidsobject onder een bepaald bereik veilig te beheren. Onder het RAII-patroon wordt een gegevensstructuur toegewezen aan de stack. Deze gegevensstructuur initialiseert of verkrijgt een resource wanneer deze wordt gemaakt en vernietigt of publiceert die resource wanneer de gegevensstructuur wordt vernietigd. Het RAII-patroon garandeert dat de destructor wordt aangeroepen voordat het omsluitbereik wordt afgesloten. Dit patroon is handig wanneer een functie meerdere return instructies bevat. Dit patroon helpt u ook bij het schrijven van uitzonderingsveilige code. Wanneer een throw instructie ervoor zorgt dat de stack tot rust komt, wordt de destructor voor het RAII-object aangeroepen. Daarom wordt de resource altijd correct verwijderd of vrijgegeven.

De runtime definieert verschillende klassen die gebruikmaken van het RAII-patroon, bijvoorbeeld concurrentie::critical_section::scoped_lock en concurrentie::reader_writer_lock::scoped_lock. Deze helperklassen worden scoped-vergrendelingen genoemd. nl-NL: Deze klassen bieden verschillende voordelen wanneer u werkt met concurrency::critical_section of concurrency::reader_writer_lock-objecten. De constructor van deze klassen krijgt toegang tot het opgegeven critical_section-object of reader_writer_lock-object; de destructor geeft die toegang weer vrij. Omdat een bereikvergrendeling automatisch toegang tot het wederzijdse uitsluitingsobject vrijgeeft wanneer het wordt vernietigd, ontgrendelt u het onderliggende object niet handmatig.

Houd rekening met de volgende klasse, accountdie is gedefinieerd door een externe bibliotheek en daarom niet kan worden gewijzigd.

// 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;
};

In het volgende voorbeeld worden meerdere transacties op een account object parallel uitgevoerd. In het voorbeeld wordt een critical_section object gebruikt om de toegang tot het account object te synchroniseren omdat de account klasse niet gelijktijdigheidsveilig is. Elke parallelle bewerking maakt gebruik van een critical_section::scoped_lock object om te garanderen dat het critical_section object wordt ontgrendeld wanneer de bewerking slaagt of mislukt. Wanneer het rekeningsaldo negatief is, mislukt de withdraw bewerking door een uitzondering te genereren.

// 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;
   }
}

In dit voorbeeld wordt de volgende voorbeelduitvoer geproduceerd:

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

Zie Walkthrough: Verwijderen van werk van een User-Interface-thread, Hoe te gebruiken: Gebruik de Context-klasse om een coöperatieve semafoor te implementeren en Hoe te gebruiken: Gebruik oversubscriptie om latentie te compenseren voor aanvullende voorbeelden die gebruikmaken van het RAII-patroon om de levensduur van gelijktijdigheidsobjecten te beheren.

[Boven]

Maak geen gelijktijdigheidsobjecten op globaal niveau.

Wanneer u een concurrency-object maakt op globale scope, kunt u problemen veroorzaken, zoals deadlocks of geheugentoegangsproblemen in uw toepassing.

Wanneer u bijvoorbeeld een Concurrency Runtime-object maakt, maakt de runtime een standaardplanner voor u als deze nog niet is gemaakt. Een runtimeobject dat tijdens de globale objectconstructie wordt gemaakt, zorgt ervoor dat de runtime deze standaardplanner maakt. Dit proces heeft echter een interne vergrendeling, die de initialisatie van andere objecten kan verstoren die ondersteuning bieden voor de gelijktijdigheidsruntime-infrastructuur. Deze interne vergrendeling is mogelijk vereist voor een ander infrastructuurobject dat nog niet is geïnitialiseerd en kan er dus een impasse optreden in uw toepassing.

In het volgende voorbeeld ziet u het maken van een globaal gelijktijdigheidsobject::Scheduler . Dit patroon is niet alleen van toepassing op de Scheduler klasse, maar op alle andere typen die worden geleverd door de Gelijktijdigheidsruntime. U wordt aangeraden dit patroon niet te volgen, omdat dit onverwacht gedrag in uw toepassing kan veroorzaken.

// 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() 
{   
}

Zie Scheduler voor voorbeelden van de juiste manier om objecten te maken.

[Boven]

Gelijktijdigheidsobjecten niet gebruiken in gedeelde gegevenssegmenten

De Gelijktijdigheidsruntime biedt geen ondersteuning voor het gebruik van gelijktijdigheidsobjecten in een sectie met gedeelde gegevens, bijvoorbeeld een gegevenssectie die is gemaakt door de data_seg-instructie#pragma . Een gelijktijdigheidsobject dat wordt gedeeld over procesgrenzen, kan de runtime een inconsistente of ongeldige status hebben.

[Boven]

Zie ook

Best practices voor Concurrency Runtime
Bibliotheek met parallelle patronen (PPL)
Bibliotheek met asynchrone agents
Taakplanner
Synchronisatiegegevensstructuren
Synchronisatiegegevensstructuren vergelijken met de Windows-API
Hoe te: Alloc en Free gebruiken om de geheugenprestaties te verbeteren
Hoe te: Oversubscription gebruiken om latentie te compenseren
Procedure: De contextklasse gebruiken om een coöperatieve semafore te implementeren
Doorloop: Werk verwijderen uit een User-Interface thread
Aanbevolen procedures in de Parallelle Patronen Bibliotheek
Aanbevolen procedures in de bibliotheek met Asynchrone agents