Общие рекомендации в среде выполнения с параллелизмом

В этом документе приведены рекомендации по работе с различными областями среды выполнения с параллелизмом.

Подразделы

Этот документ содержит следующие разделы.

  • При возможности используйте конструкции совместной синхронизации

  • Избегайте продолжительных задач, не выполняющих передачу

  • Используйте превышение лимита подписки для смещения заблокированных операций или операций с высокой задержкой

  • При возможности используйте функции параллельного управления памятью

  • Используйте RAII для управления временем существования параллельных объектов

  • Не создавайте параллельные объекты в глобальной области

  • Не используйте параллельные объекты в сегментах данных совместного доступа

При возможности используйте конструкции совместной синхронизации

Среда выполнения с параллелизмом предоставляет множество безопасных в режиме параллелизма структур, не требующих внешнего объекта синхронизации.Например concurrency::concurrent_vector класс предоставляет append параллелизма строго типизированным и операций доступа к элементу.Тем не менее, предоставляет среду выполнения для случаев, когда требуется монопольный доступ к ресурсу, concurrency::critical_section, concurrency::reader_writer_lock, и concurrency::event классы.Эти типы предназначены для совместной работы, поэтому планировщик заданий может перераспределять ресурсы обработки в другой контекст, пока первая задача ожидает получения данных.При возможности следует использовать эти типы синхронизации, а не другие механизмы, например предоставляемые Windows API, не предназначенные для совместной работы.Дополнительные сведения об этих типах синхронизации и пример кода см. в разделах Структуры данных синхронизации и Сравнение структур данных синхронизации с интерфейсом Windows API.

Top

Избегайте продолжительных задач, не выполняющих передачу

Так как планировщик заданий работает совместно, он не обеспечивает балансировку ресурсов между задачами.Поэтому задача может предотвратить запуск других задач.В некоторых случаях это допустимо, но иногда это может привести к взаимоблокировке или истощению ресурсов.

В следующем примере количество выполняемых задач больше, чем количество выделенных ресурсов обработки.Первая задача не выполняет передачу планировщику заданий, поэтому вторая задача не запускается до завершения первой.

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

В этом примере получается следующий результат:

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

Есть несколько способов обеспечить совместную работу двух задач.Один — выполнить передачу продолжительной задачи планировщику заданий.В следующем примере изменяется task функции для вызова concurrency::Context::Yield метод для выполнения планировщик задач таким образом, можно выполнить другую задачу.

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

В этом примере получается следующий результат:

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

Метод Context::Yield выполняет передачу только другому активному потоку в планировщике, к которому относится текущий поток, упрощенной задаче или потоку другой операционной системы.Этот метод не дает работать по расписанию в concurrency::task_group или concurrency::structured_task_group объекта, но еще не запущена.

Есть другие способы обеспечить совместную работу продолжительных задач.Можно разделить большие задачи на меньшие подзадачи.Также можно разрешить превышение лимита подписки при выполнении продолжительной задачи.Превышение лимита подписки позволяет создать больше потоков, чем количество доступных аппаратных потоков.Превышение лимита подписки особенно полезно, если продолжительная задача связана с большими задержками, например при считывании данных с диска или сетевого ресурса.Дополнительные сведения об упрощенных задачах и превышении лимита подписки см. в разделе Планировщик задач (среда выполнения с параллелизмом).

Top

Используйте превышение лимита подписки для смещения заблокированных операций или операций с высокой задержкой

Одновременности выполнения предоставляет примитивы синхронизации, такие как concurrency::critical_section, выполнять задачи в налаживании блокировать и выдают друг с другом.Когда одна задача выполняет совместную блокировку или передачу, планировщик заданий может перераспределять ресурсы обработки в другой контекст, пока первая задача ожидает получения данных.

В некоторых случаях нельзя использовать механизм совместной блокировки, предоставленный средой выполнения с параллелизмом.Например, может использоваться внешняя библиотека, применяющая другой механизм синхронизации.Еще один пример — выполнение операции, при которой возможны большие задержки, например использование функции Windows API ReadFile для чтения данных с сетевого ресурса.В этих случаях превышение лимита подписки может позволить выполнять другие задачи, пока текущая задача неактивна.Превышение лимита подписки позволяет создать больше потоков, чем количество доступных аппаратных потоков.

Рассмотрим следующую функцию, download, загружающую файл с данного URL-адреса.В этом примере используется concurrency::Context::Oversubscribe метод временно увеличить число активных потоков.

// 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 выполняет операцию с возможностью задержек, превышение лимита подписки может позволить выполнять другие задачи, пока текущая задача ожидает получения данных.Полную версию этого примера см. в разделе Практическое руководство. Использование лимита подписки для устранения задержек.

Top

При возможности используйте функции параллельного управления памятью

Использование функций управления памятью, concurrency::Alloc и concurrency::Free, при наличии детализированное задачи, которые часто выделения небольших объектов, которые имеют относительно короткий срок жизни.Среда выполнения с параллелизмом содержит отдельный кэш памяти для каждого выполняющегося потока.Функции Alloc и Free выделяют и высвобождают память из такого кэша, не используя блокировки или барьеры памяти.

Дополнительные сведения об этих функциях управления памятью см. в разделе Планировщик задач (среда выполнения с параллелизмом).Пример использования этих функций см. в разделе Практическое руководство. Использование функций Alloc и Free для повышения производительности памяти.

Top

Используйте RAII для управления временем существования параллельных объектов

Среда выполнения с параллелизмом использует обработку исключений для реализации таких функций, как отмена.Поэтому при выполнении вызовов в среду выполнения или в другую библиотеку, которая выполняет вызовы в среду выполнения, следует писать код, безопасный в отношении исключений.

Шаблон Получение ресурса есть инициализация (RAII) — один из способов безопасного управления временем существования параллельных объектов в данной области.При использовании шаблона RAII структура данных располагается в стеке.Эта структура данных инициализирует или приобретает ресурс во время создания структуры и уничтожает или освобождает ресурс во время уничтожения структуры данных.Шаблон RAII гарантирует, что деструктор вызывается до выхода из внешней области видимости.Этот шаблон полезен, если функция содержит несколько операторов return.Этот шаблон также помогает писать код, безопасный в отношении исключений.Когда оператор throw вызывает раскручивание стека, вызываются деструкторы объекта RAII; поэтому ресурс всегда правильно удаляется или высвобождается.

Среда выполнения определяет несколько классов, например, использовать шаблон RAII concurrency::critical_section::scoped_lock и concurrency::reader_writer_lock::scoped_lock.Эти вспомогательные классы называются блокировками с областью.Эти классы предоставляют ряд преимуществ при работе с concurrency::critical_section или concurrency::reader_writer_lock объектов.Конструктор этих классов получает доступ к предоставленному объекту critical_section или reader_writer_lock; деструктор высвобождает доступ к этому объекту.Так как блокировка с областью высвобождает доступ к своему взаимоисключающему объекту автоматически при ее уничтожении, не требуется вручную разблокировать основной объект.

Рассмотрим следующий класс, account, который нельзя изменять, так как он определен внешней библиотекой.

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

В следующем примере параллельно выполняется несколько транзакций в объекте account.В примере для синхронизации доступа к объекту account используется объект critical_section, так как класс account не является безопасным в режиме параллелизма.Каждая параллельная операция использует объект critical_section::scoped_lock, чтобы разблокировать объект critical_section после выполнения операции (ее успеха или сбоя).Если баланс счета отрицательный, операция withdraw дает сбой и создает исключение.

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

В данном примере получается следующий результат.

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

Дополнительные примеры, использующие шаблон RAII для управления временем существования параллельных объектов, см. в разделах Пошаговое руководство. Удаление задач из потоков пользовательского интерфейса, Практическое руководство. Использование класса Context для реализации семафора, поддерживающего параллельный доступ и Практическое руководство. Использование лимита подписки для устранения задержек.

Top

Не создавайте параллельные объекты в глобальной области

При создании объекта параллелизма в глобальной области видимости нарушения прав доступа в приложении может вызвать проблемы, например взаимоблокировки или памяти.

Например при создании объекта параллелизма во время выполнения планировщика по умолчанию среда выполнения создает Если один еще не создан.Объект общеязыковой среды выполнения, который создается во время создания глобального объекта приведет к соответствующим образом во время выполнения для создания этого планировщика по умолчанию.Однако этот процесс занимает внутренней блокировки, что может помешать инициализации других объектов, которые поддерживают инфраструктуру времени выполнения параллельной обработки.Это внутренняя блокировка может быть необходим другой объект инфраструктуры, который еще не был инициализирован и таким образом может вызвать взаимоблокировку в приложении.

В следующем примере демонстрируется создание глобальной concurrency::Scheduler объект.Этот шаблон применяется не только к Scheduler классов, но другие типы, предоставляемые средой выполнения параллельной обработки.Мы рекомендуем не следовать этому шаблону, так как он может вызвать неожиданное поведение приложения.

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

Примеры правильного создания объектов Scheduler см. в разделе Планировщик задач (среда выполнения с параллелизмом).

Top

Не используйте параллельные объекты в сегментах данных совместного доступа

Среда выполнения с параллелизмом не поддерживает использование параллельных объектов в разделах данных совместного доступа, например разделах данных, созданных директивой data_seg#pragma.Параллельный объект, использующийся совместно в нескольких процессах, может привести к несогласованному или недопустимому состоянию среды выполнения.

Top

См. также

Задачи

Практическое руководство. Использование функций Alloc и Free для повышения производительности памяти

Практическое руководство. Использование лимита подписки для устранения задержек

Практическое руководство. Использование класса Context для реализации семафора, поддерживающего параллельный доступ

Пошаговое руководство. Удаление задач из потоков пользовательского интерфейса

Основные понятия

Библиотека параллельных шаблонов

Библиотека асинхронных агентов

Планировщик задач (среда выполнения с параллелизмом)

Структуры данных синхронизации

Сравнение структур данных синхронизации с интерфейсом Windows API

Рекомендации по работе с библиотекой параллельных шаблонов

Рекомендации по работе с библиотекой асинхронных агентов

Другие ресурсы

Рекомендации по работе со средой выполнения с параллелизмом