Práticas recomendadas de 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 seções a seguir:
Usar construções de sincronização cooperativo quando possível
Evitar tarefas demoradas que produz
Use o excesso de assinatura para operações que bloqueiam ou tem alta latência de deslocamento
Usar funções de gerenciamento de memória simultâneas quando possível
Use RAII para gerenciar o tempo de vida dos objetos de simultaneidade
Não crie objetos de simultaneidade no escopo Global
Não Use objetos de simultaneidade em segmentos de dados compartilhados
Usar construções de sincronização cooperativo quando possível
O Runtime de simultaneidade fornece muitas construções de prova de simultaneidade que não exigem um objeto de sincronização externa. Por exemplo, o Concurrency::concurrent_vector classe fornece prova de simultaneidade acrescentar e acessar as operações do elemento. No entanto, para casos em que exigem o acesso exclusivo a um recurso, o runtime fornece a Concurrency::critical_section, Concurrency::reader_writer_lock, e Concurrency::event classes. Esses tipos se comportam de forma cooperativa; Portanto, o Agendador de tarefas pode realocar os recursos de processamento para outro contexto, como a primeira tarefa aguarda a dados. Quando possível, use esses tipos de sincronização em vez de outros mecanismos de sincronização, tais como aqueles fornecidos pela API do Windows, que não se comportarem 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 as estruturas de dados de sincronização para a API do Windows..
go to top
Evitar tarefas demoradas que produz
Como o Agendador de tarefas se comporta de forma cooperativa, ele não fornece honestidade entre tarefas. Portanto, uma tarefa pode impedir que outras tarefas iniciando. Embora isso seja aceitável em alguns casos, em outros casos isso pode causar bloqueio ou starvation.
O exemplo a seguir executa mais tarefas que o número de recursos de processamento alocado. A primeira tarefa não produz o Agendador de tarefas e, portanto, a segunda tarefa não inicie até que a primeira tarefa é 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 produz 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. Uma maneira é ocasionalmente produzir para o Agendador de tarefas em uma tarefa de execução demorada. O exemplo seguinte modifica a task a função para chamar o Concurrency::Context::Yield método para gerar a execução para o Agendador de tarefas para que outra tarefa possa ser executado.
// 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 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 somente outro thread ativo no Agendador ao qual o thread atual pertence, uma tarefa simples, ou em outro segmento de sistema operacional. Este método não produz trabalhar que está programado para ser executado em um Concurrency::task_group ou Concurrency::structured_task_group de objeto, mas ainda não foi iniciado.
Existem outras maneiras de habilitar a cooperação entre as tarefas de execução demorada. Você pode interromper uma tarefa grande em subtarefas menores. Você também pode ativar o excesso de assinaturas durante uma tarefa demorada. Excesso de assinatura lhe permite criar mais threads que o número de segmentos de hardware disponíveis. Excesso de assinatura é especialmente útil quando uma tarefa demorada contém uma grande quantidade de latência, por exemplo, a leitura de dados de disco ou de uma conexão de rede. Para obter mais informações sobre tarefas leves e excesso de assinatura, consulte Agendador de tarefas (Runtime de simultaneidade).
go to top
Use o excesso de assinatura para operações que bloqueiam ou tem alta latência de deslocamento
O Runtime de simultaneidade oferece primitivos de sincronização, como Concurrency::critical_section, que permitem que as tarefas para bloquear cooperativamente e produzir uns aos outros. Quando uma tarefa de forma cooperativa bloqueia ou produz, o Agendador de tarefas pode realocar os recursos de processamento para outro contexto, como a primeira tarefa aguarda a dados.
Há casos em que você não pode usar o mecanismo de bloqueio cooperativo que é fornecido pelo Runtime de simultaneidade. Por exemplo, a biblioteca externa que você use pode usar um mecanismo de sincronização diferente. Outro exemplo é quando você executar uma operação que poderia ter uma grande quantidade de latência, por exemplo, quando você usa a API do Windows ReadFile função para ler os dados de uma conexão de rede. Nesses casos, o excesso de assinaturas pode habilitar outras tarefas a executar quando outra tarefa estiver ociosa. Excesso de assinatura lhe permite criar mais threads que o número de segmentos de hardware disponíveis.
Considere a seguinte função, download, que baixa o arquivo em um determinado URL. Este exemplo usa a Concurrency::Context::Oversubscribe método para aumentar temporariamente o número de segmentos 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;
}
Porque o GetHttpFile função realiza uma operação potencialmente latente, excesso de assinaturas pode ativar a outras tarefas para serem executados como o atual tarefa aguarda dados. Para obter a versão completa deste exemplo, consulte Como: Use o excesso de assinatura para deslocamento latência.
go to top
Usar 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 você tem tarefas refinadas que freqüentemente alocam pequenos objetos que têm uma vida útil relativamente curta. O Runtime de simultaneidade mantém um cache de memória separado para cada segmento em execução. O Alloc e Free funções alocar e liberar a 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 (Runtime de simultaneidade). Para obter um exemplo que usa essas funções, consulte Como: Use Alloc e gratuita de melhorar o desempenho de memória.
go to top
Use RAII para gerenciar o tempo de vida dos objetos de simultaneidade
O Runtime de simultaneidade usa para implementar recursos como, por exemplo, cancelamento de manipulação de exceção. Portanto, escreva um código de exceção segura quando você chama o tempo de execução ou chama outra biblioteca que chama o tempo de execução.
O É inicialização de aquisição de recursos padrão de (RAII) é uma maneira de gerenciar com segurança o tempo de vida de um objeto de simultaneidade em um determinado escopo. Em padrão de 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 é chamado antes que sai do escopo de fechamento. 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 de exceção. Quando um throw instrução faz com que a pilha de desenrolamento, o destruidor do objeto RAII é chamado; Portanto, o recurso é excluído ou liberado sempre corretamente.
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 conhecidos como escopo bloqueios. Essas classes fornecem vários benefícios quando você trabalha com Concurrency::critical_section ou Concurrency::reader_writer_lock objetos. O construtor dessas classes adquire o acesso a fornecida critical_section ou reader_writer_lock objeto; o destruidor libera o acesso ao objeto. Porque um bloqueio de escopo libera automaticamente o acesso ao seu objeto de exclusão mútua quando destruí-la, você não desbloquear manualmente o objeto subjacente.
Considere a seguinte classe, account, que é definido pela biblioteca externa e, portanto, não pode ser modificado.
// 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 o objeto em paralelo. O exemplo usa um critical_section objeto para sincronizar o acesso para o account porque a account classe não é a simultaneidade-safe. Cada operação paralela usa um critical_section::scoped_lock o objeto para garantir que o critical_section objeto está desbloqueado quando a operação for bem-sucedida ou ou falhar. Quando o saldo da conta é negativo, o withdraw operação falhar, 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 saída de exemplo a seguir:
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 dos objetos de simultaneidade, consulte Demonstra Passo a passo: Removendo o trabalho de um segmento de Interface do usuário, Como: Use a classe de contexto para implementar um semáforo cooperativo, e Como: Use o excesso de assinatura para deslocamento latência.
go to top
Não crie objetos de simultaneidade no escopo Global
Se você criar um objeto de simultaneidade no escopo global, o deadlock pode ocorrer em seu aplicativo.
Quando você cria um objeto de Runtime de simultaneidade, o runtime cria um agendador padrão, se um ainda não foi criado. Isso também ocorre para um objeto de tempo de execução que é criado durante a construção do objeto global. No entanto, esse processo leva um bloqueio interno, que pode interferir com a inicialização de outros objetos que dêem suporte à infra-estrutura do Runtime de simultaneidade. Porque esse bloqueio interno pode ser exigido por outro objeto de infra-estrutura que ainda não foi inicializado, o deadlock pode ocorrer em seu aplicativo.
O exemplo a seguir demonstra a criação de um global Concurrency::Scheduler objeto. Esse padrão não só é aplicável a Scheduler classe, mas também para todos os outros tipos que são fornecidos pelo Runtime de simultaneidade. Recomendamos não seguir esse padrão, porque pode fazer com que seu aplicativo para o deadlock.
// 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 de maneira correta de criar Scheduler objetos, consulte Agendador de tarefas (Runtime de simultaneidade).
go to top
Não Use objetos de simultaneidade em segmentos de dados compartilhados
O Runtime de simultaneidade não suporta o uso de objetos de simultaneidade em uma seção de dados compartilhados, por exemplo, uma seção de dados é criada pelo data_seg #pragma diretiva. Um objeto de simultaneidade é compartilhado entre limites de processo poderia colocar o tempo de execução em um estado inconsistente ou inválido.
go to top
Consulte também
Tarefas
Como: Use Alloc e gratuita de melhorar o desempenho de memória
Como: Use o excesso de assinatura para deslocamento latência
Demonstra Passo a passo: Removendo o trabalho de um segmento de Interface do usuário
Conceitos
As práticas recomendadas de Runtime de simultaneidade
Biblioteca paralela de padrões (PPL)
Biblioteca de agentes assíncronos
Agendador de tarefas (Runtime de simultaneidade)
Estruturas de dados de sincronização
Comparando as estruturas de dados de sincronização para a API do Windows.
Outros recursos
Como: Use a classe de contexto para implementar um semáforo cooperativo
Práticas recomendadas para a biblioteca de padrões paralelos
Práticas recomendadas para a biblioteca de agentes assíncronos
Histórico de alterações
Date |
History |
Motivo |
---|---|---|
Março de 2011 |
Adicionadas informações sobre o potencial de deadlock no escopo global. |
Comentários do cliente. |