Partilhar via


Práticas recomendadas gerais no tempo de execução da simultaneidade

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

Secções

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

Usar construções de sincronização cooperativa quando possível

O Concurrency Runtime fornece muitas construções seguras para simultaneidade que não exigem um objeto de sincronização externo. Por exemplo, a classe concurrency::concurrent_vector fornece operações de acréscimo e acesso a elementos seguras para simultaneidade. Aqui, simultaneidade segura significa que ponteiros ou iteradores são sempre válidos. Não é uma garantia de inicialização de elementos ou de uma ordem transversal específica. No entanto, para casos em que você precisa de acesso exclusivo a um recurso, o tempo de execução fornece as classes concurrency::critical_section, concurrency::reader_writer_lock e concurrency::event . Estes tipos comportam-se de forma cooperativa; 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 cooperativamente. Para obter mais informações sobre esses tipos de sincronização e um exemplo de código, consulte Estruturas de dados de sincronização e Comparando estruturas de dados de sincronização com a API do Windows.

[Topo]

Evite tarefas longas que não rendem

Como o agendador de tarefas se comporta de forma cooperativa, ele não fornece equidade entre as tarefas. Portanto, uma tarefa pode impedir que outras tarefas sejam iniciadas. Embora isso seja aceitável em alguns casos, em outros casos pode causar impasse 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 termine.

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

Este exemplo produz a seguinte saída:

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

Existem várias formas de permitir a cooperação entre as duas tarefas. Uma maneira é ceder ocasionalmente ao agendador de tarefas em uma tarefa de longa execução. O exemplo a seguir modifica a função task para chamar o método concorrência::Contexto::Yield para ceder a execução ao agendador de tarefas, permitindo que outra tarefa seja 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();
}

Este exemplo produz a seguinte saída:

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

O Context::Yield método 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 cede ao trabalho agendado para ser executado em um objeto de concorrência::task_group ou concorrência::structured_task_group, mas que ainda não foi iniciado.

Existem outras formas de permitir a cooperação entre tarefas de longa duração. Você pode dividir uma tarefa grande em subtarefas menores. Você também pode habilitar a assinatura excessiva durante uma tarefa demorada. A sobresubscrição permite-lhe criar mais threads do que o número disponível de threads de hardware. A sobresubscrição é especialmente útil quando uma tarefa longa tem uma grande quantidade de latência, por exemplo, na leitura de dados a partir de um disco ou de uma conexão de rede. Para obter mais informações sobre tarefas leves e excesso de assinatura, consulte Agendador de tarefas.

[Topo]

Utilize a "sobresubscrição" para compensar operações que bloqueiam ou têm alta latência.

O Concurrency Runtime fornece primitivas de sincronização, como concurrency::critical_section, que permitem que as tarefas bloqueiem e cedam umas às outras cooperativamente. Quando uma tarefa bloqueia ou cede cooperativamente, o agendador de tarefas pode realocar recursos de processamento para outro ambiente enquanto a primeira tarefa aguarda por dados.

Há casos em que você não pode usar o mecanismo de bloqueio cooperativo fornecido pelo Concurrency Runtime. 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 ReadFile do Windows para ler dados de uma conexão de rede. Nesses casos, a sobreassinatura pode permitir que outras tarefas sejam executadas quando outra tarefa estiver ociosa. A sobresubscrição permite-lhe criar mais threads do que o número disponível de threads de hardware.

Considere a seguinte função, download, que baixa o arquivo na URL fornecida. Este exemplo utiliza o método concurrency::Context::Oversubscribe para aumentar temporariamente o número de threads que estão 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, a sobressubscrição pode permitir que outras tarefas sejam executadas enquanto a tarefa atual aguarda por dados. Para obter a versão completa deste exemplo, consulte Como usar a sobrescrição para compensar a latência.

[Topo]

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

Use as funções de gerenciamento de memória, concurrency::Alloc e concurrency::Free, quando tiver tarefas refinadas que frequentemente alocam pequenos objetos com uma vida útil relativamente curta. O Concurrency Runtime contém um cache de memória separado para cada thread em execução. As Alloc funções 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 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.

[Topo]

Use o RAII para gerir o tempo de vida dos objetos de concorrência

O Concurrency Runtime usa o tratamento de exceções para implementar recursos como cancelamento. Portanto, escreva código seguro para exceções quando chamar o tempo de execução ou chamar outra biblioteca que chame o tempo de execução.

O padrão Resource Acquisition Is Initialization (RAII) é uma maneira de gerir com segurança o ciclo de vida de um objeto de concorrência num determinado âmbito. Sob o 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 de fechamento saia. Esse padrão é útil quando uma função contém várias return instruções. Esse padrão também ajuda você a escrever código seguro para exceções. Quando uma throw declaração faz com que a pilha se desempilhe, o destrutor do objeto RAII é chamado; portanto, o recurso é sempre corretamente excluído ou libertado.

O runtime define várias classes que usam o padrão RAII, por exemplo, concurrency::critical_section::scoped_lock e concurrency::reader_writer_lock::scoped_lock. Essas classes auxiliares são conhecidas como bloqueios delimitados. Essas classes fornecem vários benefícios quando trabalha com objetos de concorrência::critical_section ou concorrência::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. Como um bloqueio com escopo libera automaticamente o acesso ao seu objeto de exclusão mútua quando ele é destruído, você não desbloqueia manualmente o objeto subjacente.

Considere a seguinte classe, , que é definida por uma biblioteca externa e, portanto, accountnã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 account objeto em paralelo. O exemplo usa um critical_section objeto para sincronizar o account acesso ao objeto porque a account classe não é segura para 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 withdraw operação falha lançando 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 produz a seguinte saída:

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 gerir o ciclo de vida dos objetos de concorrência, consulte Passo a passo: Remover trabalho de um thread de User-Interface, Como usar a classe de contexto para implementar um semáforo cooperativo e Como usar a sobresubscrição para compensar a latência.

[Topo]

Não crie objetos de concorrência no escopo global

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

Por exemplo, quando você cria um objeto Concurrency Runtime, o tempo de execução cria um agendador padrão para você se um ainda não tiver sido criado. Um objeto de tempo de execução criado durante a construção do objeto global fará com que o tempo de execução crie este agendador padrão. No entanto, esse processo usa um bloqueio interno, que pode interferir na inicialização de outros objetos que suportam a infraestrutura Concurrency Runtime. Esse bloqueio interno pode ser exigido por outro objeto de infraestrutura que ainda não foi inicializado e, portanto, pode causar um impasse em seu aplicativo.

O exemplo a seguir demonstra a criação de um objeto concurrency::Scheduler global. Esse padrão se aplica não apenas à classe, Scheduler mas a todos os outros tipos fornecidos pelo Concurrency Runtime. 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.

[Topo]

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

O Concurrency Runtime não suporta o 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 tempo de execução em um estado inconsistente ou inválido.

[Topo]

Ver também

Práticas recomendadas de tempo de execução 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 a sobresubscrição para compensar a latência
Como: Usar a classe de contexto para implementar um semáforo cooperativo
Passo a passo: Removendo o trabalho de um thread User-Interface
Práticas recomendadas na biblioteca de padrões paralelos
Práticas recomendadas na biblioteca de agentes assíncronos