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

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

Разделы

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

Использование конструкций совместной синхронизации, когда это возможно

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

[В начало]

Избегайте длительных задач, которые не дают

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

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

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

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

[В начало]

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

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

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

Рассмотрим следующую функцию, downloadкоторая скачивает файл по указанному URL-адресу. В этом примере используется метод параллелизма::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 Так как функция выполняет потенциально скрытую операцию, oversubscription может позволить другим задачам выполняться в качестве текущей задачи ожидания данных. Полный вариант этого примера см. в разделе "Практическое руководство. Использование Oversubscription для смещения задержки".

[В начало]

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

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

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

[В начало]

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

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

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

Среда выполнения определяет несколько классов, использующих шаблон RAII, например параллелизм::critical_section:::областьd_lock и параллелизм:::reader_writer_lock::областьd_lock. Эти вспомогательные классы называются область блокировками. Эти классы предоставляют несколько преимуществ при работе с параллелизмом::critical_section или параллелизмом::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 для объекта параллельно. В примере используется critical_section объект для синхронизации доступа к account объекту, так как 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 для управления временем существования объектов параллелизма, см . в пошаговом руководстве. Удаление работы из потока пользовательского интерфейса, практическое руководство. Использование класса Контекста для реализации кооперативного Семафора и практическое руководство. Использование oversubscription для смещения задержки.

[В начало]

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

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

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

В следующем примере показано создание глобального объекта параллелизма::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 . Объект параллелизма, совместно используемый между границами процесса, может поместить среду выполнения в несогласованное или недопустимое состояние.

[В начало]

См. также

Рекомендации по работе со средой выполнения с параллелизмом
Библиотека параллельных шаблонов
Библиотека асинхронных агентов
Планировщик заданий
Структуры данных синхронизации
Сравнение структур данных синхронизации с интерфейсом Windows API
Практическое руководство. Использование функций Alloc и Free для повышения производительности операций с памятью
Практическое руководство. Использование лимита подписки для устранения задержек
Практическое руководство. Использование класса Context для реализации семафора, поддерживающего параллельный доступ
Пошаговое руководство. Удаление задач из потока пользовательского интерфейса
Рекомендации по работе с библиотекой параллельных шаблонов
Рекомендации по работе с библиотекой асинхронных агентов