Поделиться через


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

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

Подразделы

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

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

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

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

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

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

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

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

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

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

[в начало]

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

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

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

// 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, но еще не запущенной.

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

[в начало]

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

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

[в начало]

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

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

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

[в начало]

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

[в начало]

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

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

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

В следующем примере показано создание глобального объекта 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 см. в разделе Планировщик задач (среда выполнения с параллелизмом).

[в начало]

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

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

[в начало]

См. также

Задачи

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Журнал изменений

Дата

Журнал

Причина

Март 2011

Добавлены сведения о возможных взаимоблокировках в глобальной области.

Обратная связь от клиента.