Obecné osvědčené postupy v Concurrency Runtime

Tento dokument popisuje osvědčené postupy, které platí pro více oblastí modulu Concurrency Runtime.

Oddíly

Tento dokument obsahuje následující části:

Pokud je to možné, použijte konstrukty synchronizace spolupráce.

Concurrency Runtime poskytuje mnoho konstruktorů bezpečných pro souběžnost, které nevyžadují objekt externí synchronizace. Například concurrency::concurrent_vector třída poskytuje operace připojení a přístupu elementů bezpečným pro souběžnost. V této chvíli souběžnost znamená, že ukazatele nebo iterátory jsou vždy platné. Nejedná se o záruku inicializace prvků ani konkrétního pořadí procházení. V případech, kdy však potřebujete výhradní přístup k prostředku, modul runtime poskytuje souběžnost::critical_section, concurrency::reader_writer_lock a concurrency::event classes. Tyto typy se chovají spolu; Plánovač úloh proto může prostředky zpracování relokovat do jiného kontextu, protože první úkol čeká na data. Pokud je to možné, použijte tyto typy synchronizace místo jiných synchronizačních mechanismů, například ty, které poskytuje rozhraní API systému Windows, které se nechovají spolu. Další informace o těchto typech synchronizace a příkladu kódu najdete v tématu Synchronizace datových struktur a porovnání synchronizačních datových struktur s rozhraním API systému Windows.

[Nahoře]

Vyhněte se zdlouhavým úkolům, které nepřinášejí

Vzhledem k tomu, že se plánovač úkolů chová spolu, neposkytuje nestrannost mezi úkoly. Proto může úkol zabránit spuštění jiných úkolů. I když je to v některých případech přijatelné, v jiných případech to může způsobit zablokování nebo hladovění.

Následující příklad provádí více úloh než počet přidělených prostředků zpracování. První úkol nepřináší plánovači úkolů, a proto se druhý úkol nespustí, dokud se nedokončí první úkol.

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

Tento příklad vytvoří následující výstup:

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

Existuje několik způsobů, jak umožnit spolupráci mezi těmito dvěma úkoly. Jedním ze způsobů je příležitostně přinést plánovači úloh v dlouhotrvající úloze. Následující příklad upraví task funkci tak, aby volala concurrency::Context::Yield metoda, aby bylo možné spustit provádění plánovači úloh, aby bylo možné spustit jinou úlohu.

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

Tento příklad vytvoří následující výstup:

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

Metoda Context::Yield poskytuje pouze jiné aktivní vlákno v plánovači, do kterého aktuální vlákno patří, lehká úloha nebo jiné vlákno operačního systému. Tato metoda nepřináší práci, která je naplánovaná ke spuštění v souběžnosti::task_group nebo souběžnosti::structured_task_group objektu , ale ještě nebyla spuštěna.

Existují i další způsoby, jak umožnit spolupráci mezi dlouhotrvajícími úkoly. Velký úkol můžete rozdělit na menší dílčí úkoly. Můžete také povolit přesazení během zdlouhavého úkolu. Oversubscription umožňuje vytvořit více vláken než dostupný počet hardwarových vláken. Oversubscription je zvláště užitečný v případě, že zdlouhavá úloha obsahuje vysokou latenci, například čtení dat z disku nebo síťového připojení. Další informace o odlehčených úkolech a nadsazení najdete v tématu Plánovač úloh.

[Nahoře]

Použití oversubscription k posunu operací, které blokují nebo mají vysokou latenci

Concurrency Runtime poskytuje primitivy synchronizace, jako je souběžnost::critical_section, které umožňují úlohám vzájemně blokovat a poskytovat je. Když jeden úkol spolupracující blokuje nebo přináší, plánovač úkolů může relokovat prostředky zpracování do jiného kontextu, protože první úkol čeká na data.

Existují případy, kdy nemůžete použít mechanismus blokování spolupráce poskytovaný modulem Concurrency Runtime. Například externí knihovna, kterou používáte, může používat jiný synchronizační mechanismus. Dalším příkladem je provedení operace, která může mít vysokou latenci, například při použití funkce rozhraní API ReadFile systému Windows ke čtení dat ze síťového připojení. V těchto případech může oversubscription povolit spuštění jiných úloh, když je jiný úkol nečinný. Oversubscription umožňuje vytvořit více vláken než dostupný počet hardwarových vláken.

Zvažte následující funkci, downloadkterá stáhne soubor na danou adresu URL. V tomto příkladu se používá souběžnost::Context::Oversubscribe metoda k dočasnému zvýšení počtu aktivních vláken.

// 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 Vzhledem k tomu, že funkce provádí potenciálně latentní operaci, může oversubscription umožnit spuštění jiných úloh jako aktuální úkol čekání na data. Kompletní verzi tohoto příkladu najdete v tématu Postupy: Použití oversubscription k posunu latence.

[Nahoře]

Pokud je to možné, používejte funkce souběžné správy paměti.

Funkce správy paměti, souběžnost::Alloc a souběžnost::Free, pokud máte jemně odstupňované úlohy, které často přidělují malé objekty s relativně krátkou životností. Modul Concurrency Runtime obsahuje samostatnou mezipaměť paměti pro každé spuštěné vlákno. Free Funkce Alloc přidělují a uvolní paměť z těchto mezipamětí bez použití zámků nebo paměťových bariér.

Další informace o těchto funkcích správy paměti naleznete v tématu Plánovač úloh. Příklad, který používá tyto funkce, naleznete v tématu Postupy: Použití funkce Alloc a Free ke zlepšení výkonu paměti.

[Nahoře]

Použití RAII ke správě životnosti objektů souběžnosti

Concurrency Runtime používá zpracování výjimek k implementaci funkcí, jako je zrušení. Proto zapište kód bezpečný pro výjimky při volání do modulu runtime nebo volání jiné knihovny, která volá modul runtime.

Model Inicializace prostředků (RAII) je jedním ze způsobů, jak bezpečně spravovat životnost objektu souběžnosti v rámci daného oboru. V rámci vzoru RAII je datová struktura přidělena v zásobníku. Tato datová struktura inicializuje nebo získá prostředek při jeho vytvoření a zničí nebo uvolní tento prostředek při zničení datové struktury. Vzor RAII zaručuje, že destruktor je volána před uzavřením rozsahu. Tento vzor je užitečný, pokud funkce obsahuje více return příkazů. Tento vzor vám také pomůže napsat kód bezpečný pro výjimky. throw Když příkaz způsobí uvolnění zásobníku, je volána destruktor objektu RAII; proto je prostředek vždy správně odstraněn nebo uvolněn.

Modul runtime definuje několik tříd, které používají vzor RAII, například concurrency:::critical_section::scoped_lock a souběžnost::reader_writer_lock::scoped_lock. Tyto pomocné třídy se označují jako zámky s vymezeným oborem. Tyto třídy poskytují několik výhod při práci s concurrency::critical_section nebo concurrency::reader_writer_lock objekty. Konstruktor těchto tříd získává přístup k poskytnutému critical_section objektu nebo reader_writer_lock objektu; destruktor uvolní přístup k danému objektu. Vzhledem k tomu, že vymezený zámek uvolní přístup k objektu vzájemného vyloučení automaticky při jeho zničení, neodemykání základního objektu ručně.

Zvažte následující třídu, accountkterá je definována externí knihovnou, a proto ji nelze změnit.

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

Následující příklad provádí více transakcí na account objekt paralelně. Příklad používá critical_section objekt k synchronizaci přístupu k objektu account , protože account třída není concurrency-safe. Každá paralelní operace používá critical_section::scoped_lock objekt k zajištění toho, že critical_section je objekt odemknut, když operace proběhne úspěšně nebo selže. Pokud je zůstatek účtu záporný, withdraw operace selže vyvoláním výjimky.

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

Tento příklad vytvoří následující ukázkový výstup:

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

Další příklady, které používají vzor RAII ke správě životnosti objektů souběžnosti, naleznete v části Návod: Odebrání práce z vlákna uživatelského rozhraní, Postupy: Použití třídy kontextu k implementaci kooperativní semaphore a Postupy: Použití oversubscription k posunu latence.

[Nahoře]

Nevytváření objektů souběžnosti v globálním oboru

Při vytváření objektu souběžnosti v globálním oboru můžete způsobit problémy, jako je zablokování nebo narušení přístupu k paměti ve vaší aplikaci.

Když například vytvoříte objekt Concurrency Runtime, modul runtime pro vás vytvoří výchozí plánovač, pokud ještě nebyl vytvořen. Objekt modulu runtime, který se vytvoří během globálního vytváření objektů, odpovídajícím způsobem způsobí, že modul runtime vytvoří tento výchozí plánovač. Tento proces však přebírá vnitřní zámek, který může narušit inicializaci jiných objektů, které podporují infrastrukturu Concurrency Runtime. Tento vnitřní zámek může vyžadovat jiný objekt infrastruktury, který ještě nebyl inicializován, a může tak způsobit zablokování ve vaší aplikaci.

Následující příklad ukazuje vytvoření globální souběžnosti::Scheduler objektu. Tento model platí nejen pro Scheduler třídu, ale i všechny ostatní typy, které poskytuje Concurrency Runtime. Doporučujeme, abyste tento vzor nepoužádli, protože může způsobit neočekávané chování ve vaší aplikaci.

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

Příklady správného způsobu vytváření Scheduler objektů naleznete v tématu Plánovač úloh.

[Nahoře]

Nepoužívejte objekty souběžnosti ve sdílených datových segmentech.

Concurrency Runtime nepodporuje použití objektů souběžnosti ve sdílené datové části, například datový oddíl vytvořený direktivou data_seg#pragma . Objekt souběžnosti sdílený přes hranice procesu může modul runtime umístit do nekonzistentního nebo neplatného stavu.

[Nahoře]

Viz také

Osvědčené postupy v Concurrency Runtime
Knihovna PPL (Parallel Patterns Library)
Knihovna asynchronních agentů
Plánovač úloh
Synchronizační datové struktury
Porovnávání synchronizačních datových struktur s rozhraním API systému Windows
Postupy: Použití funkcí Alloc a Free ke zlepšení výkonu paměti
Postupy: Kompenzace latence vytvořením nadbytečného počtu vláken
Postupy: Použití třídy kontextu pro implementaci semaforu pro spolupráci
Návod: Odebrání práce z vlákna uživatelského rozhraní
Osvědčené postupy v knihovně PPL (Parallel Patterns Library)
Osvědčené postupy v knihovně asynchronních agentů