Compartilhar via


Práticas recomendadas gerais no runtime de simultaneidade

Este documento descreve as práticas recomendadas que se aplicam a várias áreas do Runtime de Simultaneidade.

Seções

Este documento contém as seguintes seções:

Use constructos de sincronização cooperativa quando possível

O Runtime de Simultaneidade fornece muitas construções seguras de simultaneidade que não exigem um objeto de sincronização externa. Por exemplo, a classe concurrency::concurrent_vector fornece operações de acréscimo seguro de simultaneidade e de acesso a elementos. Aqui, concurrency-safe significa que os ponteiros ou iteradores são sempre válidos. Não é uma garantia de inicialização do elemento ou de uma ordem de passagem específica. No entanto, para casos em que você precisa de acesso exclusivo a um recurso, o runtime fornece as classes concurrency::critical_section, concurrency::reader_writer_lock e concurrency::event. Esses tipos se comportam cooperativamente; portanto, o agendador de tarefas pode realocar recursos de processamento para outro contexto à medida que a primeira tarefa aguarda dados. Quando possível, use esses tipos de sincronização em vez de outros mecanismos de sincronização, como os fornecidos pela API do Windows, que não se comportam de forma cooperativa. Para obter mais informações sobre esses tipos de sincronização e um exemplo de código, consulte Estruturas de dados de sincronização and Comparando estruturas de dados de sincronização com a API do Windows.

[Parte superior]

Evite tarefas demoradas que não rendem

Como o agendador de tarefas se comporta cooperativamente, ele não fornece imparcialidade entre as tarefas. Portanto, uma tarefa pode impedir que outras tarefas sejam iniciadas. Embora isso seja aceitável em alguns casos, em outros casos isso pode causar deadlock ou fome.

O exemplo a seguir executa mais tarefas do que o número de recursos de processamento alocados. A primeira tarefa não cede ao agendador de tarefas e, portanto, a segunda tarefa não é iniciada até que a primeira tarefa seja concluída.

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

Esse exemplo gera a saída a seguir:

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

Há várias maneiras de habilitar a cooperação entre as duas tarefas. Uma maneira é ocasionalmente ceder ao agendador de tarefas em uma tarefa de longa execução. O exemplo a seguir modifica a task função para chamar o método concurrency::Context::Yield para gerar a execução para o agendador de tarefas para que outra tarefa possa ser executada.

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

Esse exemplo gera a saída a seguir:

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

O método Context::Yield produz apenas outro thread ativo no agendador ao qual o thread atual pertence, uma tarefa leve ou outro thread do sistema operacional. Esse método não gera trabalho agendado para ser executado em um objeto concurrency::task_group ou concurrency::structured_task_group, mas ainda não foi iniciado.

Há outras maneiras de habilitar a cooperação entre tarefas de longa execução. Você pode dividir uma tarefa grande em subtarefas menores. Você também pode habilitar o excesso de assinatura durante uma tarefa longa. O excesso de assinaturas lhe permite criar mais threads do que o número de threads de hardware disponíveis. O excesso de assinatura é especialmente útil quando uma tarefa demorada contém uma alta quantidade de latência, por exemplo, a leitura de dados do disco ou de uma conexão de rede. Para obter mais informações sobre tarefas leves, confira Agendador de Tarefas.

[Parte superior]

Use o excesso de assinaturas para compensar operações que bloqueiam ou têm alta latência

O Runtime de Simultaneidade fornece primitivos de sincronização, como simultaneidade::critical_section, que permitem que as tarefas bloqueiem e produzam cooperativamente entre si. Quando uma tarefa bloqueia ou produz cooperativamente, o agendador de tarefas pode realocar recursos de processamento para outro contexto enquanto a primeira tarefa aguarda dados.

Há casos em que você não pode usar o mecanismo de bloqueio cooperativo fornecido pelo Runtime de Simultaneidade. Por exemplo, uma biblioteca externa que você usa pode usar um mecanismo de sincronização diferente. Outro exemplo é quando você executa uma operação que pode ter uma alta quantidade de latência, por exemplo, quando você usa a função de API do Windows ReadFile para ler dados de uma conexão de rede. Nesses casos, o excesso de assinatura pode permitir que outras tarefas sejam executadas quando outra tarefa estiver ociosa. O excesso de assinaturas lhe permite criar mais threads do que o número de threads de hardware disponíveis.

Considere a função a seguir, download, que baixa o arquivo na URL fornecida. Este exemplo usa o método concurrency::Context::Oversubscribe para aumentar temporariamente o número de threads ativos.

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

Como a função GetHttpFile executa uma operação potencialmente latente, o excesso de assinatura pode permitir que outras tarefas sejam executadas à medida que a tarefa atual aguarda dados. Para obter a versão completa deste exemplo, consulte Como usar o excesso de assinatura para compensar a latência.

[Parte superior]

Use funções simultâneas de gerenciamento de memória quando possível

Use as funções de gerenciamento de memória, simultaneidade::Alloc e simultaneidade::Gratuito, quando você tiver tarefas refinadas que frequentemente alocam objetos pequenos que têm um tempo de vida relativamente curto. O Runtime de Simultaneidade contém um cache de memória separado para cada thread em execução. As funções Alloc e Free alocam e liberam memória desses caches sem o uso de bloqueios ou barreiras de memória.

Para obter mais informações sobre essas funções de gerenciamento de memória, consulte o Agendador de Tarefas. Para obter um exemplo que usa essas funções, consulte Como usar o Alloc e o Free para melhorar o desempenho da memória.

[Parte superior]

Usar RAII para gerenciar o tempo de vida de objetos de simultaneidade

O Runtime de Simultaneidade usa tratamento de exceção para implementar recursos como cancelamento. Portanto, escreva um código seguro de exceção quando você chamar o runtime ou chamar outra biblioteca que chama o runtime.

O padrão RAII (Aquisição de recursos é inicialização) é uma maneira de gerenciar com segurança o tempo de vida de um objeto de simultaneidade em um determinado escopo. No padrão RAII, uma estrutura de dados é alocada na pilha. Essa estrutura de dados inicializa ou adquire um recurso quando ele é criado e destrói ou libera esse recurso quando a estrutura de dados é destruída. O padrão RAII garante que o destruidor seja chamado antes que o escopo delimitador seja encerrado. Esse padrão é útil quando uma função contém várias instruções return. Esse padrão também ajuda você a escrever código seguro contra exceções. Quando uma instrução throw faz com que a pilha seja descontraída, o destruidor do objeto RAII é chamado; portanto, o recurso é sempre excluído ou liberado corretamente.

O runtime define várias classes que usam o padrão RAII, por exemplo, simultaneidade::critical_section::scoped_lock e simultaneidade::reader_writer_lock::scoped_lock. Essas classes auxiliares são conhecidas como bloqueios com escopo. Essas classes oferecem vários benefícios quando você trabalha com objetos de simultaneidade::critical_section ou simultaneidade::reader_writer_lock. O construtor dessas classes adquire acesso ao objeto fornecido critical_section ou reader_writer_lock; o destruidor libera o acesso a esse objeto. Um bloqueio com escopo libera o acesso ao objeto de exclusão mútua automaticamente quando ele é destruído; portanto, você não desbloqueia manualmente o objeto subjacente.

Considere a classe a seguir, account, que é definida por uma biblioteca externa e, portanto, não pode ser modificada.

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

O exemplo a seguir executa várias transações em um objeto em paralelo account. O exemplo usa um objeto critical_section para sincronizar o acesso ao objeto account porque a classe account não é segura de simultaneidade. Cada operação paralela usa um objeto critical_section::scoped_lock para garantir que o objeto critical_section seja desbloqueado quando a operação for bem-sucedida ou falhar. Quando o saldo da conta é negativo, a operação withdraw falha gerando uma exceção.

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

Este exemplo gera a seguinte saída de amostra:

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

Para obter exemplos adicionais que usam o padrão RAII para gerenciar o tempo de vida de objetos de simultaneidade, consulte Passo a passo: removendo o trabalho de um thread de interface de usuário, Como usar a classe de contexto para implementar um semáforo cooperativo e Como usar o excesso de assinatura para compensar a latência.

[Parte superior]

Não criar objetos de simultaneidade no escopo global

Ao criar um objeto de simultaneidade no escopo global, você pode causar problemas como deadlock ou violações de acesso à memória em seu aplicativo.

Por exemplo, quando você cria um objeto Runtime de Simultaneidade, o runtime cria um agendador padrão para você se ainda não foi criado. Um objeto de runtime criado durante a construção de objeto global fará com que o runtime crie esse agendador padrão. No entanto, esse processo usa um bloqueio interno, que pode interferir na inicialização de outros objetos que dão suporte à infraestrutura de Runtime de Simultaneidade. Esse bloqueio interno pode ser exigido por outro objeto de infraestrutura que ainda não foi inicializado e, portanto, pode fazer com que o deadlock ocorra em seu aplicativo.

O exemplo a seguir demonstra a criação de um objeto global concurrency::Scheduler. Esse padrão se aplica não apenas à Scheduler classe, mas a todos os outros tipos fornecidos pelo Runtime de Simultaneidade. Recomendamos que você não siga esse padrão porque ele pode causar um comportamento inesperado em seu aplicativo.

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

Para obter exemplos da maneira correta de criar Scheduler objetos, consulte Agendador de Tarefas.

[Parte superior]

Não usar objetos de simultaneidade em segmentos de dados compartilhados

O Runtime de Simultaneidade não dá suporte ao uso de objetos de simultaneidade em uma seção de dados compartilhados, por exemplo, uma seção de dados criada pela diretiva data_seg#pragma. Um objeto de simultaneidade compartilhado entre os limites do processo pode colocar o runtime em um estado inconsistente ou inválido.

[Parte superior]

Confira também

Práticas recomendadas do runtime de simultaneidade
Biblioteca de padrões paralelos (PPL)
Biblioteca de agentes assíncronos
Agendador de Tarefas
Estruturas de dados de sincronização
Comparando estruturas de dados de sincronização com a API do Windows
Como usar Alloc e Free para melhorar o desempenho da memória
Como usar excesso de assinatura para compensar a latência
Como usar a classe de contexto para implementar um semáforo cooperativo
Instruções passo a passo: removendo trabalho de um thread de interface de usuário
Práticas recomendadas na biblioteca de padrões paralelos
Práticas recomendadas na biblioteca de agentes assíncronos