Este artigo foi traduzido por máquina.

Agentes assíncronos

Programação baseada em atores com a Biblioteca de Agentes Assíncronos

Michael Chu

Com processadores de vários núcleos agora comuns no mercado, desde servidores e desktops, laptops, a paralelização de código nunca foi tão importante. Para atender a essa área vital, o Visual Studio 2010 introduz várias novas maneiras para ajudá-lo opers do C++ devel ­ tirar proveito desses recursos com um novo tempo de execução paralelo e paralelo novo modelos de programação. No entanto, um obstáculo principal à esquerda para desenvolvedores é decidir qual modelo de programação é correto para seus aplicativos. O modelo correto significativamente pode explorar o paralelismo subjacente, mas também pode requerer um rethink de como seu programa está estruturado e, na verdade, é executado.

Os modelos de programação paralelos mais comuns atualmente envolvem contêineres de finalidade gerais, reconhece a simultaneidade e algoritmos como a paralelização de iterações de loop. Embora essas técnicas tradicionais podem ser um método eficaz para aplicativos de dimensionamento para tirar proveito de uma máquina com vários núcleos, elas não solucionam um dos outros fatores importantes que afetam o desempenho paralelo: o impacto de cada vez maior de latência. Como as técnicas de paralelização aceleram os cálculos e se espalhar para fora em vários núcleos, lei de Amdahl (wikipedia.org/wiki/Amdahl's_law ) mostra nos que a melhoria de desempenho é limitada por parte de execução mais lenta. Em muitos casos, há uma porcentagem de cada vez maior de tempo gasto aguardando dados de e/S, como disquetes ou redes.

Modelos de programação baseado em ator lidam bem com problemas, como a latência e foram introduzidos pela primeira vez na década de 70 antecipado para explorar os recursos dos computadores altamente paralelos com centenas ou milhares de processadores independentes. O conceito fundamental por trás de um modelo de ator é tratar os componentes de um aplicativo como atores individuais que podem interagir com o mundo, enviar, receber e processar mensagens.

Mais recentemente, com a abundância de processadores de vários núcleos, o modelo de ator possui resurfaced como um método eficaz para ocultar as latências de execução em paralelo eficiente. O Visual Studio 2010 introduz o Asynchronous agentes Library (AAL), um interessante ator com modelo de novo com interfaces de transmissão de mensagens em que os agentes são os atores. AAL permite aos desenvolvedores projetar os aplicativos de forma mais centrada no fluxo de dados. Como um projeto normalmente faz para uso produtivo de latência ao mesmo tempo esperando pelos dados.

Neste artigo, nós irá fornecer uma visão geral sobre o AAL e demonstram como você pode tirar proveito em seus aplicativos.

O Runtime de simultaneidade

A base para o suporte à simultaneidade no Visual Studio 2010 e AAL é o tempo de execução de simultaneidade novos, que é fornecido como parte do tempo de execução do CRT (C) no Visual Studio 2010. O Runtime de simultaneidade oferece um Agendador de tarefas cooperativo e um Gerenciador de recursos que tenha uma compreensão profunda dos recursos subjacentes da máquina. Isso permite que o tempo de execução executar tarefas de uma maneira de balanceamento de carga em uma máquina com vários núcleos.

A Figura 1 mostra uma estrutura de tópicos do suporte ao Visual Studio 2010 de concorrência no código nativo. O Agendador é o principal componente que determina quando e onde executar as tarefas. Ele utiliza as informações coletadas pelo Gerenciador de recursos para melhor utilizar os recursos de execução. Aplicativos e bibliotecas propriamente ditas principalmente interagem com o tempo de execução simultâneos por meio de dois modelos de programação situadas na parte superior do Agendador de AAL e os padrões PPL (biblioteca paralelo), embora também diretamente, eles podem interagir com o tempo de execução.

image: The Concurrency Runtime

Figura 1 do tempo de execução o concorrência

A PPL oferece técnicas de paralelização mais tradicionais, como, por exemplo, construções parallel_for e parallel_for_each, bloqueios sensíveis ao tempo de execução e estruturas de dados simultâneos como, por exemplo, filas e vetores. Ao mesmo tempo não é o foco deste artigo, a PPL é uma ferramenta poderosa para desenvolvedores que podem ser usados em conjunto com todos os novos métodos apresentados de AAL. Para obter mais informações sobre a PPL, consulte a edição de fevereiro de 2009 da coluna Windows com C++ (msdn.microsoft.com/magazine/dd434652 ).

Por outro lado, o AAL fornece a capacidade de paralelizar aplicativos em um nível superior e de uma perspectiva diferente que técnicas tradicionais. Os desenvolvedores precisam pensar em aplicativos do ponto de vista dos dados a serem processados e considerar como o processamento de dados pode ser separado em componentes ou estágios que podem ser executados em paralelo.

O tipo de AAL fornece dois componentes principais: uma estrutura de transmissão de mensagens e os agentes assíncronos.

A estrutura de transmissão de mensagens inclui um conjunto de blocos de mensagens, que pode receber, processo e propagar as mensagens. Por encadeamento juntas blocos de mensagens, as tubulações do trabalho podem ser criadas que podem ser executados simultaneamente.

Os agentes assíncronos são os atores interagem com o mundo pelo recebimento de mensagens, a execução local de trabalho no seu próprio estado mantido e enviar mensagens.

Juntos, esses dois componentes permitem aos desenvolvedores explorar o paralelismo em termos de fluxo de dados em vez do fluxo de controle e melhor tolerar latências utilizando os recursos paralelos com mais eficiência.

Framework de passagem de mensagens

O primeiro componente importante de AAL é a estrutura de transmissão de mensagens, um conjunto de construções para ajudar a desenvolver redes de fluxo de dados para o trabalho de pipeline. Processamento de trabalho é uma parte fundamental do modelo de fluxo de dados, pois ele permite a transmissão de dados para serem processados em paralelo, sempre que os dados estiverem prontos, dividindo o trabalho em vários estágios independentes. Quando termina de processamento de dados em um estágio, estágio pode passar os dados de para o próximo estágio enquanto o primeiro procura por novos dados na qual deseja trabalhar.

Por exemplo, considere um aplicativo de email que formata as mensagens de saída e censors-los para conteúdos inadequados. O código para esse tipo de operação é mostrado aqui:

std::foreach(reader.begin(); reader.end(); 
  [](const string& word) { 
    auto w1 = censor(word); 
    auto w2 = format(w1); 
    writer.write_word(w2);
  });

Para cada palavra no email, o aplicativo precisa verificar se ele existe em um dicionário de palavras censored, substituindo-o se ele faz. O código, em seguida, formata cada palavra de acordo com a um conjunto de diretrizes.

Não há quantidade asignificant de paralelismo inerente em uma situação como essa. No entanto, técnicas tradicionais de paralelismo cair curtas. Por exemplo, uma abordagem simples seria possível usar um algoritmo de parallel_for_each em cadeias de caracteres no texto para censor e formatá-los.

O primeiro empecilho principal para essa solução é que ele deve ler todo o arquivo para que um iterador corretamente pode dividir o trabalho.
Forçar o arquivo inteiro seja lido torna o processo/S ligadas e pode diminuir a paralelização ganhos. É claro, você poderia usar um iterador inteligente para sobrepor o processamento de palavras com a entrada de leitura.

A segunda questão mais importante com uma abordagem tradicional de paralelização é pedido. É claro que, no caso de uma mensagem de email, processamento paralelo do texto deve manter a ordem do texto ou o significado da mensagem é totalmente perdido. Para manter a ordenação do texto, uma técnica de parallel_for_each poderia incorrer em sobrecarga significativa em termos de sincronização e o armazenamento em buffer, que é tratada automaticamente pelo AAL.

Ao processar a mensagem em um pipeline, você pode evitar esses dois problemas e ainda aproveitar paralelização. Considere a possibilidade de 2 Figura , onde um pipeline simples foi criado. Neste exemplo, as principais tarefas do aplicativo — censoring e a formatação — estão divididos em dois estágios. O primeiro estágio leva uma seqüência de caracteres e pesquise em um dicionário de palavras censored. Se uma correspondência for encontrada, o bloco de censor substitui a cadeia de caracteres com uma palavra diferente do dicionário. Caso contrário, ela produz a mesma mensagem que foi inputted. Da mesma forma, a segunda fase, o formato de bloco leva cada palavra e corretamente a formatação para um determinado estilo.

image: E-mail Processing Pipeline

De pipeline de processamento de email, a Figura 2

Este exemplo pode aproveitar a abordagem de fluxo de dados de diversas maneiras. Em primeiro lugar, pois elimina a necessidade de ler a mensagem inteira antes do processamento, as cadeias de caracteres da mensagem podem iniciar imediatamente streaming por meio do censoring e estágios de formatação. Em segundo lugar, o processamento do pipeline permite uma seqüência de caracteres a ser processada pelo formato de bloco durante a próxima seqüência de caracteres que está sendo processada pelo bloco censor. Por fim, como seqüências de caracteres são processadas na ordem em que aparecem no texto original, nenhuma sincronização adicional precisa ser feito.

Blocos de mensagem

Os blocos de mensagens recebem, processo, armazenam e propagam as mensagens. Blocos de mensagens são fornecidos em uma das três formas: origens, destinos e propagators. Fontes só têm a capacidade para propagar as mensagens, enquanto os destinos podem receber, armazenar e processá-los. A maioria dos blocos é propagators, que são as origens e destinos. Em outras palavras, eles têm a capacidade de receber, armazenar e processar mensagens, bem como para girar e enviam essas mensagens.

O tipo de AAL contém um conjunto de primitivos de bloco de mensagem que abrangem a maioria dos casos de uso para os desenvolvedores. A Figura 3 mostra uma visão geral de todos os blocos de mensagem incluída de AAL. No entanto, o permanece modelo aberto, portanto, se seu aplicativo requer um bloco de mensagens com um comportamento específico, você pode escrever um personalizado bloquear você mesmo pode interagir com todos os blocos predefinidos. Cada bloco tem suas próprias características exclusivas para processamento, armazenar e propagação de mensagens.

A Figura 3 do AAL blocos de mensagens

Bloco de mensagem Finalidade
unbounded_buffer <Type> Armazena o número de mensagens não acoplado e propaga para seus destinos.
overwrite_buffer <Type> Armazena uma única mensagem será sobrescrita sempre que uma nova mensagem é propagada para ele, e transmite-o para seus destinos.
single_assignment <Type> Armazena uma única mensagem é de gravação - uma vez e transmite-o para seus destinos.
Transformador de < entrada, saída > Obtém uma mensagem do tipo de entrada e executa uma função fornecido pelo usuário para transformá-lo a uma mensagem do tipo de saída. Esta mensagem transformada é propagada para seus destinos.
chamada <Type> Obtém uma mensagem e executa uma função fornecido pelo usuário com a carga da mensagem como um argumento. Isso é essencialmente um destino da mensagem.
Timer <Type> Propaga a uma mensagem para seu destino após um período definido pelo usuário de tempo. Isso pode ser de repetição ou não-repetição. Este bloco é meramente uma fonte de mensagem.
Escolha < Type 1, Tipo2... > Aceita mensagens de várias origens de vários tipos e só aceitará a mensagem a partir do primeiro bloco propagadas para a escolha.
ingressar <Type> Obtém mensagens de várias origens e as combina em conjunto para dar saída a uma única mensagem. Aguarda assincronamente as mensagens para preparar-se de que cada entrada de origem.
multitype_join < Type 1, Tipo2... > Obtém mensagens de várias origens de vários tipos e as combina em conjunto. Aguarda assincronamente as mensagens para preparar-se de que cada entrada de origem.

Um dos principais benefícios dos primitivos de bloco de mensagem fornecidos pelo AAL é que eles são compostos. Portanto, você pode combiná-las, com base em que o comportamento desejado. Por exemplo, você pode criar facilmente um bloco que adiciona várias entradas em conjunto, anexando um bloco de transformador até o final do bloco de junção. Quando o bloco de junção tiver êxito na recuperação de mensagens de cada uma das suas origens, ele pode passá-las para o transformador, que soma as cargas de mensagem.

Você também pode conectar um bloco de timer de repetição como uma origem de um bloco de junção. Isso resultaria em um bloco que regula mensagens, apenas, permitindo que eles por meio de sempre que o bloco de timer é acionado sua mensagem. Esses dois blocos compostos são ilustrados no do Figura 4.

De compondo adicionador e otimização de blocos de primitivos de mensagem, a Figura 4

Criando um pipeline de passagem de mensagens

Agora let’s dar uma olhada no código para criar o pipeline de bloco de mensagem mostrada anteriormente. Nós pode substituir o pipeline com dois blocos de mensagem do transformador, conforme mostrado no do Figura 5. A finalidade de um bloco de transformador é para levar a uma mensagem de um determinado tipo e a execução de um usuário-­ definido função essa mensagem, que pode modificar a carga da mensagem ou até mesmo completamente alterar o tipo da mensagem. Por exemplo, o bloco de censor aceita como uma mensagem que contém uma seqüência de caracteres de entrada e precisa para processá-lo.

image: A Message Block Pipeline

Do Pipeline de bloqueio de uma mensagem, a Figura 5

O código para criar e conectar os blocos de mensagem é mostrado no do Figura 6. Esse código começa com a instanciação de blocos de mensagem transformador de dois. O C + + 0x parâmetro lambda no construtor de bloco censor define a função de transformação, o que procura seqüência de entrada armazenados da mensagem de em um dicionário para ver se ele deve ser alterado para uma seqüência de caracteres diferente. A seqüência de caracteres resultante é retornada e dentro do bloco censor tem, em seguida, encapsulado em uma mensagem e propagadas para fora do bloco. Um caminho semelhante é feito para o bloco de transformador de formato, exceto sua saída é uma seqüência de caracteres foi alterada por uma função de formato.

Figura 6 do pipeline de mensagem simples

dictionary dict;

transformer<string, string> 
  censor([&dict](const string& s) -> string {

  string result = s;
  auto iter = dict.find(s);

  if (iter != dict.end()) {
    result =  iter->second;
  }

  return result;
});

transformer<string, string> 
  format([](const string& s) -> string {

  string result = s;
  for (string::size_type i = 0; i < s.size(); i++) {
    result[i] = (char)Format(s[i]);
  }

  return result;
});

censor.link_target(&format);

asend(&censor, "foo");
string newStr = receive(format);
printf("%s\n", newStr);

Seguindo a instanciação de dois blocos, a próxima linha vincula os dois blocos, chamando o método de link_target no bloco censor. Cada bloco de origem e de propagador tem um método de link_target é usado para determinar para quais blocos de mensagens a origem deve propagar suas mensagens.

Depois que o censor e o formato de blocos foram vinculados juntos, qualquer mensagem propagada para o bloco de censor passará por meio de sua função de transformação e a mensagem resultante será implicitamente ser passada para o bloco de formato para processamento. Se um bloco de mensagens é uma fonte ou propagador ainda não possui nenhum destino conectado, o bloco de mensagens pode armazenar a mensagem de uma forma de bloco específico até um destino vinculado, ou a mensagem é recuperada.

As três últimas linhas de código de exemplo mostram o processo de iniciar as mensagens em um bloco e recuperar uma mensagem de um bloco. Existem APIs em de AAL iniciação de duas mensagens: enviar e asend. Esses uma mensagem de entrada em um bloco de forma síncrona e assíncrona, respectivamente.

A principal diferença é que quando uma chamada de envio retorna, ele tem a garantia de já ter transferido a mensagem de dentro e através de bloco ao qual a mensagem é enviada. A chamada asend pode retornar imediatamente e permitirá que o tempo de execução de simultaneidade agendar sua propagação. Da mesma forma, existem duas APIs em de AAL a recuperação de mensagens: receber e try_receive. O método de recebimento será bloqueado até que uma mensagem chega, enquanto o try_receive retornará imediatamente se não consegue recuperar uma mensagem.

Do Figura 6, a seqüência de caracteres “ foo ” é enviada em assincronamente ao bloco censor. O bloco de censor irá levar a mensagem, verifique se sua seqüência está no dicionário de palavras censored e, em seguida, propaga a seqüência de caracteres resultante em uma mensagem. Isso será passado para o bloco de formato, o que levar a seqüência de caracteres, capitalizar cada letra e porque ela tem nenhum destino, manter a mensagem. Quando receber é chamado, ele irá pegar a mensagem a partir do bloco de formato. Assim, presumindo-se de que não era “ foo ” no dicionário, a saída desse exemplo seria “ FOO ”. Embora este exemplo envia apenas uma única cadeia de caracteres através da rede, você pode ver como um fluxo de cadeias de caracteres de entrada forma um pipeline de execução.

Olhando para esse exemplo de mensagens, observe a ausência distinta de referências de mensagens em si. Uma mensagem é simplesmente um envelope que encapsula os dados que você deseja passar em torno de sua rede de fluxo de dados. A mensagem passar propriamente dito é tratada por meio de um processo de oferta e aceitar. Quando um bloco de mensagens recebe uma mensagem, ela tem a capacidade de armazenar a mensagem de nenhuma maneira que quiser. Se posteriormente desejar enviar uma mensagem para fora, ele oferece a mensagem para cada um de seus destinos conectados. Para realmente se a mensagem imediatamente, o receptor deve aceitar a mensagem oferecida para concluir a transação. Todo o processo de transmissão entre os blocos de mensagens está agendado e manipulado por tarefas agendadas e executadas pelo tempo de execução de simultaneidade.

Propagação do bloco de mensagem

Agora que você já viu como mensagem de blocos são criados e ligados juntos e como as mensagens podem ser iniciadas e let’s recuperados de cada um deles, faça uma breve examinar como as mensagens são passadas entre blocos e como o Runtime de simultaneidade se adapta a essência de AAL.

Essas informações não não necessárias para o uso de blocos de mensagens ou de AAL, mas podem ajudá-lo a dar um entendimento mais profundo de como os protocolos de transmissão de mensagens funcionam e como você pode tirar proveito delas. No restante desta seção, abordarei propagador bloqueia, porque elas são as origens e destinos. Obviamente, uma origem pura ou um bloco puro destino seria apenas um subconjunto da implementação do bloco de propagador.

Internamente, cada bloco de propagador tem uma fila para mensagens de entrada e o outro recipiente de armazenamento específicas do bloco de mensagens. Outros blocos que estão vinculados a este bloco de propagador enviam mensagens que são armazenadas na fila de entrada.

Por exemplo, do Figura 7, o bloco de transformador censor tem uma fila de entrada está armazenando no momento uma mensagem com uma seqüência de caracteres de str6 nele. O transformador real próprio contém duas mensagens: str4 e str5. Como esse é um transformador, seu armazenamento de bloco específico é outra fila. Tipos de bloco diferentes podem ter recipientes de armazenamento diferentes. Por exemplo, o bloco overwrite_buffer armazena somente uma única mensagem sempre deverá obter sobrescrita.

image: Message-Passing Protocol

A Figura 7 do protocolo de passagem de mensagens

Quando uma mensagem é apresentada a um bloco de uma das suas origens de links (ou a APIs de envio/asend), o bloco de primeiro verifica uma função de filtro para determinar se deve ou não aceitar a mensagem. Se ele decidir aceitar a mensagem, a mensagem é colocada na fila de entrada. Um filtro é uma função opcional que pode ser passada para o construtor de cada destino ou o bloco de propagador que retorna um Boolean que determina se uma mensagem é oferecido de uma origem deve ser aceitas. Se a mensagem foi recusada, a fonte continuará para seu próximo de destino para oferecer a mensagem.

Depois que uma mensagem é colocada na fila de entrada, o bloco de origem que foi emitido por não mantém em à mensagem. No entanto, o bloco aceitando ainda não tem a mensagem de pronta para a propagação. Dessa forma, as mensagens podem buffer até na fila de entrada de enquanto aguardam o processamento.

Quando uma mensagem chega na fila de entrada em um bloco de mensagens, uma leve (LWT) será agendada no Agendador de simultaneidade em tempo de execução. O objetivo deste LWT é dupla. Em primeiro lugar, ele deve mover as mensagens da fila de entrada para o armazenamento interno do bloco (o que nos referimos como processamento de mensagens). Em segundo lugar, também deve tentar propagar as mensagens de qualquer alvo (o qual nos referimos como propagação da mensagem).

Por exemplo, do Figura 7, havia mensagens na fila de entrada solicitado LWT ser agendado. O LWT processado a mensagem, em seguida, executando primeira função do transformador de fornecido pelo usuário na mensagem, a verificação no dicionário de cadeia de caracteres censored e depois mover a mensagem para o buffer de armazenamento para o bloco.

Depois de transferi-lo em um buffer de armazenamento, o LWT começa a etapa de propagação de onde as mensagens são enviadas para o bloco de formato de destino. Nesse caso, como mensagem str4 estava no topo do transformador, é propagada para o formato de bloco primeiro e, em seguida, a próxima mensagem str5, é propagada. Todo o mesmo processo ocorre no formato de bloco.

Processamento de mensagens pode ser diferente, dependendo do tipo de bloco de mensagens. Por exemplo, um unbounded_buffer tinha uma etapa do processamento simples de mover uma mensagem para o buffer de armazenamento. O transformador processa mensagens chamando a função definida pelo usuário na mensagem antes de movê-lo para um buffer de armazenamento. Outros blocos podem se tornar ainda mais complexos, como, por exemplo, a associação, que deve combinar várias mensagens de diferentes origens e armazená-los para um buffer de preparação para a propagação.

Para a eficiência do desempenho, o AAL é inteligente na criação de LWTs, de modo que somente uma é agendada de uma vez para cada bloco de mensagens. Se ainda mais as mensagens chegam em uma fila de entrada enquanto o processamento LWT estiver ativo, ele continuará a pegar e processar essas mensagens. Dessa forma, na Figura 7, se LWT do transformador ainda está processando quando a mensagem str7 entra a fila de entrada, ele irá pegar e processar esta mensagem em vez de iniciar uma nova tarefa de processamento e a propagação.

O fato de que cada bloco de mensagens tem seu próprio LWT que manipula o processamento e propagação é fundamental para o design, que permite que a estrutura de transmissão de mensagens para o trabalho de uma maneira de fluxo de dados do pipeline. Como cada bloco de mensagens não seu processamento e a propagação de suas mensagens em sua própria LWT, o AAL é capaz de desacoplar os blocos de um do outro e permitir paralelo de trabalho a ser executado em vários blocos. Cada LWT simplesmente deve propagar suas mensagens em filas de entrada ’ seus blocos de destino, e cada destino simplesmente agendará um LWT para lidar com suas próprias entradas. Usar um único LWT para processar e propagar garante que a ordem de mensagem é mantida para os blocos de mensagem.

Agentes assíncronos

O segundo componente principal de AAL é o agente assíncrono. Agentes assíncronos são componentes de blocos de aplicativos que são destinados a assincronamente lidam com tarefas de computação com maiores e e/S. Os agentes devem se comunicar com outros agentes e iniciar o paralelismo de nível inferior. Eles são isolados porque sua visão de mundo está inteiramente contido dentro de sua classe, e eles podem se comunicar com outros componentes do aplicativo por meio de transmissão de mensagens. Os agentes propriamente ditas são agendados como as tarefas dentro do Runtime de simultaneidade. Isso permite que eles bloquear e gerar de forma cooperativa com os outros trabalho em execução ao mesmo tempo.

Um agente assíncrono tem um conjunto de ciclo de vida, conforme mostrado na do Figura 8. O ciclo de vida pode ser monitorado e aguardado. Estados em verde significam estados de execução, enquanto os estados em vermelho são os estados de terminal. Os desenvolvedores podem criar seus próprios agentes, derivando da classe base do agente.

image: The Asynchronous Agent Lifecycle

Do ciclo de vida do agente assíncrono, a Figura 8

Três funções de classe de base, iniciar, Cancelar e feito — o agente entre os diferentes estados de transição. Depois de criada, os agentes estão no estado criado. Iniciar um agente é semelhante a um thread de inicialização. Eles não executará qualquer coisa até que o método start denomina-se neles. Nesse momento, o agente está agendado para execução e o agente é movido para o estado de execução.

Quando o tempo de execução de simultaneidade pega o agente, ele é movido para o estado iniciado e continuará a ser executado até que o usuário chama o método done, indicando que o trabalho foi concluída. Qualquer momento depois do agente foi programado, mas ainda não iniciado, uma chamada para cancelar o farão a transição do agente para um estado cancelado e nunca executará.

Let’s olha para trás no exemplo de filtragem a email, onde os blocos de mensagem pipeline introduziu o fluxo de dados do aplicativo e melhorou a capacidade de palavras de processo paralelo. No entanto, o exemplo não mostrou como tratar a e/S de lidar com emails em si e dividindo-os em fluxos de cadeias de caracteres para o pipeline de processamento. Além disso, uma vez que as seqüências foram passadas pelo pipeline, as cadeias de caracteres devem ser reunidas para que o texto pode ser reescrito em seu estado de recém-censored e formatado. Isso é onde os agentes podem entram em cena para ajudar a suportar as diferenças de latências de e/S.

Por exemplo, considere a possibilidade de final de nosso canal de email. Neste ponto, seqüências de caracteres estão sendo saídas no formato e precisam ser gravados em arquivos em uma caixa de correio. A Figura 9 mostra como um agente de saída pode capturar as seqüências de caracteres e criar mensagens de email de saída. A função de execução do WriterAgent recebe mensagens do bloco de formato em um loop.

image: An Agent Capturing the Output of the Format Block

A Figura 9 do agente de uma captura da saída do Block Format

Embora a maioria do processamento feito neste aplicativo está usando o fluxo de dados, o WriterAgent mostra como alguns fluxo de controle pode ser introduzido no programa. Por exemplo, quando chega uma mensagem de final de arquivo, o WriterAgent deve ter um comportamento diferente dependendo da seqüência de caracteres de entrada que está sendo recebidoEle precisa saber para encerrar a operação. O código para o WriterAgent é do Figura 10.

A Figura 10 do WriterAgent

class WriterAgent : public agent {
public:
  WriterAgent(ISource<string> * src) : m_source(src) {
  }

  ~WriterAgent() {
    agent::wait(this);
  }

  virtual void run() {
    FILE *stream;
    fopen_s( &stream, ...
);

    string s;
    string eof("EOF");

    while (!feof(stream) && ((s=receive(m_source)) != eof)) {
      write_string(stream, s);
    }

    fclose(stream);
    done();
  }

private:

  ISource<string> * m_source;
};

Existem algumas partes deste código de observar o interessante. Em primeiro lugar, dentro do destruidor é feita uma chamada para uma função estática de agent::wait. Esta função pode ser chamada com um ponteiro para qualquer agente e será bloqueado até que o agente entra em um dos Estados de terminal: concluído ou cancelado. Durante a espera de chamada no destruidor não é necessária para todos os agentes, na maioria dos casos ele deve ser feito, pois garante que o agente não está executando qualquer código quando destructing.

Em segundo lugar, um aspecto interessante desse código é o próprio método de execução. Este método define a execução principal do agente. Nesse código, o agente está lidando com a gravação de seqüências de caracteres que ele lê a partir de sua origem (no nosso exemplo, o formato de bloco).

Finalmente, observe a última linha do método de execução, que é uma chamada para a função do agente feita. A chamada ao método done move o agente do estado de execução para o estado concluído. Na maioria dos casos, isso precisa ser chamado no final do método de execução. No entanto, em algumas circunstâncias, aplicativos talvez queira usar os agentes para configurar o estado, como em uma rede de fluxo de dados, deverá permanecer ativa após o tempo de vida do método execute.

Ligar tudo junto

Agora que criamos um pipeline de mensagens para o filtro e seqüências de caracteres de formato e um agente de saída para processá-los, podemos adicionar um agente de entrada que tem comportamento semelhante ao agente de saída. A Figura 11 mostra um exemplo de como esse aplicativo se encaixa em conjunto.

image: Agents Used to Process E-mail Messages

A Figura 11 de agentes usado para processar mensagens de correio electrónico

Um dos benefícios do agente de processamento é a capacidade de usar os atores assíncronos no aplicativo. Dessa forma, chegada de dados de processamento, o agente de entrada será assíncrona começar a enviar que as seqüências de caracteres por meio do pipeline e o agente de saída poderá da mesma maneira os arquivos de leitura e de saída. Esses atores podem iniciar e parar o processamento de forma totalmente independente e totalmente orientado por dados. Esse comportamento funciona perfeitamente em muitas situações, especialmente orientadas a latência e e/S assíncrona, como no exemplo de processamento de email.

Neste exemplo, adicionei um agente de segundo, um ReaderAgent, que funciona da mesma forma para WriterAgent, exceto que ele manipula a e/S para lidar com a leitura de emails e enviar seqüências para a rede. O código para o ReaderAgent é do Figura 12.

A Figura 12 do ReaderAgent

class ReaderAgent : public agent {
public:
  ReaderAgent(ITarget<string> * target) : m_target(target) {
  }

  ~ReaderAgent() {
    agent::wait(this);
  }

  virtual void run() {
    FILE *stream;       
    fopen_s( &stream, ...);

    while (!feof(stream)) {
      asend(m_target, read_word(stream));
    }

    fclose( stream );

    asend(m_target, string("eof"));
    done();
  }

private:

  ITarget<string> * m_target;
};

Agora que temos um ReaderAgent e um WriterAgent manipular assíncrona de e/S para o programa, precisamos simplesmente vinculá-las para os blocos de transformador de rede para começar o processamento. Isso pode ser feito facilmente depois de vincular os dois blocos em conjunto:

censor.link_target(&format);

ReaderAgent r(&censor);
r.start();

WriterAgent w(&format);
w.start();

O ReaderAgent é criado com uma referência para o censor para que ele corretamente pode enviar mensagens, enquanto o WriterAgent é criado com uma referência para o formato para que ele possa recuperar mensagens. Cada agente é iniciado com o seu início API, os agentes para execução dentro do Runtime de simultaneidade de agenda. Como cada agente chama o agent::wait(this) em seu próprio destruidor, a execução aguardará até que os dois agentes tiverem alcançado seu estado concluído.

Sincronizando para cima

Este artigo foi escrito para oferecer um vislumbre em algumas das novas possibilidades para programação de ator e o processamento de fluxo de dados incorporada ao Visual Studio 2010. Incentivamos você a experimentá-lo.

Se você quiser se aprofundar mais profunda, há vários outros recursos que não foram capazes de abordar neste artigo: criação de bloco de mensagem personalizada, a filtragem de mensagens e muito mais. O computação em paralelo developer center no MSDN (msdn.microsoft.com/concurrency ) contém mais detalhes e orientações passo a passo de como esse novo modelo de programação e interessante pode ajudar a paralelizar o seu programa de formas totalmente novas.

Michael Chu é um engenheiro de desenvolvimento de software no grupo de plataforma de computação paralela da Microsoft. Ele trabalha na equipe do Runtime de simultaneidade.

Varadarajan Krishnan é um engenheiro de desenvolvimento de software no grupo de plataforma de computação paralela da Microsoft. Ele trabalha na equipe do Runtime de simultaneidade.

Graças aos seguintes especialistas técnicos para revisão deste artigo: Equipe do Runtime de simultaneidade