Compartilhar via


Práticas recomendadas gerais no Tempo de Execução de Simultaneidade

Este documento descreve as práticas recomendadas que se aplicam a várias áreas de tempo de execução de simultaneidade.

Seções

Este documento contém as seções a seguir:

  • Usar Constructos de Sincronização Cooperativos Quando Possível

  • Evitar Tarefas Longas que não Produzam

  • Usar Excesso de Assinatura para Deslocar Operações que Bloqueiam ou Têm Alta Latência

  • Usar Funções de Gerenciamento de Memória Simultâneas Quando Possível

  • Usar RAII para Gerenciar o Tempo de Vida de Objetos de Simultaneidade

  • Não Criar Objetos de Simultaneidade em Escopo Global

  • Não Usar Objetos de Simultaneidade em Segmentos de Dados Compartilhados

Usar Constructos de Sincronização Cooperativos Quando Possível

O tempo de execução de simultaneidade fornece muitos as construções simultaneidade- seguras que não exigem um objeto externo de sincronização. Por exemplo, a classe de concurrency::concurrent_vector fornece simultaneidade- seguro acrescenta e operações de acesso do elemento. No entanto, para casos onde você requer acesso exclusivo a um recurso, o tempo de execução fornece concurrency::critical_section, concurrency::reader_writer_lock, e classes de concurrency::event . Esses tipos se comportam cooperativa; em virtude disso, o agendador de tarefas pode realocar os recursos de processamento para outro contexto como a primeira tarefa espera dados. Quando possível, use esses tipos de sincronização em vez de outros mecanismos de sincronização, como a fornecida pela API do windows, que não se comportam 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 e Comparando estruturas de dados com a API do Windows.

[Superior]

Evitar Tarefas Longas que não Produzam

Como o agendador de tarefas se comporta cooperativa, o não fornece a equidade entre tarefas. Em virtude disso, uma tarefa pode impedir que outras tarefas iniciem. Embora isto seja aceitável em alguns casos, em outros casos isso pode causar um deadlock ou privação.

O exemplo a seguir executa tarefas mais do que o número de recursos processando atribuídos. A primeira tarefa não gerencie ao agendador de tarefas e como consequência a segunda tarefa não for iniciado 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();
}

Este exemplo gera a seguinte saída:

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. Um modo é produzir ocasionalmente ao agendador de tarefas em uma tarefa demorada. O exemplo a seguir altera a função de task para chamar o método de concurrency::Context::Yield para produzir a execução no agendador de tarefas de modo que outra tarefa 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();
}

Este exemplo gera a seguinte saída:

  

O método de Context::Yield gerencie somente outro thread ativo no agendador a que o thread atual, em uma tarefa de peso leve, ou em outro thread do sistema operacional. Esse método não gerencie que o trabalho foi agendado para ser executado em um objeto de concurrency::task_group ou de concurrency::structured_task_group mas ainda não foi iniciado.

Há outras maneiras de habilitar a cooperação entre tarefas demoradas. Você pode dividir uma grande tarefa em subtarefas menores. Você também pode habilitar a sobresubscrição durante uma tarefa demorada. A sobresubscrição permite criar mais threads que o número de threads de hardware disponível. A sobresubscrição é especialmente útil quando uma tarefa demorada contém uma quantidade de alta latência, por exemplo, ler dados de disco ou de uma conexão de rede. Para obter mais informações sobre as tarefas e de sobresubscrição superficial, consulte Agendador de tarefas (Tempo de Execução de Simultaneidade).

[Superior]

Usar Excesso de Assinatura para Deslocar Operações que Bloqueiam ou Têm Alta Latência

O tempo de execução de simultaneidade fornece as primitivas de sincronização, como concurrency::critical_section, que permitem tarefas bloqueio e gerar cooperativa entre si. Quando os blocos ou os vai de uma tarefa cooperativa, o agendador de tarefas possam realocar os recursos de processamento para outro contexto como a primeira tarefa espera dados.

Há casos nos quais você não pode usar o mecanismo cooperativo de bloqueio que é fornecido em tempo de execução de simultaneidade. Por exemplo, uma biblioteca externo que você usa pode usar um mecanismo diferente de sincronização. Outro exemplo é quando você executa uma operação que pode ter uma quantidade de alta 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 sobresubscrição pode habilitar outras tarefas executar enquanto outra tarefa está ocioso. A sobresubscrição permite criar mais threads que o número de threads de hardware disponível.

Considere a seguinte função, download, que carrega o arquivo na URL fornecido. Este exemplo usa o método de concurrency::Context::Oversubscribe temporariamente para aumentar 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 de GetHttpFile executa uma operação potencialmente latente, a sobresubscrição pode habilitar outras tarefas executar enquanto a tarefa espera dados atual. Para a versão completo deste exemplo, consulte Como usar excesso de assinatura para deslocar latência.

[Superior]

Usar Funções de Gerenciamento de Memória Simultâneas Quando Possível

Use as funções, concurrency::Alloc e concurrency::Freede gerenciamento de memória, quando você tem as tarefas refinados que atribuem que frequência os objetos pequenos que têm um tempo de vida relativamente curto. O tempo de execução de simultaneidade mantém um cachê de memória separado para cada thread em execução. As funções de Alloc e de Free atribuem e liberar a memória desses caches sem o uso de bloqueios ou de barreiras de memória.

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

[Superior]

Usar RAII para Gerenciar o Tempo de Vida de Objetos de Simultaneidade

O tempo de execução de simultaneidade usa a manipulação de exceção para implementar recursos como o cancelamento. Consequentemente, escreva o código exceções gerais seguro quando você chama em tempo de execução chama ou outra biblioteca que chama em tempo de execução.

O padrão Aquisição de recurso é inicialização (RAII) é uma maneira de gerenciar a segurança do tempo de vida de um objeto de simultaneidade em um escopo fornecido. No padrão de RAII, uma estrutura de dados é atribuída na pilha. Se a estrutura de dados inicializa ou adquire um recurso quando é criada e destrói o ou versões esse recurso quando a estrutura de dados é destruída. O padrão de RAII garante que o destruidor é chamado antes que o escopo inclusive encerrado. Esse padrão é útil quando uma função contém várias instruções de return . Ajuda desse padrão também você escreve o código seguro. exceções gerais Quando uma instrução de throw faz com que a pilha desenrole, o destruidor para o objeto de RAII é chamado; em virtude disso, o recurso sempre corretamente é excluído ou liberado.

O tempo de execução define várias classes que usam o padrão de RAII, por exemplo, concurrency::critical_section::scoped_lock e concurrency::reader_writer_lock::scoped_lock. Essas classes auxiliares são conhecidas como bloqueios o escopo. Essas classes fornecem vários benefícios quando você trabalha com concurrency::critical_section ou objetos de concurrency::reader_writer_lock . O construtor dessas classes adquire o acesso a critical_section ou ao objeto fornecido de reader_writer_lock ; o acesso das versões de destruidor a esse objeto. Como um acesso o escopo das versões de bloqueio ao objeto de exclusão mútua automaticamente quando é destruído, não desbloqueia manualmente o objeto subjacente.

Considere a seguinte classe, account, que é definida por uma biblioteca externo e não pode ser alterada em virtude disso.

// 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 de account em paralelo. O exemplo usa um objeto de critical_section para sincronizar o acesso ao objeto de account como a classe de account não é simultaneidade- segura. Cada operação paralela usa um objeto de critical_section::scoped_lock para garantir que o objeto de critical_section está desbloqueado quando a operação é bem-sucedida ou falha. Quando o balanço de contas for negativo, a operação de withdraw falha se lançar 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 gerencia a seguinte saída de exemplo:

  

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

[Superior]

Não Criar Objetos de Simultaneidade em Escopo Global

Quando você cria um objeto de simultaneidade no escopo global você pode fazer com que os problemas como violações de acesso de deadlock ou de memória lógica em seu aplicativo.

Por exemplo, quando você cria um objeto de tempo de execução de simultaneidade, o tempo de execução cria um agendador padrão para um se você ainda não foi criado. Um objeto de tempo de execução que é criado durante a compilação do objeto global adequadamente fará com que o tempo de execução criar este agendador padrão. No entanto, esse processo leva um bloqueio interno, que poderão interferir na inicialização de outros objetos que oferecem suporte à infraestrutura de tempo de execução de simultaneidade. Esse bloqueio interno pode ser necessário por outro objeto de infraestrutura que não foi inicializado, e isso pode fazer com que o deadlock ocorre em seu aplicativo.

O exemplo a seguir demonstra a criação de um objeto global de concurrency::Scheduler . Esse padrão é válido não só para a classe de Scheduler mas a todos os outros tipos que são fornecidos em tempo de execução de simultaneidade. É recomendável não segue esse padrão porque pode provocar 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 forma correta de criar objetos de Scheduler , consulte Agendador de tarefas (Tempo de Execução de Simultaneidade).

[Superior]

Não Usar Objetos de Simultaneidade em Segmentos de Dados Compartilhados

O tempo de execução de simultaneidade não oferece suporte ao uso de objetos de simultaneidade em uma seção compartilhada de dados, por exemplo, uma seção de dados que foi criada pela política de data_seg#pragma . Um objeto de simultaneidade que é compartilhado entre limites de processo pode colocar o tempo de execução em um estado inconsistente ou inválido.

[Superior]

Consulte também

Tarefas

Como usar Alloc e Free para melhorar o desempenho da memória

Como usar excesso de assinatura para deslocar 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

Conceitos

Biblioteca de padrões paralelos (PPL)

Biblioteca de Agentes Assíncronos

Agendador de tarefas (Tempo de Execução de Simultaneidade)

Estruturas de dados de sincronização

Comparando estruturas de dados com a API do Windows

Práticas recomendadas na Biblioteca de Padrões Paralelos

Práticas recomendadas na Biblioteca de Agentes Assíncrona

Outros recursos

Práticas recomendadas do Tempo de Execução de Simultaneidade