Współbieżność środowiska wykonawczego — Najlepsze praktyki ogólne

W tym dokumencie opisano najlepsze rozwiązania dotyczące wielu obszarów środowiska uruchomieniowego współbieżności.

Sekcje

Ten dokument zawiera następujące sekcje:

Korzystanie z konstrukcji synchronizacji spółdzielczej, jeśli jest to możliwe

Środowisko uruchomieniowe współbieżności zapewnia wiele konstrukcji bezpiecznych współbieżności, które nie wymagają obiektu synchronizacji zewnętrznej. Na przykład klasa concurrency::concurrent_vector zapewnia bezpieczne dołączanie współbieżności i operacje dostępu do elementów. W tym miejscu wskaźniki lub iteratory są zawsze prawidłowe. Nie jest to gwarancja inicjowania elementów ani określonej kolejności przechodzenia. Jednak w przypadku, gdy wymagane jest wyłączny dostęp do zasobu, środowisko uruchomieniowe zapewnia współbieżność::critical_section, współbieżność::reader_writer_lock i klasy współbieżności::event . Te typy zachowują się wspólnie; Dlatego harmonogram zadań może ponownie przydzielić zasoby przetwarzania do innego kontekstu, ponieważ pierwsze zadanie czeka na dane. Jeśli to możliwe, użyj tych typów synchronizacji zamiast innych mechanizmów synchronizacji, takich jak te udostępniane przez interfejs API systemu Windows, które nie zachowują się wspólnie. Aby uzyskać więcej informacji o tych typach synchronizacji i przykładzie kodu, zobacz Synchronizacja struktur danych i Porównywanie struktur danych synchronizacji z interfejsem API systemu Windows.

[Top]

Unikaj długich zadań, które nie dają

Ponieważ harmonogram zadań działa kooperacyjnie, nie zapewnia sprawiedliwości między zadaniami. W związku z tym zadanie może uniemożliwić uruchamianie innych zadań. Chociaż jest to dopuszczalne w niektórych przypadkach, w innych przypadkach może to spowodować zakleszczenie lub głodu.

Poniższy przykład wykonuje więcej zadań niż liczba przydzielonych zasobów przetwarzania. Pierwsze zadanie nie zwraca się do harmonogramu zadań, dlatego drugie zadanie nie jest uruchamiane do momentu zakończenia pierwszego zadania.

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

Ten przykład generuje następujące wyniki:

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

Istnieje kilka sposobów umożliwienia współpracy między dwoma zadaniami. Jednym ze sposobów jest sporadyczne zwracanie się do harmonogramu zadań w długotrwałym zadaniu. Poniższy przykład modyfikuje task funkcję w celu wywołania współbieżności::Context::Yield metody w celu uzyskania wykonania do harmonogramu zadań, aby można było uruchomić inne zadanie.

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

Ten przykład generuje następujące wyniki:

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

Metoda Context::Yield daje tylko inny aktywny wątek w harmonogramie, do którego należy bieżący wątek, lekkie zadanie lub inny wątek systemu operacyjnego. Ta metoda nie daje pracy zaplanowanej do uruchomienia w współbieżności::task_group lub współbieżności::structured_task_group obiektu, ale nie został jeszcze uruchomiony.

Istnieją inne sposoby umożliwienia współpracy między długotrwałymi zadaniami. Duże zadanie można podzielić na mniejsze podzadania. Możesz również włączyć nadsubskrypcję podczas długiego zadania. Zasubskrypcja umożliwia tworzenie większej liczby wątków niż dostępna liczba wątków sprzętowych. Zasubskrypcja jest szczególnie przydatna, gdy długie zadanie zawiera duże opóźnienie, na przykład odczytywanie danych z dysku lub z połączenia sieciowego. Aby uzyskać więcej informacji na temat uproszczonych zadań i nadsubskrypcji, zobacz Harmonogram zadań.

[Top]

Użyj przesubskrypcji, aby zrównoważyć operacje, które blokują lub mają duże opóźnienie

Środowisko uruchomieniowe współbieżności udostępnia typy pierwotne synchronizacji, takie jak współbieżność::critical_section, które umożliwiają zadania współpracy blokowania i zwracania się do siebie nawzajem. Gdy jedno zadanie wspólnie blokuje lub daje, harmonogram zadań może ponownie przydzielać zasoby przetwarzania do innego kontekstu, ponieważ pierwsze zadanie czeka na dane.

Istnieją przypadki, w których nie można użyć mechanizmu blokowania współpracy, który jest udostępniany przez środowisko uruchomieniowe współbieżności. Na przykład używana biblioteka zewnętrzna może używać innego mechanizmu synchronizacji. Innym przykładem jest wykonanie operacji, która może mieć duże opóźnienie, na przykład w przypadku używania funkcji interfejsu API ReadFile systemu Windows do odczytywania danych z połączenia sieciowego. W takich przypadkach zasubskrypcja może umożliwić uruchamianie innych zadań, gdy inne zadanie jest bezczynne. Zasubskrypcja umożliwia tworzenie większej liczby wątków niż dostępna liczba wątków sprzętowych.

Rozważmy następującą funkcję , downloadktóra pobiera plik pod podanym adresem URL. W tym przykładzie użyto metody concurrency::Context::Oversubscribe , aby tymczasowo zwiększyć liczbę aktywnych wątków.

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

GetHttpFile Ponieważ funkcja wykonuje potencjalnie ukryte operacje, zawyżanie może umożliwić wykonywanie innych zadań jako bieżące zadanie czeka na dane. Aby uzyskać pełną wersję tego przykładu, zobacz How to: Use Oversubscription to Offset Latency (Instrukcje: używanie przesubskrypcji do przesunięcia opóźnienia).

[Top]

Korzystanie z funkcji zarządzania pamięcią współbieżną, jeśli jest to możliwe

Użyj funkcji zarządzania pamięcią, współbieżności::Alloc i współbieżności::Bezpłatna, gdy masz szczegółowe zadania, które często przydzielają małe obiekty o stosunkowo krótkim okresie istnienia. Środowisko uruchomieniowe współbieżności przechowuje oddzielną pamięć podręczną dla każdego uruchomionego wątku. Funkcje Alloc i Free przydzielają i zwalniają pamięć z tych pamięci podręcznych bez użycia blokad lub barier pamięci.

Aby uzyskać więcej informacji na temat tych funkcji zarządzania pamięcią, zobacz Harmonogram zadań. Przykład użycia tych funkcji można znaleźć w temacie How to: Use Alloc and Free to Improve Memory Performance (Instrukcje: Używanie funkcji Alloc i Free w celu zwiększenia wydajności pamięci).

[Top]

Zarządzanie okresem istnienia obiektów współbieżności za pomocą RAII

Środowisko uruchomieniowe współbieżności używa obsługi wyjątków do implementowania funkcji, takich jak anulowanie. W związku z tym należy napisać kod bezpieczny dla wyjątków podczas wywoływania środowiska uruchomieniowego lub wywoływania innej biblioteki wywołującej środowisko uruchomieniowe.

Wzorzec pozyskiwania zasobów to inicjowanie (RAII, Resource Acquisition Is Initialization ) to jeden ze sposobów bezpiecznego zarządzania okresem istnienia obiektu współbieżności w danym zakresie. W ramach wzorca RAII struktura danych jest przydzielana na stosie. Ta struktura danych inicjuje lub uzyskuje zasób podczas jego tworzenia i niszczy lub zwalnia ten zasób, gdy struktura danych zostanie zniszczona. Wzorzec RAII gwarantuje, że destruktor jest wywoływany przed zamknięciem zakresu otaczającego. Ten wzorzec jest przydatny, gdy funkcja zawiera wiele return instrukcji. Ten wzorzec ułatwia również pisanie kodu bezpiecznego dla wyjątków. throw Gdy instrukcja powoduje odwijanie stosu, wywoływany jest destruktor obiektu RAII. W związku z tym zasób jest zawsze poprawnie usuwany lub zwalniany.

Środowisko uruchomieniowe definiuje kilka klas, które używają wzorca RAII, na przykład współbieżność::critical_section::scoped_lock i współbieżność::reader_writer_lock::scoped_lock. Te klasy pomocnicze są nazywane blokadami o określonym zakresie. Te klasy zapewniają kilka korzyści podczas pracy z obiektami concurrency::critical_section lub concurrency::reader_writer_lock . Konstruktor tych klas uzyskuje dostęp do dostarczonego critical_section obiektu lub reader_writer_lock ; destruktor zwalnia dostęp do tego obiektu. Ponieważ blokada o określonym zakresie zwalnia dostęp do obiektu wzajemnego wykluczania automatycznie, gdy zostanie zniszczony, nie odblokuj obiektu bazowego ręcznie.

Rozważmy następującą klasę , accountktóra jest zdefiniowana przez bibliotekę zewnętrzną i dlatego nie można jej modyfikować.

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

Poniższy przykład wykonuje wiele transakcji na account obiekcie równolegle. W przykładzie użyto critical_section obiektu do synchronizacji dostępu do account obiektu, ponieważ account klasa nie jest bezpieczna współbieżności. Każda operacja równoległa używa critical_section::scoped_lock obiektu w celu zagwarantowania, że critical_section obiekt jest odblokowany, gdy operacja zakończy się powodzeniem lub niepowodzeniem. Gdy saldo konta jest ujemne, operacja kończy się niepowodzeniem, withdraw zgłaszając wyjątek.

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

W tym przykładzie są generowane następujące przykładowe dane wyjściowe:

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

Aby zapoznać się z dodatkowymi przykładami, które używają wzorca RAII do zarządzania okresem istnienia obiektów współbieżności, zobacz Przewodnik: usuwanie pracy z wątku interfejsu użytkownika, Instrukcje: używanie klasy kontekstu do implementowania semafora spółdzielczego i Instrukcje: używanie przesubskrypcji do przesunięcia opóźnienia.

[Top]

Nie twórz obiektów współbieżności w zakresie globalnym

Podczas tworzenia obiektu współbieżności w zakresie globalnym mogą wystąpić problemy, takie jak zakleszczenie lub naruszenia dostępu do pamięci w aplikacji.

Na przykład podczas tworzenia obiektu środowiska uruchomieniowego współbieżności środowisko uruchomieniowe tworzy domyślny harmonogram, jeśli jeszcze nie został utworzony. Obiekt środowiska uruchomieniowego, który jest tworzony podczas tworzenia obiektów globalnych, odpowiednio spowoduje utworzenie tego domyślnego harmonogramu przez środowisko uruchomieniowe. Jednak ten proces przyjmuje blokadę wewnętrzną, która może zakłócać inicjowanie innych obiektów obsługujących infrastrukturę środowiska uruchomieniowego współbieżności. Ta wewnętrzna blokada może być wymagana przez inny obiekt infrastruktury, który nie został jeszcze zainicjowany, a tym samym może spowodować zakleszczenie w aplikacji.

W poniższym przykładzie pokazano tworzenie globalnego obiektu współbieżności::Scheduler . Ten wzorzec dotyczy nie tylko Scheduler klasy, ale także wszystkich innych typów udostępnianych przez środowisko uruchomieniowe współbieżności. Zalecamy, aby nie przestrzegać tego wzorca, ponieważ może to spowodować nieoczekiwane zachowanie w aplikacji.

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

Przykłady poprawnego sposobu tworzenia Scheduler obiektów można znaleźć w temacie Harmonogram zadań.

[Top]

Nie używaj obiektów współbieżności w udostępnionych segmentach danych

Środowisko uruchomieniowe współbieżności nie obsługuje używania obiektów współbieżności w sekcji danych udostępnionych, na przykład sekcji danych utworzonej przez dyrektywę data_seg#pragma . Obiekt współbieżności, który jest współużytkowany przez granice procesów, może umieścić środowisko uruchomieniowe w niespójnym lub nieprawidłowym stanie.

[Top]

Zobacz też

Środowisko uruchomieniowe współbieżności — najlepsze praktyki
Biblioteka równoległych wzorców (PLL)
Biblioteki agentów asynchronicznych
Harmonogram zadań
Struktury danych synchronizacji
Porównywanie struktur danych synchronizacji z Windows API
Instrukcje: używanie z funkcji Alloc i Free do poprawiania wydajności pamięci
Instrukcje: używanie nadsubskrypcji do przesuwania opóźnienia
Instrukcje: korzystanie z klasy kontekstu do wdrażania a kooperatywnego semafora
Przewodnik: usuwanie pracy z wątku interfejs użytkownika
Biblioteka wzorów równoległych — najlepsze praktyki
Biblioteka agentów asynchronicznych — najlepsze praktyki