: MSDN Magazine October 2007

October 2007
OCTOBER 2007 Volume 21 Número 10

• Segmentos Agrupados: Aumente a escalabilidade com as novas APIs do pool de segmentos
Robert Saccone

• SEGMENTOS DO WPF: Crie aplicativos que respondem melhor com o despachante
Shawn Wildermuth

• LINQ paralelo: Como executar consultas em processadores de vários núcleos
Joe Duffy e Ed Essey

• Desempenho paralelo: Otimizar o código gerenciado para máquinas de vários núcleos
Daan Leijen e Judd Hall

• Aplicativos móveis: Ajuste o volume do toque do celular ao som ambiente
Chris Mitchell

Code Download (1483 KB) HTMLHelp Format
COLUNAS

• Tudo sobre o CLR: IronPython e o runtime de linguagem dinâmico
Bill Chiles

• Instintos básicos: Inferência de tipos no Visual Basic 2008
Bill Horst

• Pontos de dados: Expressões de tabela comuns
John Papa

• Cutting Edge: Arquitetura de aplicativos AJAX, parte 2
Dino Esposito

• Execução de teste: A automação de teste com o Windows XP Embedded
Dr. James McCaffrey e Mike Hall

• Fundamentos: O texto 3D no WPF
Charles Petzold

• Windows com C++: Agendador de Tarefas 2.0
Kenny Kerr

• Netting C++: Descrevendo o sistema EEK!
Stanley B. Lippman

• .Assuntos .NET: Monitor de deadlocks
Stephen Toub

• { End Bracket }: Mashup de casamento
Michael Richter

Em cada edição

• Nota do editor: A complexidade da complexidade
Howard Dierking

• Toolbox: Gerencie bancos de dados, FTP mais fácil e caches agrupados
Scott Mitchell


Segmentos Agrupados: Amplie a Escalabilidade com Novas APIs de Pool de Segmentos

Segmentos Agrupados

Amplie a Escalabilidade com Novas APIs de Pool de Segmentos

Robert Saccone
Trechos deste artigo se baseiam em uma versão de pré-lançamento do Windows Server 2008. Detalhes do conteúdo estão sujeitos a alterações.
Este artigo aborda:
  • Funcionalidade de pool de segmentos
  • Limitações herdadas de pool de segmentos
  • Novas APIs de pool de segmentos
  • Objetos de pool de segmentos
Este artigo utiliza as seguintes tecnologias:
Windows Vista, Windows Server 2008
Conteúdo
Visão geral de Pool de Segmentos
Objetos de Pool de Segmentos
Objetos de Ambientes de Chamada de Retorno
Objetos de Trabalho
Objetos de Espera
Objetos de Timer
Objetos de Conclusão E/S
Simplificar a Limpeza com Grupos
APIs de Instância de Chamada de Retorno
Faça um Teste com os Aplicativos de Amostra

C
om o lançamento do Windows Vista®e o iminente lançamento do Windows Server® 2008, a Microsoft enriqueceu a plataforma Windows® com tanta tecnologia nova para desenvolvedores de aplicativos gerenciados que é fácil acabarmos esquecendo de citar os avanços que podem beneficiar desenvolvedores acostumados com o Windows. O componente de pool de segmentos, que faz parte da plataforma desde o lançamento do Windows 2000, por exemplo, passou por uma completa reestruturação. A nova implementação traz um novo API de pool de segmentos que deve facilitar para os desenvolvedores escreverem códigos corretamente. As APIs herdadas ainda são contempladas, de forma que os aplicativos preexistentes ainda podem ser executados, mas como você verá, há diversos benefícios na migração para as novas APIs.

Visão geral de Pool de Segmentos

Antes de abordarmos as novas APIs, vamos rever o tipo de funcionalidade que o componente de pool de segmentos fornece. Em poucas palavras, o pool de segmentos habilita a chamada de funções a determinados intervalos de tempo, assincronicamente, quando um objeto de kernel tiver sido sinalizado, e quando solicitações E/S assíncronas tiverem sido concluídas. A cada vez que houver necessidade de solicitar uma dessas funções de chamada de retorno, uma solicitação de função de chamada de retorno é adicionada ao final da fila de ordem FIFO (primeiro a entrar, primeiro a sair) mantida pelo pool de segmentos. Segmentos de trabalho gerenciados internamente pelo pool de segmentos removem itens da fila e convocam as funções de chamada de retorno. Já que o pool de segmentos gerencia o tempo de vida útil dos segmentos de trabalho, o desenvolvedor de aplicativos não precisa se ocupar desse gerenciamento. Isso também significa que o aplicativo nunca deve encerrar um segmento de trabalho de um pool de segmentos usando a função TerminateThread ou chamando a função ExitThread a partir de outra função executada em um segmento de trabalho.

Aplicativos sempre criam segmentos dedicados que gastam muito tempo esperando um evento ou ativando periodicamente o pool para alguma condição. O pool de segmentos torna essa função mais eficiente. Para um aplicativo de servidor que esteja tentando processar diversas solicitações de clientes, o pool de segmentos habilita o modelo de concorrência de servidor preferencial para a plataforma Windows, que é processar simultaneamente tantas solicitações quantos forem os processadores no sistema. Uma alternativa que vi os desenvolvedores testando é dedicar um segmento para cada cliente que está sendo atendido, mas este modelo não se adapta bem a um grande número de clientes. Não apenas faz com que um grande número de segmentos consuma recursos importantes, como o custo da troca entre eles acaba se tornando significativo e influi na qualidade do serviço que cada cliente recebe.

O novo componente de pool de segmentos aborda diversas limitações de pool de segmentos herdado. Por exemplo, o novo pool de segmentos permite criar múltiplos pools em cada processo, enquanto o antigo modelo permitia criar apenas um. Isso permite que você isole as tarefas que o aplicativo executa por um algum critério de escolha. Por exemplo: suponha que você esteja escrevendo um aplicativo de servidor distribuído que usará o pool de segmentos para processar assincronicamente conclusões E/S em conexões de rede. As conexões de rede são de dois tipos, uma de um aplicativo cliente e a outra de outras instâncias do aplicativo de servidor executado em diferentes computadores. Vamos considerar ainda que você deve manter um grande número de conexões cliente em cada servidor e que o número de solicitações de cliente é muito maior do que as conexões servidor £ servidor, mas são estas que têm a prioridade.

Quando as conexões de rede cliente e servidor são processadas em um pool de segmentos único, há uma fila única para a qual todas as notificações de conclusões são enviadas. Isso significa que uma conclusão E/S de uma conexão de servidor não será processada até que todas conclusões E/S que estejam na sua frente na fila sejam processadas, independente do cliente. Isso pode resultar em atrasos significativos no processamento de conexões de servidor quando o sistema está sob forte carga do cliente. Estabelecer um pool de segmentos para as conexões de cliente e outro para as conexões de servidor permitem o isolamento porque cada pool de segmento possui sua própria fila e conjunto de segmentos de trabalho. O agendador no sistema operacional irá garantir que os processadores sejam divididos igualitariamente entre os segmentos em ambos os pools. Entretanto, é importante usar múltiplos pools de segmentos criteriosamente, uma vez que criar vários em cada processo pode prejudicar a performance e reduzir o desempenho.

O pool de segmentos herdado utilizou dois tipos de segmentos de trabalho: E/S e não-E/S, o que às vezes causou confusão. Se o desenvolvedor não entendeu a diferença, o resultado poderia ser desempenho limitado ou comportamento incorreto. Além disso, ter dois grupos distintos de segmentos significaria que a implementação do pool de segmentos propriamente era menos eficiente, na medida em que ela não poderia dividir segmentos entre diferentes tipos de funções de chamadas de retorno. No novo pool de segmentos, essa diferença é removida e todos os segmentos de trabalho são iguais.

Na API herdada, QueueUserWorkItem foi utilizada para enfileirar uma função a ser chamada assincronicamente em um segmento de trabalho de um pool de segmentos. A API herdada não fornecia meios para que o aplicativo determinasse quando o segmento de trabalho tinha terminado de executar a função de chamada de retorno. Se a função de chamada de retorno estivesse em uma DLL, seria virtualmente impossível garantir que a DLL fosse descarregada com segurança. Isso significa que descarregar a DLL às vezes causou o mal funcionamento do processo de hospedagem. Não havia nenhuma outra maneira de cancelar as chamadas de retorno que estavam na fila de pool de segmentos aguardando o processamento. Assim, não havia escolha a não ser esperar que todas as solicitações fossem atendidas, o que poderia causar atrasos significativos.

Finalmente, a API herdada não separa a alocação de recurso do uso de recurso. Já que essas alocações de recurso falham ou podem falhar, a API herdada dificulta o desenvolvimento de sistemas confiáveis. A nova API de pool de segmentos distingue a separação de alocação e uso de recursos, de forma que, uma vez alocado um recurso com sucesso, praticamente não existe possibilidade de falha em códigos bem-escritos quando esses recursos são utilizados.

A nova API de pool de segmentos é baseada em objeto, no qual cada tipo de objeto possui um conjunto de funções para criação, limpeza e modificação de propriedades. A Figura 1 resume os tipos de objetos expostos pela API. É importante entender que todos os objetos criados pelo pool de segmentos, e todos os segmentos de trabalho gerenciados por ele, tornam-se parte do processo do aplicativo que os utiliza.

Figure 1 Tipos de Objetos de Pool de Segmentos (Close Figure 1)

Tipo de Objeto | Descrição
TP_POOL | Pool de segmentos usados para executar chamadas de retorno.
TP_TIMER | Solicite uma chamada de retorno no tempo decorrido.
TP_WAIT | Solicite uma função de chamada de retorno quando um objeto de kernel for sinalizado e a espera se encerrar.
TP_WORK | Solicite uma função de chamada de retorno assincronicamente.
TP_IO | Solicite uma função de chamada de retorno quando uma E/S assíncrona estiver completa.
TP_CLEANUP_GROUP | Rastreie um ou mais objetos de chamada de retorno do pool de segmentos.
TP_CALLBACK_ENVIRON | Junte um pool de segmentos aos seus objetos de chamadas de retorno e, opcionalmente, um grupo de limpeza.

Objetos de Pool de Segmentos

O primeiro passo ao preparar um pool de segmentos para uso é criá-lo usando a função CreateThreadpool, mostrada na Figura 2. A função obtém um parâmetro reservado que deve ser NULL. Se a função for bem-sucedida, ela retorna um PTP_POOL representando um pool de segmentos novamente alocado. Se a função falhar, ela retorna NULL, e o GetLastError pode ser usado para obter mais informações sobre os erros.

Figure 2 API de Objeto de Pool de Segmentos (Close Figure 2)
PTP_POOL WINAPI CreateThreadpool(PVOID reserved);

BOOL WINAPI SetThreadpoolThreadMinimum(PTP_POOL ptpp,
                      DWORD cthrdMin);

VOID WINAPI SetThreadpoolThreadMaximum(PTP_POOL ptpp,
                      DWORD cthrdMost);

VOID WINAPI CloseThreadpool(PTP_POOL ptpp);

Uma vez criado o pool de segmentos, é possível controlar o número máximo e mínimo de segmentos que o pool gerenciará com as duas novas APIs. Por padrão, o pool de segmentos mínimo é 0 e o máximo é 500. Esses números foram escolhidos para compatibilidade com o pool de segmentos herdado, uma vez que alguns aplicativos usando o pool de segmentos herdado requerem um grande número de segmentos. Esses padrões podem mudar em versões futuras do Windows na medida em que qualquer aplicativo que usa um número de segmentos tão grande tende a ter fraco desempenho devido à troca excessiva de contexto.

Há alguns recursos interessantes para ficar-se atento com essas APIs. Observe que o SetThreadpoolThreadMinimum retorna um BOOL, enquanto o SetThreadpoolThreadMaximum não retorna nada. O motivo é que, se ao configurar-se o número mínimo de segmentos é necessário o SetThreadpoolThreadMinimum para aumentar o número de segmentos de trabalho no pool e alcançar-se o novo mínimo, e uma falha ocorre enquanto se alocam os segmentos, esse erro pode ser relatado ao chamador. Configurar o número máximo de segmentos no pool não causa nenhuma alocação de recursos porque tudo o que a função faz é definir um limite máximo de segmentos que o pool pode criar. O pool de segmentos irá variar o número de segmentos do mínimo ao máximo, baseado em sua carga de trabalho real. Infelizmente, o SetThreadpoolThreadMinimum tem um bug no Windows Vista que se manifesta quanto é necessário aumentar o número de segmentos de trabalho no pool: a função retorna sem criar imediatamente os segmentos adicionais. Esse bug será corrigido no Service Pack 1 do Windows Vista e não é um problema no Windows Server 2008. Para o Windows Vista hoje, no entanto (exceto a questão da falha da criação de segmentos de trabalho), uma vez que haja trabalho enfileirado suficiente para fazer o pool de segmentos criar o número mínimo de segmentos especificado, o pool de segmentos justificará o mínimo e manterá pelo menos aquela quantidade de segmentos no pool.

Configurar o número mínimo e máximo de segmentos para o mesmo valor cria um pool de segmentos persistentes que nunca se encerrarão até que o pool seja fechado. É útil usar um pool de segmentos com uma função, como a RegNotifyChangeKeyValue, que deve ser chamada de um segmento persistente. Para obter mais informações sobre RegNotifyChangeKeyValue, consulte msdn2.microsoft.com/ms724892.

Observe que chamar o SetThreadpoolThreadMinimum com um mínimo maior do que o máximo atual não apenas faz com que um novo mínimo seja definido mas também que um novo máximo seja definido para esse mínimo. Chamar o SetThreadpoolThreadMaximum com um máximo que é menor do que o mínimo faz o mínimo ser definido no mesmo nível do novo máximo. O parâmetro de contagem dos segmentos é especificado como um DWORD em ambas as funções, mas a faixa completa de um DWORD não está disponível para a contagem de segmentos. Internamente, o valor é tratado como LONG e é validado para ser maior ou igual a 0.

Esta é a hora certa para explicar a filosofia de relatório de erro das APIs do pool de segmentos. Se uma operação é das que podem ser previstas como falha, o resultado é relatado por um código de retorno a partir da função que está sendo chamada. Entretanto, os erros excepcionais são relatados usando-se as exceções estruturadas. Por exemplo, para as funções SetThreadpoolThreadMinimum e SetThreadpoolThreadMaximum, informar um valor inválido para o número de segmentos de trabalho ou um pool de segmentos NULL causa o surgimento de uma exceção estruturada. Entretanto, uma falha ao criar um novo segmento de trabalho durante uma chamada para o SetThreadpoolThreadMinimum é relatada para o chamador como FALSE, devido à falha de alocação de recursos ser um erro esperado. Pode ser tentador envolver os manipuladores de exceção estruturada em chamadas para as APIs do pool de segmentos, de forma que um aplicativo não será encerrado devido à exceção sem tratamento, mas isso não é uma boa idéia na medida em que capturar essas exceções e continuar executando o programa apenas serve para esconder um problema do próprio aplicativo. Esse procedimento apenas dificulta o diagnóstico do problema quando o aplicativo finalmente falha.

A compreensão de como o modelo de concorrência do pool de segmentos é implementado ajuda no uso adequado dessas APIs. Esse modelo se baseia no número de processadores disponíveis no sistema. Por exemplo, se o sistema possui dois processadores físicos com dois núcleos cada um, o ideal é que você tenha apenas dois segmentos executáveis na maioria das vezes. Isso elimina a sobrecarga de alternância de contexto. Entretanto, se na maioria das vezes o aplicativo possui apenas uma chamada de retorno, é perfeitamente aceitável ajustar o pool de segmentos para um segmento. Voltando ao exemplo de dois processadores com dois núcleos, considerando que o aplicativo tem uma dúzia de chamadas de retorno pendentes, deveria haver pelo menos quatro segmentos de trabalho no pool para que cada processador processe um item. Entretanto, para manter uma concorrência ideal, talvez seja necessário que o pool de segmentos contenha mais de quatro segmentos de trabalho. Para entender por que, vamos considerar um exemplo no qual cada um dos quatro núcleos já está ocupado executando uma função de chamada de retorno e também há uma chamada de retorno pendente na fila do pool de segmentos. O que acontece se um daqueles segmentos de trabalho executando chamadas de retorno trava? Existem três possibilidades:

  • Se o pool de segmentos possui um segmento disponível, ele irá enviar outro segmento para remover o próximo item da fila e chamar a função de chamada de retorno pendente.
  • Se o pool de segmentos não possui outro segmento de trabalho disponível e se o número de segmentos de trabalho criado é menor do que a contagem máxima de segmentos, depois de um pequeno intervalo será criado um novo segmento de trabalho que executará a função de chamada de retorno pendente.
  • Se o pool de segmentos não possui outro segmento de trabalho disponível e se o número de segmentos de trabalho já criado chegou à contagem máxima de segmentos, não serão criados novos segmentos. Além disso, o item da chamada de retorno pendente permanecerá na fila até que um segmento de trabalho previamente enviado termine de processar a função de chamada de retorno e retorne ao pool de segmentos para verificar se há algum item para ser processado.

Desta forma, se uma função de chamada de retorno travar durante a execução, o tamanho máximo do pool de segmentos deve ser maior do que o número de processadores disponíveis no sistema para realizar a concorrência máxima que o hardware suporta. O ideal é que uma função de chamada de retorno nunca trave. Uma função de chamada de retorno que trava não apenas diminui a concorrência mas também diminui o nível de reutilização de segmentos de trabalho. Por causa disso, vale a pena se esforçar para eliminar ou minimizar o tempo de travamento de uma função de chamada de retorno. Você pode reduzir esse tempo se entender o que a chamada de retorno está travando. Por exemplo, se a chamada de retorno está executando E/S síncrono, tente alterar para E/S assíncrono, que, ao contrário, completa o pool de segmento. E procure minimizar qualquer sincronização que deve ocorrer entre as funções de chamada de retorno porque o bloqueio de contenção pode causar travamento.

Se a maioria das funções de chamada de retorno não bloqueiam, o pool de segmentos pode ser ajustado para conter pouco excesso de segmentos na quantidade de processadores disponíveis. Se a maioria das funções de chamada de retorno vai terminar bloqueando, o pool de segmentos poderá ser ajustado para conter muito mais segmentos de trabalho do que a quantidade de processadores disponíveis. Nesse caso, a maioria dos segmentos terminarão em estado de espera. Quando as funções de chamada de retorno estiverem concluídas, para maximizar a produtividade geral, o pool de segmentos manterá os segmentos de trabalho, reduzindo a quantidade de segmentos executáveis ideal para a configuração do processador. A questão é que você deseja ajustar o pool de segmentos de forma que quando houver tarefas a executar e largura de banda suficiente disponível no processador, o pool possa criar mais segmentos para fazer o trabalho. Entretanto, se você acha que, na verdade, seu aplicativo utiliza uma quantidade desproporcional de segmentos £ como os 500, nas configurações padrão £ ou que há custo exagerado de alternância de contexto, isso significa que o aplicativo não foi desenvolvido adequadamente e você precisa verificar seu código, utilizando o monitor de desempenho e um código de perfil para determinar onde as melhorias podem ser feitas.

Uma vez encerrado, o pool de segmentos deve ser fechado usando a função CloseThreadpool. O pool de segmentos é fechado imediatamente se não houver objetos de chamada de retorno pendentes ligados a ele. Se houver, então o pool de segmentos será liberado assincronicamente quando os objetos pendentes forem liberados.

Objetos de Ambientes de Chamada de Retorno

Agora que você entende como criar um pool de segmentos, o próximo passo é verificar o que significa um objeto de ambiente de chamada de retorno e como deve ser usado. Um objeto de ambiente de chamada de retorno é usado para ligar uma instância de pool de segmentos às instâncias de objetos de chamadas de retorno dos pools de segmentos que o seu aplicativo cria para realmente executar funções em seu pool de segmentos. Um objeto de ambiente de chamada de retorno também permite que você anexe um objeto de limpeza, que torna muito mais simples os objetos de chamadas de retorno dos pools de segmentos. Os grupos de limpeza serão discutidos mais à frente, neste artigo. As APIs do ambiente de chamada de retorno são exibidas na Figura 3.

Figure 3 APIs de Ambientes de Chamadas de Retorno (Close Figure 3)
VOID InitializeThreadpoolEnvironment(PTP_CALLBACK_ENVIRON pcbe);
VOID DestroyThreadpoolEnvironment(PTP_CALLBACK_ENVIRON pcbe);
VOID SetThreadpoolCallbackPool(PTP_CALLBACK_ENVIRON pcbe,
                    PTP_POOL ptpp);
VOID SetThreadpoolCallbackLibrary(PTP_CALLBACK_ENVIRON pcbe,
                    PVOID mod);
VOID SetThreadpoolCallbackRunsLong(PTP_CALLBACK_ENVIRON pcbe);

VOID SetThreadpoolCallbackCleanupGroup(PTP_CALLBACK_ENVIRON pcbe,
                    PTP_CLEANUP_GROUP ptpcg,
                    PTP_CLEANUP_GROUP_CANCEL_CALLBACK pfng);

O primeiro passo ao criar um ambiente de chamada de retorno é estabelecer uma estrutura TP_CALL_BACK_ENVIRON em armazenamento estático, na pilha, ou então alocá-la fora da pilha. O próximo passo é inicializar o ambiente com a função InitializeThreadpoolEnvironment, que aponta para TP_CALLBACK_ENVIRON. Finalmente, para associar um pool de segmentos ao ambiente de chamada de retorno, use SetThreadpoolCallbackPool. Se você não associar um pool de segmentos ao ambiente de chamada de retorno ou se NULL estiver especificado na chamada para SetThreadpoolCallbackPool, será usado o pool de segmento padrão do processo. Como você poderá ver, o ambiente de chamada de retorno é usado eventualmente para criar várias instâncias de objetos de chamadas de retorno do pool de segmentos. Quando seu aplicativo tiver terminado de modificar as propriedades do ambiente de chamadas de retorno e de criar todas as instâncias necessárias dos objetos de chamadas de retorno, o ambiente de chamada de retorno deve ser destruído usando a função DestroyThreadpoolEnvironment.

A API SetThreadpoolCallbackRunsLong é usada para indicar ao pool de segmentos que as funções de chamada de retorno associadas a este ambiente podem não retornar rapidamente. Você verá posteriormente como isso afeta o resultado da chamada da função de API CallbackMayRunLong.

Mencionei anteriormente que o pool de segmentos herdado não deu suporte a um aplicativo que precisava determinar quando era seguro descarregar uma DLL contendo uma função de chamada de retorno que seria processada no pool de segmentos. A nova API do pool de segmentos fornece a função SetThreadpoolCallbackLibrary para garantir que uma biblioteca permaneça carregada durante o processamento de uma função de chamada de retorno dentro de uma DLL. O que acontece, essencialmente, é que, antes de uma função de chamada de retorno ser solicitada, é obtido o bloqueio do carregador do sistema operacional (sempre mantido quando uma DLL está em processo de carga ou descarga) e a contagem de referência da DLL é aumentada. Posteriormente, o bloqueio do carregador é liberado. Depois que a chamada de retorno completa o processamento, o bloqueio do carregador é obtido novamente para reduzir a contagem de referência. Isso impossibilita o descarregamento da DLL durante o processamento de uma função de chamada de retorno. Observe que, enquanto as chamadas de retorno estão pendentes na fila do pool de segmentos, a contagem de referência na DLL permanece inalterada. Isso significa que uma DLL pode ser descarregada com chamadas de retorno pendentes. No entanto, esse é o trabalho da função DllMain, que você gravou para ter certeza de lidar com o evento DLL_PROCESS_DETACH e cancelar todas as chamadas de retorno pendentes. Explicarei isso daqui a pouco.

Objetos de Trabalho

Um objeto de trabalho é usado para fazer com que o pool de segmentos solicite uma função de chamada de retorno assincronicamente em um segmento de trabalho. A função CreateThreadpoolWork, mostrada na Figura 4 junto com todas as APIs relacionadas a outros objetos de trabalho, é usada para criar um objeto de trabalho.

Figure 4 APIs de Objetos de Trabalho (Close Figure 4)
PTP_WORK WINAPI CreateThreadpoolWork(PTP_WORK_CALLBACK pfnwk,
                    PVOID Context,
                    PTP_CALLBACK_ENVIRON pcbe);
VOID WINAPI SubmitThreadpoolWork(PTP_WORK pwk);
VOID CALLBACK WorkCallback(PTP_CALLBACK_INSTANCE Instance,
                    PVOID Context,
                    PTP_WORK Work);
VOID WINAPI WaitForThreadpoolWorkCallbacks(PTP_WORK pwk,
                    BOOL fCancelPendingCallbacks);
VOID WINAPI CloseThreadpoolWork(PTP_WORK pwk);

BOOL WINAPI TrySubmitThreadpoolCallback(PTP_SIMPLE_CALLBACK pfns,
                    PVOID pv,
                    PTP_CALLBACK_ENVIRON pcbe);
VOID CALLBACK SimpleCallback(PTP_CALLBACK_INSTANCE Instance,
                    PVOID Context);

O primeiro parâmetro, pfnwk, é um ponteiro para a função de retorno de chamada a ser executada por um segmento de trabalho. O segundo parâmetro, Context, é digitado como PVOID e pode ser usado para fornecer qualquer dado específico do aplicativo de que a função de chamada de retorno necessite. Finalmente, o último parâmetro é um ponteiro para TP_CALLBACK_ENVIRON. O objeto de trabalho será ligado ao pool de segmentos e ao grupo de limpeza (se houver um) associado ao ambiente de chamada de retorno.

Depois que o objeto de trabalho tiver sido criado, ele pode ser enfileirado para o pool de segmentos usando a função SubmitThreadpoolWork. Eventualmente, um segmento de trabalho removerá o objeto de trabalho da fila e chamará sua função de chamada de retorno associada. Cada chamada para SubmitThreadpoolWork irá gerar outra chamada para a função de chamada de retorno do item de trabalho.

A função WorkCallback para um objeto de trabalho opera com três parâmetros. O primeiro, Instance, é usado para identificar a instância específica do processamento da função de chamada de retorno e permanece válido para processar a função de chamada de retorno. O parâmetro Context é o ponteiro fornecido como o segundo parâmetro para CreateThreadpoolWork. O parâmetro final, Work, é a instância do objeto de trabalho para a qual a função de chamada de retorno está sendo solicitada.

É importante entender que uma função de chamada de retorno pode ser solicitada por qualquer segmento de trabalho no pool de segmentos. O princípio a ser seguido é que a função de chamada de retorno não deve fazer suposições sobre o segmento de trabalho no qual ela será processada e deve manter o segmento de trabalho no mesmo estado em que estava antes da solicitação. Por exemplo, se a função de chamada de retorno usará COM, então deve chamar CoInitializeEx a cada vez que for solicitada. A função de chamada de retorno também deve solicitar CoUninitialize antes de retornar, na medida em que o segmento de trabalho pode ser reutilizado para enviar as funções de chamada de retorno para diversas tarefas diferentes, ou ela também pode ser encerrada quando retornar para o pool de segmentos. Isso previne vazamento de recursos e descarta informações de estado que poderiam afetar o processamento da próxima função de chamada de retorno, que pode ter requisitos diferentes para o ambiente de processamento do segmento. O Aplication Verifier é uma ferramenta de verificação de runtime para código não-gerenciado. Ela ajuda a identificar erros de programação sutis que seriam mais difíceis de notar em testes de aplicativos normais. Ele foi aprimorado para ajudar na identificação de erros de programação relacionados ao pool de segmentos. Entre os erros que ele pode detectar estão chamadas desbalanceadas CoInitializeEx e CoUninitialize, alterações na prioridade e na afinidade de segmentos que não foram revertidas antes que o segmento de trabalho retornasse para o pool de segmentos, representações não-revertidas e seções críticas órfãs. Para uma lista completa, consulte o documento instalado junto com o Application Verifier, cujo download pode ser feito em microsoft.com/downloads/details.aspx?familyid=bd02c19c-1250-433c-8c1b-2619bd93b3a2.

A função The WaitForThreadpoolWorkCallbacks bloqueia o segmento de chamada até que todas as funções de chamada de retorno pendentes para um objeto de trabalho tenham completado o processamento. O segundo parâmetro controla a decisão se as chamadas de retorno pendentes (aquelas que foram enfileiradas para o pool de segmento mas ainda não foram enviadas para processamento por um segmento de trabalho) devem ser processadas ou canceladas. Tenha cuidado com essa API: usá-la em uma função de chamada de retorno de trabalho pode causar bloqueio.

Concluindo o conjunto de API relativo a objetos de trabalho está a função CloseThreadpoolWork. O objeto de trabalho é liberado imediatamente se não houver chamadas de retorno pendentes. De outra forma, o objeto de trabalho será liberado assincronicamente assim que essas chamadas estiverem completadas. Isso também significa que qualquer chamada de retorno aguardando processamento na fila de pool de segmentos será cancelada.

A função restante na Figura 4, TrySubmitThreadpoolCallback, oferece o equivalente a criar um objeto de trabalho, enviá-lo ao pool de segmentos, e garantir que o objeto de trabalho será fechado quando a chamada de retorno especificada pelo parâmetro pfns tiver completado o processamento. A assinatura da função de chamada de retorno deve estar em conformidade com a assinatura de SimpleCallback, em Figura 4. Isso é um pouco diferente da chamada de retorno associada ao objeto de trabalho. Assim que o pool de segmentos cuidar da alocação e da liberação do objeto de trabalho internamente, a função de chamada de retorno é apenas passada do ponteiro da instância da função e do contexto definido do aplicativo, especificado no parâmetro de Contexto TrySubmitThreadpoolCallback. Assim que o TrySubmitThreadpoolCallback aloca recursos para fazer seu trabalho, existe a possibilidade de falha, que é a causa do retorno o código BOOL.

Objetos de Espera

Objetos de espera são usados para chamar a função de chamada de retorno quando um objeto de kernel tiver sido sinalizado ou quando o tempo de espera especificado tiver expirado. A função CreateThreadpoolWait é usada para criar um objeto de espera. Esses parâmetros seguem o mesmo modelo de CreateThreadpoolWork, com a exceção de que o ponteiro da função de chamada de retorno fornecido no primeiro parâmetro deve corresponder à assinatura da função WaitCallback, conforme mostrado em Figura 5.

Figure 5 APIs de Objetos de Espera (Close Figure 5)
PTP_WAIT WINAPI CreateThreadpoolWait(PTP_WAIT_CALLBACK pfnwa,
             PVOID pv,
             PTP_CALLBACK_ENVIRON pcbe);
VOID WINAPI SetThreadpoolWait(PTP_WAIT pwa,
             HANDLE h,
             PFILETIME pftTimeout);
VOID CALLBACK WaitCallback(PTP_CALLBACK_INSTANCE Instance,
             PVOID Context,
             PTP_WAIT Wait,
             TP_WAIT_RESULT WaitResult);
VOID WINAPI WaitForThreadpoolWaitCallbacks(PTP_WAIT pwa,
             BOOL fCancelPendingCallbacks);
VOID WINAPI CloseThreadpoolWait(PTP_WAIT pwa);

Depois de criar um objeto de espera, a próxima etapa é fazer o pool de segmentos aguardar que um objeto de kernel fique sinalizado. A função SetThreadpoolWait é usada para isso. O primeiro parâmetro, pwa, é um ponteiro para a instância do objeto de espera ser ajustada. O segundo parâmetro é o HANDLE, para que o objeto de kernel aguarde. O parâmetro final, pftTimeout, é um ponteiro para uma estrutura FILETIME que indica quanto tempo o pool de segmentos deve esperar para que o objeto de kernel seja sinalizado. O tempo de espera pode ser expresso em termos absolutos ou relativos e é especificado em unidades de 100 nanosegundos. Passar um valor positivo indica que o tempo limite é absoluto desde 1/1/1600. Um valor negativo indica um tempo relativo para o tempo atual na chamada da função. Passar o valor 0 indica que a espera se encerra imediatamente. Finalmente, passar NULL indica uma espera infinita, que nunca se encerra.

Quando o objeto de kernel torna-se sinalizado ou a espera chega ao limite de tempo, o pool de segmentos chama a função WaitCallback dos objetos de espera. Os primeiros dois parâmetros são exatamente os mesmos da função WorkCallback descrita anteriormente. O parâmetro Wait indica para qual objeto de espera a chamada de retorno está sendo solicitada e o WaitResult usado para indicar o motivo da solicitação. WaitResult será WAIT_ABANDONED_0, WAIT_OBJECT_0, ou WAIT_TIMEOUT. Se o WaitResult estiver configurado para WAIT_OBJECT_0, significa que o objeto de kernel ficou sinalizado e que a espera foi atendida.

Se o WaitResult estiver configurado para WAIT_TIMEOUT, significa que a espera não foi atendida e que o tempo especificado na chamada para o SetThreadpoolWait já passou. Um resultado de WAIT_ABANDONED_0 indica que o objeto especificado é um mutex e que não foi liberado pelo segmento que possuía o objeto de mutex antes de encerrar. Entretanto, deve-se evitar o uso de mutex com um objeto de espera porque o segmento de trabalho que chama a função WaitCallback não é o mesmo que realizou a espera no mutex. O pool de segmentos utiliza um tipo diferente de segmento, chamado de segmento de serviço, para esperar no objeto de kernel. Na verdade, é o segmento de serviço que possui o mutex. Os segmentos de serviço não estão expostos aos aplicativos, então não há forma de liberar o mutex, na medida em que o segmento adquire a propriedade dele.

Depois que a função WaitCallback tiver sido solicitada por um objeto de espera, SetThreadpoolWait deve ser chamada novamente para reutilizar o objeto de espera e colocar o pool de segmentos em espera novamente por um objeto de kernel para ficar sinalizado. Observe que, ao chamar SetThreadpoolWait novamente para reutilizar o objeto de espera, você tem a opção de especificar qualquer identificador de objeto de kernel para espera. Você não precisa usar o identificador especificado na primeira chamada para SetThreadpoolWait se desejar esperar em um objeto de kernel diferente. Finalmente, as APIs de objeto de espera restantes, WaitForThreadpoolWaitCallbacks e CloseThreadpoolWait, comportam-se da mesma forma que as contrapartes de seus objetos de trabalho.

Objetos de Timer

Objetos de timer são usados para solicitar uma função de chamada de retorno quando o objeto de timer conclui seu tempo. São criados usando-se a função CreateThreadpoolTimer, exibida na Figura 6 com as outras APIs relacionadas ao objeto de timer. Mais uma vez, os parâmetros seguem o mesmo modelo da função CreateThreadpoolWork (na Figura 4), com a exceção de que o ponteiro para a função de chamada de retorno fornecido no primeiro parâmetro deve combinar com a assinatura da função TimerCallback, conforme mostrado na Figura 6.

Figure 6 APIs de Objetos de Timer (Close Figure 6)
PTP_TIMER WINAPI CreateThreadpoolTimer(PTP_TIMER_CALLBACK pfnti,
              PVOID pv,
              PTP_CALLBACK_ENVIRON pcbe);
VOID WINAPI SetThreadpoolTimer(PTP_TIMER pti,
              PFILETIME pftDueTime,
              DWORD msPeriod,
              DWORD msWindowLength);
VOID CALLBACK TimerCallback(PTP_CALLBACK_INSTANCE Instance,
              PVOID Context,
              PTP_TIMER Timer);
VOID WINAPI WaitForThreadpoolTimerCallbacks(PTP_TIMER pti,
              BOOL fCancelPendingCallbacks);
BOOL WINAPI IsThreadpoolTimerSet(PTP_TIMER pti);
VOID WINAPI CloseThreadpoolTimer(PTP_TIMER pti);

Depois de criar um objeto de timer, o próximo passo é configurá-lo usando a função SetThreadpoolTimer. O parâmetro pftDueTime é usado para definir o tempo quando o timer inicialmente estiver concluído. Esse tempo é expresso da mesma forma que o tempo de conclusão usado na função SetThreadpoolWait descrita anteriormente. Como o nome sugere, o parâmetro msPeriod instala um timer, expresso em milissegundos e acionado periodicamente. Entretanto, quando o intervalo de tempo inicial expresso pelo pftDueTime tiver expirado e causado um enfileiramento da chamada de retorno para o pool de segmentos, cada término do intervalo subseqüente definido pelo msPeriod causará o enfileiramento de outra chamada de retorno.

O parâmetro msWindowLength especifica uma janela de tempo, expressa em milissegundos, durante a qual o pool de segmentos pode atrasar o término do timer. Esse parâmetro oferece mais eficiência quando você está utilizando um grande número de timers e o tempo de expiração não precisa ser exato. A janela é um fator de classificações que permite ao sistema unificar todos os encerramentos do timer que ficam juntos na janela, de forma que possam ser loteados. Isso é mais eficiente do que despertar um segmento, expirar um timer, adormecer, despertar um segmento, expirar outro timer... e assim por diante. O atraso ocorrerá somente se não existirem outros timers na janela que devem ser encerrados definitivamente. Para entender melhor como o msWindowLength pode ser usado, considere um aplicativo de servidor que possui um grande número de conexões cliente de entrada e que, para reduzir o uso de recursos, as conexões que não estiverem ativas por cinco minutos devem ser fechadas. Neste caso, é aceitável especificar uma janela de tamanho diferente de zero para os timers inativos, o que pode resultar em manter uma conexão ligeiramente depois do tempo gasto..

O SetThreadpoolTimer também pode ser usado para definir um novo tempo máximo, período, e a extensão da janela para um timer ajustado anteriormente. Se o parâmetro pftDueTime for NULL, ele parará de enfileirar chamadas de retorno para a função TimerCallback, mas as chamadas de retorno que já tiverem sido enfileiradas serão executadas. Então, você poderá cancelar um timer sem fechar o objeto dele, permitindo que ele seja reutilizado.

Quando o objeto de timer estiver concluído, uma solicitação para a chamada de retorno será enfileirada para o pool de segmentos. Um segmento de trabalho tomará o pedido e chamará a função TimerCallback fornecida com a chamada a SetThreadpoolTime. Os parâmetros para a função de chamada de retorno são os mesmos daqueles descritos para uma função de chamada de retorno de objeto de trabalho, com exceção de que o terceiro parâmetro é um PTP_TIMER em vez de um PTP_WORK.

A partir das funções de API do timer remanescente na Figura 5, a função IsThreadpoolTimerSet, como o nome indica, retorna TRUE se o timer tiver sido definido e FALSE, para o contrário. Os dois restantes, WaitForThreadpoolTimerCallbacks e CloseThreadpoolTimer comportam-se exatamente como suas contrapartes dos objetos de trabalho.

Objetos de Conclusão E/S

O tipo final do objeto de chamada de retorno suportado pelo pool de segmentos é o objeto de conclusão E/S. A Figura 7 lista todas as APIs relacionadas aos objetos de conclusão E/S. Um objeto de conclusão E/S é usado para unir um suporte de arquivo ao pool de segmentos, de forma que as notificações de conclusão E/S assíncronas são enfileiradas no pool de segmentos para serem processadas pelos segmentos de trabalho. Os parâmetros para a função CreateThreadpoolIo seguem um modelo similar para as outras funções de criação com a adição do parâmetro HANDLE, que deve ser aberto para as conclusões E/S sobrepostas.

Figure 7 APIs de Objetos de Conclusão E/S (Close Figure 7)
PTP_IO WINAPI CreateThreadpoolIo(HANDLE fl,
                PTP_WIN32_IO_CALLBACK pfnio,
                PVOID pv,
                PTP_CALLBACK_ENVIRON pcbe);
VOID WINAPI StartThreadpoolIo(PTP_IO pio);
VOID CALLBACK IoCompletionCallback(PTP_CALLBACK_INSTANCE Instance,
                PVOID Context,
                PVOID Overlapped,
                ULONG IoResult,
                ULONG_PTR NumberOfBytesTransferred,
                PTP_IO Io);
VOID WINAPI WaitForThreadpoolIoCallbacks(PTP_IO pio,
                BOOL fCancelPendingCallbacks);
VOID WINAPI CancelThreadpoolIo(PTP_IO pio);
VOID WINAPI WaitForThreadpoolIoCallbacks(PTP_IO pio,
                BOOL fCancelPendingCallbacks);
VOID WINAPI CloseThreadpoolIo(PTP_IO pio);

Conforme mostrada na Figura 7, o primeiro parâmetro para CreateThreadpoolIo é o HANDLE para receber notificações de conclusão, enquanto os parâmetros dois, três e quatro representam, respectivamente, o ponteiro para a função de chamada de retorno que será solicitada, um contexto opcional específico do aplicativo e um ponteiro para o ambiente de chamada de retorno. CreateThreadpoolIo retorna um ponteiro não-NULL, se for bem-sucedido, e, do contrário, um ponteiro NULL.

Para que as notificações de conclusão E/S possam ser processadas pelo pool de segmentos, a função StartThreadpoolIo deve ser solicitada antes para emitir cada operação de pedidos E/S no identificador. Se esse procedimento não for realizado, podem ocorrer conseqüências sérias porque o pool de segmentos ignorará a conclusão E/S quando ela acontecer, podendo causar corrompimento da memória. Você deve chamar uma função relacionada, CancelThreadpoolIo, quando a chamada para iniciar uma operação E/S assincronicamente retornar com um aviso de falha que não seja ERROR_IO_PENDING. Omitir acidentalmente a chamada para CancelThreadpoolIo fará com que o pool de segmentos cause vazamento de memória.

Quando ocorrer uma conclusão E/S, um segmento de trabalho chamará o IoCompletionCallback associado ao objeto de conclusão E/S. A assinatura para a função segue a mesma convenção de todas as outras chamadas de retorno: os dois primeiros parâmetros para a função são um ponteiro para a instância de chamada de retorno e o ponteiro de contexto fornecido para a função de criação. O terceiro parâmetro, Overlapped, é o ponteiro para a estrutura sobreposta fornecida quando se inicia a operação E/S assincronicamente. O quarto é IoResult, que contém o resultado da operação. O IoResult conterá NO_ERROR se a operação for bem-sucedida; caso contrário ele conterá um código de erro de sistema (ver msdn2.microsoft.com/ms681381). O quinto parâmetro, NumberOfBytesTransferred, contém o número de bytes transferidos durante a operação E/S, enquanto o sexto parâmetro é um ponteiro para o próprio objeto de conclusão E/S.

De novo, as funções restantes em Figura 7,WaitForThreadpoolIoCallbacks e CloseThreadpoolIo, comportam-se exatamente como as contrapartes dos objetos de trabalho.

Simplificar a Limpeza com Grupos

Agora vamos ver como os grupos de limpeza podem ajudar a simplificar o processo de limpeza de objetos de chamadas de retorno do pool de segmentos criado por um aplicativo. Ao associar um grupo de limpeza a um ambiente de chamada de retorno de pool de segmento, todos os objetos de chamada de retorno de pool de segmento que utilizam esse ambiente serão rastreados pelo grupo de limpeza. Quando o aplicativo terminar de usar os objetos de chamada de retorno, tudo o ele precisa fazer para garantir que cada um seja fechado é realizar uma função de chamada em vez de uma chamada fechada para cada objeto de chamada de retorno. De fato, se um objeto de chamada de retorno estiver associado a um grupo de limpeza, a API fechada não deverá ser solicitada.

O primeiro passo ao usar um grupo de limpeza é criar um objeto de grupo de limpeza com a função CreateThreadpoolCleanupGroup, mostrada na Figura 8.

Figure 8 APIs de Grupos de Limpeza (Close Figure 8)
PTP_CLEANUP_GROUP WINAPI CreateThreadpoolCleanupGroup(void);
VOID WINAPI CloseThreadpoolCleanupGroupMembers(PTP_CLEANUP_GROUP ptpcg,
                       BOOL fCancelPendingCallbacks,
                       PVOID pvCleanupContext);
VOID CALLBACK CleanupGroupCancelCallback(PVOID ObjectContext,
                       PVOID CleanupContext);
VOID WINAPI CloseThreadpoolCleanupGroup(PTP_CLEANUP_GROUP ptpcg);

O próximo passo é associar o grupo de limpeza ao ambiente de chamada de retorno com o qual deve ser usado. A função SetThreadpoolCallbackCleanupGroup da Figura 3 configura essa associação.

O primeiro parâmetro para a função é um ponteiro para o ambiente de chamada de retorno. O segundo parâmetro é o PTP_CLEANUP_GROUP e o terceiro parâmetro é um ponteiro para uma função de chamada de retorno que será solicitada quando a função CloseThreadpoolCleanupGroupMembers for chamada.

Quando um aplicativo tiver terminado de usar os objetos de chamada de retorno do pool de segmentos rastreados pelo grupo de limpeza, tudo o que tem a fazer é chamar CloseThreadpoolGroupMembers, que será bloqueado até que todos os processamentos de funções de chamadas de retorno sejam concluídos. Se fCancelPendingCallbacks for TRUE, as chamadas de retorno enfileiradas para o pool de segmentos ainda não iniciadas também serão canceladas. Se fCancelPendingCallbacks for FALSE, CloseThreadpoolCleanupGroupMembers não retornará até que todas as funções de chamadas de retorno pendentes sejam enviadas para um segmento de trabalho e completem o processamento. O parâmetro pvCleanupContext é usado para transmitir um dado de aplicativo para a função de cancelamento opcional de limpeza da chamada de retorno especificada na chamada SetThreadpoolCallbackCleanupGroup. A função de cancelamento de chamada de retorno será solicitada uma vez para cada objeto de chamada de retorno de pool de segmento que estiver sendo limpo. A assinatura de cancelamento da chamada de retorno deve corresponder à função CleanupGroupCancelCallback exibida na Figura 3. O primeiro parâmetro, ObjectContext, é o dado opcional fornecido à função de criação do objeto de chamada de retorno do pool de segmentos que estiver sendo limpo. O segundo parâmetro, CleanupContext, é o dado opcional fornecido pela chamada para a função CloseThreadpoolGroupMembers. Depois de chamar CloseThreadpoolCleanupGroupMembers, a função CloseThreadpoolCleanupGroup é usada para fechar o grupo de limpeza e liberar os recursos associados a ele. É importante que CloseThreadpoolCleanupGroup não seja solicitado enquanto o grupo de limpeza tiver membros porque pode causar perda de recursos.

APIs de Instância de Chamada de Retorno

O conjunto final de APIs para exame é exibido na Figura 9. Essas APIs devem ser usadas a partir do segmento de trabalho que estiver executando a função de chamada de retorno do objeto de chamada de retorno, de forma que eles solicitem que o PTP_CALLBACK_INSTANCE seja transmitido para a função de chamada de retorno. A primeira função, CallbackMayRunLong, é usada para informar o pool de segmentos que a função de chamada de retorno deseja para um período maior de tempo. O pool de segmentos rastreia o número de segmentos de trabalho que estiverem executando chamadas de retorno de longa duração. Relembrando, usamos a função SetThreadpoolCallbackRunsLong para informar ao pool de segmentos que as funções de chamada de retorno associadas ao ambiente de chamada de retorno fornecido são todas de longa duração.

Figure 9 APIs de Instância de Chamada de Retorno (Close Figure 9)

BOOL WINAPI CallbackMayRunLong(
                PTP_CALLBACK_INSTANCE Instance);
VOID WINAPI DisassociateCurrentThreadFromCallback(
                PTP_CALLBACK_INSTANCE Instance);
VOID WINAPI SetEventWhenCallbackReturns(
                PTP_CALLBACK_INSTANCE Instance,
                HANDLE evt);
VOID WINAPI ReleaseSemaphoreWhenCallbackReturns(
                PTP_CALLBACK_INSTANCE Instance,
                HANDLE sem,
                DWORD crel);
VOID WINAPI LeaveCriticalSectionWhenCallbackReturns(
                PTP_CALLBACK_INSTANCE Instance,
                PCRITICAL_SECTION pcs);
VOID WINAPI ReleaseMutexWhenCallbackReturns(
                PTP_CALLBACK_INSTANCE Instance,
                HANDLE mut);
VOID WINAPI FreeLibraryWhenCallbackReturns(
                PTP_CALLBACK_INSTANCE Instance,
                HMODULE mod);

Um resultado TRUE do CallbackMayRunLong indica que o pool de segmentos possui segmentos de trabalho disponíveis para processar chamadas de retorno de longa duração. Considerando a disponibilidade no segmento de trabalho, o pool de segmentos só considera o atual conjunto de segmentos de trabalho existente no momento em que a função é solicitada. Um retorno de código FALSE indica que todos os segmentos de trabalho disponíveis já estão ocupados executando chamadas de retorno de longa duração. Nesse caso, a função de chamada de retorno deve voltar o mais rápido possível e postergar o trabalho de longa duração, se quiser manter um segmento de trabalho disponível para executar chamadas de retorno de curta duração.

A próxima função da Figura 9, DisassociateCurrentThreadFromCallback, interrompe a associação entre a função de chamada de retorno atualmente em execução e o objeto que iniciou a chamada. O segmento atual não será mais considerado o executor da chamada de retorno em nome do objeto. Por exemplo, se a função de chamada de retorno para um objeto de trabalho chama DisassociateCurrentThreadFromCallback, ela pode chamar depois WaitForThreadpoolWorkCallbacks usando o ponteiro para o objeto de trabalho que passou na função de chamada de retorno sem risco de bloqueio. No entanto, DisassociateCurrentThreadFromCallback retém a associação da chamada de retorno em execução com o grupo de limpeza dos ambientes de chamada de retorno, de forma que, se um novo segmento tiver solicitado CloseThreadpoolCleanupGroupMembers, a função vai esperar pelo segmento executando a função de chamada de retorno para o pool de segmentos. Isso garante que as DLLs não serão descarregadas enquanto ainda existirem segmentos executando códigos. Um aspecto importante a observar é que, se você chamar DisassociateCurrentThreadFromCallback e tentar reutilizar o objeto de dentro da função de chamada de retorno (digamos, chamar uma função como SubmitThreadpoolWork para um objeto de trabalho), isso deve ser feito em sincronia com todas as chamadas para CloseThreadPoolCleanupGroupMembers porque tentar reutilizar o objeto quando o CloseThreadPoolCleanupGroupMembers tiver iniciado o processamento pode fazer com que uma exceção seja lançada dentro da função de chamada de retorno.

O próximo conjunto de funções coordena a conclusão do processamento de uma função de chamada de retorno com um objeto de sincronização. A função SetEventWhenCallbackReturns é usada para colocar um objeto de evento dentro do estado sinalizado quando a chamada de retorno atual é concluída. Da mesma forma, as funções ReleaseSemaphoreWhenCallbackReturns, ReleaseMutexWhenCallbackReturns e LeaveCriticalSectionWhenCallbackReturns são desenvolvidas para diferentes tipos de objetos de bloqueio quando a função atual de chamada de retorno é concluída. Essas funções podem ajudar a reduzir erros de programação, pois garantem a liberação do bloqueio específico, independente da forma como volta a função de chamada de retorno. Esperamos que um futuro lançamento do Windows traga novas funções para suportar Slim Reader/Writer Locks.

A função FreeLibraryWhenCallbackReturns pode ser usada para que o pool de segmentos chame FreeLibrary no suporte do módulo transmitido quando a instância específica da chamada de retorno completar o processamento. É responsabilidade do aplicativo garantir que todas as chamadas de retorno pendentes sejam completadas e que aquelas que estiverem na fila do pool de segmentos sejam canceladas antes da volta da função das chamadas de retorno.

Anteriormente, a função SetThreadpoolCallbackLibrary era descrita como uma maneira de prevenir a descarga prematura de uma DLL enquanto uma função de chamada de retorno estivesse executando um código dentro da DLL. O custo dessa segurança é realmente alto por causa da sobrecarga envolvida na obtenção e na liberação do bloqueio do carregador antes e depois da chamada da função de chamada de retorno. Observe também que existe um único bloqueio de carregador no processo e deve haver uma grande contenção nele, o que significa que confiar no mecanismo do pool de segmentos para não descarregar a DLL prematuramente pode causar um impacto negativo no desempenho do aplicativo. Dependendo do estado do aplicativo, pode ser melhor desenvolver um mecanismo específico para ele que forneça um descarregamento seguro da DLL usando uma combinação das funções descritas nesta seção com os primitivos de sincronização do Windows.

Faça um Teste com os Aplicativos de Amostra

Como você pôde ver, um novo componente do pool de segmentos inclui vários aprimoramentos para ajudar você a escrever facilmente aplicativos que sejam confiáveis e escaláveis ao mesmo tempo. O mais importante entre essas melhorias é a habilidade de hospedar mais de um pool de segmento por processo, no qual cada pool de segmento possui suas próprias características, de forma que você possa dividir os processos por tipo de trabalho realizado.

Espero que essa visão geral do novo pool de segmentos tenha aguçado seu interesse em explorar como seu uso pode beneficiar seus aplicativos. Para começar, inclui dois aplicativos de amostra neste artigo. O primeiro, ThreadPoolDemo, permite que você experimente objetos de trabalho, espera e timer, e veja como funcionam. O ThreadPoolDemo será executado tanto em um pool de segmentos no processo padrão quanto em um personalizado, permitindo especificar a contagem mínima e máxima de segmentos pelos parâmetros de linha.

A demonstração do objeto de trabalho, executado pela especificação da opção da linha de comando de trabalho, cria um objeto de trabalho. Usando a contagem fornecida com o parâmetro de linha de comando -I, o item de trabalho será enviado para o pool de segmentos tantas vezes quantas forem especificadas. Você pode usar a opção -B para definir a extensão do bloqueio da função de chamada de retorno, e a opção -E para especificar quanto tempo a função de chamada de retorno deve executar. Por padrão, o programa executará até que todos os itens de trabalho enviados tenham sido executados. Especificar tanto o –CC quanto o –CW na linha de comando faz com que o aplicativo imediatamente chame CloseThreadpoolCleanupGroupMembers depois de enviar o item de trabalho o número de vezes especificado. A diferença entre as duas opções é que a –CC cancela todas as chamadas de retorno que ainda não iniciaram, enquanto a –CW espera por elas para executar antes do retorno da chamada CloseThreadpoolCleanupGroupMembers.

Você também pode experimentar com objetos de timer. Você pode configurar múltiplos objetos de tempo, em que cada um tenha seu próprio tempo percorrido, além de um período e uma janela opcionais. Usar múltiplos timers com diferentes tamanhos de janela permite que você veja o efeito do sistema unindo encerramentos do timer.

Finalmente, a demonstração do objeto de espera permite que você defina um ou mais eventos específicos para que o pool de segmentos aguarde. Cada evento específico pode incluir um tempo decorrido opcional para indicar quando o evento deve ser sinalizado, e também um intervalo de tempo limite opcional para definir quando a espera deve terminar. Essas opções permitem que você experimente combinações de eventos sinalizados, com tempo limite de espera, ou então que nunca se tornem sinalizados e esgotem o tempo limite.

Para detalhes de parâmetros em linhas de comando para cada tipo de objeto de chamada de retorno, digite um dos seguintes comandos em um prompt: "ThreadPoolDemo –Work ?", "ThreadPoolDemo –Timer ?", ou "ThreadPoolDemo –Wait ?".

O segundo aplicativo de amostra incluído neste artigo é chamado CopyFile. Essencialmente, essa é uma atualização do exemplo da cópia do arquivo Windows SDK que demonstra como uma porta de conclusão trabalha. O CopyFile copiará um arquivo de origem para um de destino usando o pool de segmentos e demonstrando como os Objetos de Conclusão E/S atuam. Digitar "CopyFile –Usage" na linha de comando exibe uma descrição completa dos parâmetros do programa na janela do console.

Gostaria de agradecer a Rob Earhart, Eric Li e Sandeep Ranade por responder às minhas perguntas e fornecer informações valiosas. Também gostaria de agradecer a Rob Shewan por revisar o conteúdo e me dar respostas de valor.

Robert Saccone é o principal arquiteto no grupo de Forefront Server Security. Ele se interessa por design de software em larga escala, sistemas distribuídos e implementações de sistemas operacionais. Você pode contatá-lo pelo endereço robsaccone@hotmail.com.

© 2007 Microsoft Corporation e CMP Media, LLC. Todos os direitos reservados. A reprodução parcial ou completa sem autorização é proibida..


SEGMENTOS DO WPF: Crie aplicativos que respondem melhor com o Dispatcher

SEGMENTOS DO WPF

Crie aplicativos que respondem melhor com o Dispatcher

Shawn Wildermuth
Este artigo aborda:
  • Threading no WPF
  • Usando o Dispatcher
  • Tratamento de segmento sem interface do usuário
  • Usando temporizadores
Este artigo utiliza as seguintes tecnologias:
.NET Framework 3.0, Windows Presentation Foundation
Conteúdo
O modelo de threading
DispatcherObject
Usando o Dispatcher
BackgroundWorker
DispatcherTimer

S
eria uma vergonha se você passasse meses da sua vida criando uma interface intuitiva, natural e bonita para que os usuários ficassem batucando em suas mesas enquanto aguardassem ela responder. Ver o aplicativo ser interrompido por conta de um processo demorado é bastante doloroso. No entanto, criar aplicativos que sejam receptivos exige um planejamento cuidadoso, que normalmente envolve fazer com que processos demorados funcionem em outros segmentos para que o segmento da interface do usuário fique livre e acompanhe o usuário.

A minha primeira experiência real com a receptividade remete ao Visual C++®, ao MFC e à primeira grade que escrevi. Eu estava ajudando a escrever um aplicativo para farmácia que precisava mostrar todos os medicamentos em um formulário complexo. O problema era que havia 30 mil medicamentos, e eu optei por garantir a aparência de receptividade preenchendo a primeira tela de medicamentos no segmento da interface do usuário (em cerca de 50 milissegundos) e usando um segmento em segundo plano para terminar o preenchimento dos medicamentos que não estavam visíveis (em aproximadamente 10 segundos). O projeto deu certo e eu aprendi a importante lição de que a percepção do usuário pode ser mais importante do que a realidade.

O WPF (Windows® Presentation Foundation) é uma excelente tecnologia para criar interfaces do usuário convincentes, mas isso não significa que você não tenha que levar em conta a receptividade do aplicativo. O que acontece é o seguinte: independentemente do tipo dos processos demorados envolvidos – obter muitos resultados de um banco de dados, fazer chamadas de Web service assíncronas ou qualquer outro número de operações potencialmente intensivas –, tornar o aplicativo mais receptivo é uma garantia de que os usuários ficarão mais felizes ao longo do processo. Mas, antes de começar a usar um modelo de programação assíncrono no aplicativo WPF, é importante que você compreenda o modelo de threading do WPF. Neste artigo, eu não apenas apresentarei a você o modelo de threading, mas também mostrarei como funcionam os objetos com base em Dispatcher e explicarei como usar o BackgroundWorker de forma que seja possível criar interfaces do usuário convincentes e receptivas.

O modelo de threading

Todos os aplicativos WPF começam com dois segmentos importantes, um para processar e outro para gerenciar a interface do usuário. Como o segmento de processamento é um segmento oculto executado em segundo plano, o único segmento com o qual você lida diretamente é o da interface do usuário. O WPF exige que a maioria de seus objetos esteja vinculada ao segmento da interface do usuário. Isso é conhecido como afinidade de segmento, o que significa que só é possível usar um objeto WPF no segmento em que ele foi criado. Usá-lo em outros segmentos causará o lançamento de uma exceção de runtime. Observe que o modelo de threading do WPF interopera bem com APIs baseadas no Win32®. Isso significa que o WPF pode hospedar ou ser hospedado por qualquer API baseada no HWND (Windows Forms, Visual Basic®, MFC ou até mesmo Win32).

A afinidade de segmento é tratada pela classe Dispatcher, um loop de mensagem priorizado de aplicativos WPF. Normalmente, os projetos WPF têm um único objeto Dispatcher (e, por isso, um único segmento de interface do usuário) por meio do qual todo o trabalho da interface é canalizado.

Diferentemente dos loops de mensagem típicos, cada item de trabalho enviado para o WPF é enviado pelo Dispatcher com uma determinada prioridade. Isso possibilita a organização dos itens por prioridade e o adiamento de determinados tipos de trabalho até que o sistema tenha tempo para tratá-los. (Por exemplo, alguns itens de trabalho podem ser adiados até que o sistema ou o aplicativo esteja ocioso.) O suporte à priorização de itens possibilita que o WPF permita que determinados tipos de trabalho tenham mais acesso e, dessa forma, mais tempo em um segmento do que outro trabalho.

Posteriormente, neste artigo, demonstrarei que o mecanismo de processamento tem permissão para atualizar a interface do usuário com uma prioridade muito maior do que o sistema de entrada. Isso significa que as animações continuarão atualizando a interface do usuário, independentemente do que o usuário está fazendo com o mouse, o teclado ou o sistema de tinta. Isso pode fazer com que a interface do usuário pareça ser mais receptiva. Por exemplo, vamos supor que você estivesse escrevendo um aplicativo para tocar músicas (como o Windows Media® Player). Você certamente gostaria que as informações sobre a música em execução (inclusive a barra de progresso e as demais informações) fossem exibidas, independentemente do usuário estar ou não usando a interface. Para o usuário, isso pode fazer com que a interface pareça ser mais receptiva àquilo no qual ele está mais interessado (ouvir música, no caso).

Além de usar o loop de mensagem do Dispatcher para canalizar os itens de trabalho por meio do segmento da interface do usuário, todos os objetos do WPF reconhecem o Dispatcher responsável por isso (e, assim, o segmento da interface do usuário mantido). Isso significa que haverá falha em todas as tentativas de atualização dos objetos do WPF a partir dos segmentos secundários. Isso é de responsabilidade da classe DispatcherObject.

DispatcherObject

Na hierarquia de classes do WPF, grande parte delas deriva de maneira centralizada da classe DispatcherObject (por meio de outras classes). Como mostra a Figura 1, é possível ver que a classe virtual DispatcherObject está espremida um pouco abaixo de Object na hierarquia da maioria das classes do WPF.

Figura 1 Derivação de DispatcherObject
Figura 1 Derivação de DispatcherObject

A classe DispatcherObject tem duas tarefas principais: fornecer acesso ao Dispatcher atual ao qual há um objeto vinculado e oferecer métodos para garantir (CheckAccess) e verificar (VerifyAccess) se um segmento tem acesso ao objeto (derivado de DispatcherObject). A diferença entre CheckAccess e VerifyAccess é que CheckAccess retorna um valor Booleano representando se o segmento atual pode usar o objeto e VerifyAccess lança uma exceção caso o segmento não tenha acesso ao objeto. Fornecendo essa funcionalidade básica, todos os objetos do WPF dão suporte à determinação se eles podem ser usados em um determinado segmento – mais especificamente, o segmento de interface do usuário. Caso você esteja escrevendo seus próprios objetos do WPF como, por exemplo, controles, todos os métodos usados devem chamar VerifyAccess antes de realizar qualquer trabalho. Isso garante que os objetos só sejam usados no segmento de interface do usuário, como mostra a Figura 2.

Figure 2 Usando VerifyAccess e CheckAccess (Close Figure 2)
public class MyWpfObject : DispatcherObject
{
    public void DoSomething()
    {
        VerifyAccess();

        // Do some work
    }

    public void DoSomethingElse()
    {
        if (CheckAccess())
        {
            // Something, only if called
            // on the right thread
        }
    }
}

Com isso em mente, tome o cuidado de estar no segmento de interface do usuário ao chamar qualquer objeto derivado de DispatcherObject como, por exemplo, Control, Window, Panel etc. Se você fizer uma chamada para um DispatcherObject em um segmento sem interface do usuário, ele lançará uma exceção. Na verdade, se estiver trabalhando em um segmento sem interface do usuário, você precisará usar o Dispatcher para atualizar DispatcherObjects.

Usando o Dispatcher

A classe Dispatcher proporciona um gateway para o bombardeamento de mensagens no WPF e oferece um mecanismo para orientar o trabalho de processamento pelo segmento de interface do usuário. Isso é necessário para atender às demandas de afinidade de segmento, mas como o segmento de interface do usuário está bloqueado para todas as partes do trabalho que passa por Dispatcher, é importante manter o trabalho feito pelo Dispatcher pequeno e rápido. É melhor dividir grandes partes do trabalho da interface do usuário em pequenos blocos discretos a serem executados por Dispatcher. Qualquer trabalho que não precise ser feito no segmento de interface do usuário deve ser movido para outros segmentos para seja processado em segundo plano.

Você normalmente usará a classe Dispatcher para enviar itens de trabalho a serem processados pelo segmento de interface do usuário. Por exemplo, se quisesse fazer algum trabalho em um segmento separado usando a classe Thread, você poderia criar um delegado ThreadStart com algum trabalho a ser feito em um novo segmento, como mostra a Figura 3.

Figure 3 Atualizando a interface do usuário com o segmento sem interface do usuário – a maneira errada (Close Figure 3)
// The Work to perform on another thread
ThreadStart start = delegate()
{
    // ...

    // This will throw an exception
    // (it's on the wrong thread)
    statusText.Text = "From Other Thread";
};

// Create the thread and kick it started!
new Thread(start).Start();

Esse código falha porque a configuração da propriedade Text do controle statusText (TextBlock) não está sendo chamada no segmento de interface do usuário. Quando o código tenta definir Text em TextBlock, a classe TextBlock chama internamente seu método VerifyAccess para garantir que a chamada esteja vindo do segmento de interface do usuário. Ao determinar que a chamada está vindo de outro segmento, ela lança uma exceção. E como é possível usar Dispatcher para fazer a chamada no segmento da interface do usuário?

A classe Dispatcher oferece acesso à invocação de código diretamente no segmento da interface do usuário. A Figura 4 mostra o uso do método Invoke de Dispatcher para chamar um método SetStatus e alterar a propriedade Text de TextBlock para você.

Figure 4 Atualizando a interface do usuário (Close Figure 4)
// The Work to perform on another thread
ThreadStart start = delegate()
{
  // ...

  // Sets the Text on a TextBlock Control.
  // This will work as its using the dispatcher
  Dispatcher.Invoke(DispatcherPriority.Normal,
                    new Action<string>(SetStatus),
                    "From Other Thread");
};
// Create the thread and kick it started!
new Thread(start).Start();

A chamada Invoke usa três informações: a prioridade do item a ser executado, um delegado que descreve o trabalho a ser realizado e todos os parâmetros a serem passados para o delegado descrito no segundo parâmetro. Ao chamar Invoke, ela coloca na fila o delegado a ser chamado no segmento de interface do usuário. Usar o método Invoke garante que você está prestes a bloquear até que o trabalho seja realizado no segmento de interface do usuário.

Como alternativa ao uso de Dispatcher de maneira síncrona, é possível usar o método BeginInvoke de Dispatcher para colocar na fila um item de trabalho do segmento de interface de usuário de maneira assíncrona. Chamar o método BeginInvoke retorna uma instância da classe DispatcherOperation que contém informações sobre a execução do item de trabalho, inclusive o status atual do item e o resultado da execução (assim que o item de trabalho for concluído). O uso do método BeginInvoke e da classe DispatcherOperation é mostrado na Figura 5.

Figure 5 Atualizando a interface do usuário de maneira assíncrona (Close Figure 5)
// The Work to perform on another thread
ThreadStart start = delegate()
{
    // ...

    // This will work as its using the dispatcher
    DispatcherOperation op = Dispatcher.BeginInvoke(
        DispatcherPriority.Normal,
        new Action<string>(SetStatus),
        "From Other Thread (Async)");

    DispatcherOperationStatus status = op.Status;
    while (status != DispatcherOperationStatus.Completed)
    {
        status = op.Wait(TimeSpan.FromMilliseconds(1000));
        if (status == DispatcherOperationStatus.Aborted)
        {
            // Alert Someone
        }
    }
};

// Create the thread and kick it started!
new Thread(start).Start();

Diferentemente de uma implementação típica do bombardeamento de mensagens, Dispatcher é uma fila dos itens de trabalho com base na prioridade. Isso possibilita maior receptividade porque as partes mais importantes do trabalho são executadas antes das menos importantes. A natureza da priorização é exemplificada pelas prioridades especificadas na enumeração DispatchPriority (como mostra a Figura 6).

Figure 6 Níveis de priorização de DispatchPriority (em ordem de prioridade) (Close Figure 6)

Prioridade | Descrição
Inactive | Os itens de trabalho são colocados na fila, mas não são processados.
SystemIdle | Os itens de trabalho só são expedidos para o segmento de interface do usuário quando o sistema está ocioso. Essa é a menor prioridade dos itens efetivamente processados.
ApplicationIdle | Os itens de trabalho só são expedidos para o segmento de interface do usuário quando o próprio aplicativo está ocioso.
ContextIdle | Os itens de trabalho só são expedidos para o segmento de interface do usuário depois que os itens de trabalho de maior prioridade são processados.
Background | Os itens de trabalho são expedidos depois que os itens de layout, processamento e entrada são processados.
Input | Os itens de trabalho são expedidos para o segmento de interface do usuário com a mesma prioridade da entrada do usuário.
Loaded | Os itens de trabalho são expedidos para o segmento de interface do usuário depois que todo o layout e o processamento estiver concluído.
Render | Os itens de trabalho são expedidos para o segmento de interface do usuário com a mesma prioridade do mecanismo de processamento.
DataBind | Os itens de trabalho são expedidos para o segmento de interface do usuário com a mesma prioridade da ligação de dados.
Normal | Os itens de trabalho são expedidos para o segmento de interface do usuário com prioridade normal. Trata-se da prioridade com a qual a maioria dos itens de trabalho do aplicativo deve ser expedida.
Send | Os itens de trabalho são expedidos para o segmento de interface do usuário com a maior prioridade.

Normalmente, você deve sempre usar DispatcherPriority.Normal para a prioridade dos itens de trabalho que atualizam a aparência da interface do usuário (como o exemplo que usei anteriormente). Mas há situações em que você deve usar prioridades diferentes. As mais interessantes são as três prioridades de ociosidade (ContextIdle, ApplicationIdle e SystemIdle). Elas permitem que você especifique os itens de trabalho que só são executados em cargas de trabalho muito baixas.

BackgroundWorker

Agora que já tem uma idéia de como o Dispatcher funciona, você deve se surpreender ao saber que não encontrará uso para ele na maioria dos casos. No Windows Forms 2.0, a Microsoft introduziu uma classe para o tratamento de segmento sem interface do usuário a fim de simplificar o modelo de desenvolvimento para os desenvolvedores de interface do usuário. Essa classe é chamada de BackgroundWorker. A Figura 7 mostra o uso típico da classe BackgroundWorker.

Figure 7 Usando BackgroundWorker no WPF (Close Figure 7)
BackgroundWorker _backgroundWorker = new BackgroundWorker();

...

// Set up the Background Worker Events
_backgroundWorker.DoWork += _backgroundWorker_DoWork;
backgroundWorker.RunWorkerCompleted +=
    _backgroundWorker_RunWorkerCompleted;

// Run the Background Worker
_backgroundWorker.RunWorkerAsync(5000);

...

// Worker Method
void _backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
    // Do something
}

// Completed Method
void _backgroundWorker_RunWorkerCompleted(
    object sender,
    RunWorkerCompletedEventArgs e)
{
    if (e.Cancelled)
    {
        statusText.Text = "Cancelled";
    }
    else if (e.Error != null)
    {
        statusText.Text = "Exception Thrown";
    }
    else
    {
        statusText.Text = "Completed";
    }
}

O componente BackgroundWorker funciona bem com o WPF porque, na verdade, ele usa a classe AsyncOperationManager que, por sua vez, usa a classe SynchronizationContext para lidar com a sincronização. No Windows Forms, AsyncOperationManager oferece uma classe WindowsFormsSynchronizationContext que deriva da classe SynchronizationContext. Da mesma forma, no ASP.NET, ela funciona com uma derivação diferente de SynchronizationContext chamada AspNetSynchronizationContext. Essas classes derivadas de SynchronizationContext sabem como tratar a sincronização entre segmentos da invocação do método.

No WPF, esse modelo é estendido com uma classe DispatcherSynchronizationContext. Usando BackgroundWorker, Dispatcher é empregado automaticamente para invocar chamadas de método entre segmentos. A boa notícia é que, como você já deve estar familiarizado com esse padrão comum, é possível continuar usando BackgroundWorker em novos projetos do WPF.

DispatcherTimer
Recursos sobre threading do WPF
  • Laboratórios virtuais do Windows Presentation Foundation
  • WPF no .NET Framework Developer Center
  • Fundamentos do WPF: modelo de threading, biblioteca MSDN
  • Programming the Windows Presentation Foundation (Programando o Windows Presentation Foundation) de Chris Sells e Ian Griffiths (O'Reilly, 2005)

A execução periódica de códigos é uma tarefa comum no desenvolvimento dentro do Microsoft® .NET Framework, mas usar temporizadores no .NET tem sido, no mínimo, confuso. Se procurar uma classe Timer na BCL (Base Class Library) do .NET Framework, você não encontrará menos do que três classes Timer: System.Threading.Timer, System.Timers.Timer e System.Windows.Forms.Timer. Cada um desses temporizadores é diferente. O artigo de Alex Calvo na MSDN Magazine explica quando usar cada uma dessas classes Timer (consulte msdn.microsoft.com/msdnmag/issues/04/02/TimersinNET).

Para aplicativos do WPF, há um novo tipo de temporizador que usa Dispatcher – a classe DispatcherTimer. Assim como os demais temporizadores, a classe DispatcherTimer dá suporte à especificação de um intervalo entre marcações, bem como ao código a ser executado quando o evento do temporizador for ativado. É possível ver um uso simplificado de DispatcherTimer na Figura 8.

Figure 8 Classe DispatcherTimer em ação (Close Figure 8)
// Create a Timer with a Normal Priority
_timer = new DispatcherTimer();

// Set the Interval to 2 seconds
_timer.Interval = TimeSpan.FromMilliseconds(2000);

// Set the callback to just show the time ticking away
// NOTE: We are using a control so this has to run on
// the UI thread
_timer.Tick += new EventHandler(delegate(object s, EventArgs a)
{
    statusText.Text = string.Format(
        "Timer Ticked:  {0}ms", Environment.TickCount);
});

// Start the timer
_timer.Start();

Como a classe DispatcherTimer está vinculada a Dispatcher, também é possível especificar a DispatcherPriority, bem como qual Dispatcher usar. A classe DispatcherTimer usa a prioridade Normal como sendo o Dispatcher atual por padrão, mas é possível substituir esses valores:

_timer = new DispatcherTimer(
    DispatcherPriority.SystemIdle, form1.Dispatcher);

Todo o esforço envolvido no planejamento dos processos de trabalho é válido para possibilitar aplicativos mais receptivos. Fazer uma pesquisa inicial pode tornar esse planejamento ainda mais bem-sucedido. Sugiro que você explore alguns dos sites que eu mencionei na barra lateral "Referências sobre o threading de WPF" antes de começar – com este artigo, eles devem dar a você uma boa base para o desenvolvimento de aplicativos receptivos.

Shawn Wildermuth é um Microsoft MVP, MCSD.NET, MCT e fundador da Wildermuth Consulting Services. Shawn também é autor de Pragmatic ADO.NET (ADO.NET Pragmático; Addison-Wesley, 2002) e co-autor de quatro kits de treinamento de certificação Microsoft, assim como das futuras arquiteturas de dados prescritivas. Ele pode ser contatado por meio de seu site em wildermuthconsulting.com.


LINQ paralelo: Como executar consultas em processadores de v&#225;rios n&#250;cleos

LINQ paralelo

Como executar consultas em processadores de vários núcleos

Joe Duffy and Ed Essey
Este artigo baseia-se na biblioteca Parallel FX, atualmente em desenvolvimento. Todas as informações contidas neste documento estão sujeitas a alterações.
Este artigo aborda:
  • Fundamentos básicos do PLINQ
  • O modelo de programação do PLINQ
  • Exceções concorrentes
  • Classificação da saída
Este artigo utiliza as seguintes tecnologias:
Biblioteca Parallel FX
Conteúdo
De LINQ para PLINQ
O modelo de programação do PLINQ
Processando a saída de consultas
Exceções concorrentes
Ordenando os resultados da saída
Efeitos colaterais
Colocando o PLINQ em funcionamento

O
s processadores de vários núcleos chegaram. Antigamente predominantes principalmente em servidores e em desktops, os processadores de vários núcleos começam a ser usados em telefones móveis e em PDAs, resultando em grandes vantagens em consumo de energia. Respondendo à disponibilidade cada vez maior de plataformas com multiprocessadores, o PLINQ (Parallel Language Integrated Query) oferece uma forma fácil de aproveitamento de hardware paralelo, incluindo os computadores multiprocessadores tradicionais e a nova onda de processadores de vários núcleos.

PLINQ é um mecanismo de execução de consultas que aceita qualquer consulta LINQ-to-Objects ou LINQ-to-XML e utiliza vários processadores ou núcleos de forma automática para execução quando eles estiverem disponíveis. A mudança do modelo de programação é pequena, o que significa que você não precisa ser um mestre em concorrência para usá-lo. Na verdade, segmentos e bloqueios nem mesmo serão produzidos, a menos que você realmente queira mergulhar nos bastidores para entender como tudo funciona. PLINQ é um componente chave do Parallel FX, a próxima geração do suporte à concorrência no Microsoft® .NET Framework.

A utilização de tecnologias como PLINQ será cada vez mais crucial para garantir a escalabilidade do software em futuras arquiteturas de microprocessadores paralelos. Hoje, ao utilizar o LINQ em determinados locais do seu aplicativo £ como onde haja operações com muitos dados ou processamento que podem ser expressas como consultas £, você garantirá que esses fragmentos dos seus programas terão um desempenho melhor quando o PLINQ for disponibilizado e quando as máquinas que executam o seu aplicativo aumentarem de 2 a 4 processadores para 32 ou mais. E mesmo se você só executar esse código em uma máquina com um único processador, a sobrecarga do PLINQ normalmente será tão pequena que a diferença será imperceptível. Além disso, a natureza paralela dos dados do PLINQ garante que os seus programas continuarão a ser redimensionados à medida que os seus conjuntos de dados aumentarem.

Neste artigo, revisaremos os objetivos da tecnologia PLINQ, onde ela se encaixa no .NET Framework e em outras opções de concorrência e qual será a sua aparência a partir da perspectiva dos desenvolvedores de LINQ. Concluiremos com alguns cenários de exemplo em que o PLINQ já mostrou ser tremendamente valioso.

Observe que a biblioteca Parallel FX, que inclui o PLINQ, ainda está em desenvolvimento, mas o primeiro CTP (Community Tech Preview) será disponibilizado no MSDN® no outono de 2007. Assista a blogs.msdn.com/somasegar para obter detalhes.

De LINQ para PLINQ

Quando as pessoas ouvem falar do projeto PLINQ pela primeira vez, normalmente perguntam: por que devemos criar um LINQ paralelo? A resposta simples é que o modelo de programação do LINQ, definido em uma ocasião para a expressão de cálculos, enfatiza a especificação do que precisa ser feito em vez de como isso deve feito. Sem o LINQ, a parte do como normalmente seria expressa por meio de loops e estruturas de dados intermediárias, mas ao codificar tantas informações específicas, o compilador e o runtime não podem ser executados em paralelo com tanta facilidade assim. A natureza declarativa do LINQ, por outro lado, deixa a flexibilidade para uma implementação mais inteligente, como a do LINQ, de utilização do processamento em paralelo para a obtenção dos mesmos resultados.

A próxima pergunta é inevitável: se você for consultar dados suficientes para que o PLINQ faça sentido, por que não usar um banco de dados? E a resposta tem a forma de uma pergunta retórica: qual seria o código que você escreveria se não estivesse usando LINQ? Bem, é provável que você tivesse o mesmo trabalho cheio de cálculos e dados, mas ele seria capturado em uma série não estruturada de loops for, chamadas de função e assim por diante. Dessa forma, essa pergunta significa que todos os programas devem residir no banco de dados, mas tenho certeza de que muito poucas pessoas concordariam com isso.

Com o PLINQ, você não precisa mover toda a lógica de processamento do servidor de banco de dados para as consultas LINQ-to-Objects em memória do cliente. Em vez disso, o PLINQ oferece uma forma incremental de aproveitarmos o paralelismo em soluções existentes para problemas também existentes. Se o seu problema envolve muitos dados e se tradicionalmente demandasse uma solução de banco de dados, ainda será melhor que você adote essa solução. O PLINQ muda muito pouco essa situação. No entanto, se você tiver várias fontes de dados que gostaria de consultar juntas £ possivelmente bancos de dados heterogêneos e globais, arquivos XML, entre outros £, o PLINQ poderá assumir e executar em paralelo assim que os dados estiverem no cliente.

Naturalmente, as pessoas perguntariam: por que a própria LINQ-to-Objects simplesmente não executa consultas em paralelo? O paralelismo seria completamente oculto e os desenvolvedores não precisariam alterar uma única linha de código para obter os benefícios. Infelizmente, existem algumas armadilhas sutis que podem surgir na mudança do LINQ para PLINQ que nos proíbem de concretizar essa visão, pelo menos no PLINQ 1.0. Elas serão discutidas mais adiante no artigo.

O modelo de programação do PLINQ

A utilização do PLINQ é quase que idêntica à utilização do LINQ-to-Objects e do LINQ-to-XML. Você pode usar qualquer um dos operadores disponíveis por meio do C# 3.0 e da sintaxe de compreensão de consulta do Visual Basic® 9.0 ou da classe System.Linq.Enumerable, incluindo OrderBy, Join, Select, Where e assim por diante. O PLINQ oferece suporte a todos os operadores do LINQ e não somente àqueles disponíveis em compreensões de linguagem. E você pode consultar qualquer coleção em memória, como T[], List<T>, ou qualquer outro tipo de IEnumerable<T>, além de documentos XML carregados por meio das APIs de System.Xml.Linq. Se você já possui um monte de consultas LINQ, o PLINQ está preparado para executá-las.

As consultas LINQ-to-SQL e LINQ-to-Entities ainda serão executadas pelos respectivos bancos de dados e provedores de consultas e, portanto, o PLINQ não oferece uma forma de executar essas consultas em paralelo. Se você quiser processar os resultados dessas consultas em memória, incluindo a junção da saída de várias consultas heterogêneas, então o PLINQ será bastante útil.

Além de escrever consultas LINQ da mesma forma que você costuma escrevê-las, existem duas etapas extras na utilização do PLINQ:

  1. A referência ao assembly System.Concurrency.dll durante a compilação.
  2. Insira a sua fonte de dados em um IParallelEnumerable<T> com uma chamada ao método de extensão System.Linq.ParallelEnumerable.AsParallel.

A chamada ao método de extensão AsParallel na Etapa 2 garante que o compilador C# ou Visual Basic seja vinculado à versão System.Linq.ParallelEnumerable dos operadores padrão de consulta em vez de System.Linq.Enumerable. Isso oferece ao PLINQ a chance de assumir o controle e de executar a consulta em paralelo. AsParallel é definido pela obtenção de qualquer IEnumerable<T>:

public static class System.Linq.ParallelEnumerable {
    public static IParallelEnumerable<T> AsParallel<T>(
        this IEnumerable<T> source);
    ... the other standard query operators ...
}

IParallelEnumerable<T> deriva de IEnumerable<T> e adiciona muito pouco a ele, já que está ali simplesmente para facilitar a vinculação ao provedor de consultas ParallelEnumerable do PLINQ, aproveitando o suporte ao novo método de extensão do C# 3.0 e do Visual Basic .NET 9.0. A interface deriva de IEnumerable<T> para que você ainda possa usar o foreach em instâncias e passá-las para outras APIs que esperam o IEnumerable<T>. Os operadores de consulta padrão definidos em ParallelEnumerable são iguais aos de Enumerable, com uma única diferença, cada um obtém um IParallelEnumerable<T> como argumento de origem de extensão em vez de IEnumerable<T>, retorna um IParallelEnumerable<T> em vez de IEnumerable<T> (exceto pelas agregações, que só retornam tipos simples) e, é claro, usa o paralelismo internamente para a avaliação da consulta.

Por exemplo, veja uma consulta LINQ simples definida em C#:

IEnumerable<T> data = ...;
var q = data.Where(x => p(x)).Orderby(x => k(x)).Select(x => f(x));
foreach (var e in q) a(e);

Nesse caso, tudo o que é necessário para usar o PLINQ é a adição de uma chamada a AsParallel nos dados:

IEnumerable<T> data = ...;
var q = data.AsParallel().Where(x => p(x)).Orderby(x => k(x)).Select(x => f(x));
foreach (var e in q) a(e);

Isso pode ser escrito de forma mais concisa usando a sintaxe de compreensão de consulta C#, quando a versão PLINQ seria assim:

IEnumerable<T> data = ...;
var q = from x in data.AsParallel() where p(x) orderby k(x) select f(x);
foreach (var e in q) a(e);

Depois de fazer essa mudança, o PLINQ executará, de forma transparente, o Where, OrderBy e Select em todos os processadores disponíveis usando técnicas clássicas de avaliação paralela de dados. O PLINQ utiliza a execução retardada da mesma forma que o LINQ, o que significa que a consulta não será executada até você usar um foreach, chamar GetEnumerator diretamente ou forçar a inclusão dos resultados em uma lista por meio de outra API como ToList ou ToDictionary. Quando a consulta for executada, o PLINQ fará com que suas partes sejam executadas nos processadores disponíveis através do uso oculto de vários segmentos. Você nem precisa entender como tudo isso é feito, bastará comprovar o desempenho melhor e a melhor utilização dos processadores disponíveis.

Embora isso não seja óbvio, o tipo inferido q é diferente das consultas LINQ-to-Objects e PLINQ normais mostradas anteriormente. No primeiro exemplo, q tem o tipo IEnumerable<U>, onde U será qualquer tipo retornado pelo método f passado o operador Select. No segundo exemplo, no entanto, q tem o tipo IParallelEnumerable<U>. Normalmente, isso não importa: se você declarou explicitamente que q terá o tipo IEnumerable<U>, por exemplo, a mudança feita em AsParallel ainda funcionará, já que IParallelEnumerable<U> deriva de IEnumerable<U>. Mas isso significa que qualquer utilização subseqüente de q será tratada como um IParallelEnumerable<U>. Por exemplo, se você consultá-lo na seqüência, PLINQ será escolhido como o provedor de consultas.

Observe que alguns operadores LINQ são binários £ eles recebem IEnumerable<T>s como entrada. Join é um exemplo perfeito desses operadores. Nesses casos, o tipo da fonte de dados mais à esquerda determinará se LINQ ou PLINQ será usado. Dessa forma, você só precisa chamar AsParallel na primeira fonte de dados para que a sua consulta seja executada em paralelo:

IEnumerable<T> leftData = ..., rightData = ...;
var q = from x in leftData.AsParallel()
        join y in rightData on x.a == y.b
        select f(x, y);

Toda essa discussão supõe que você esteja usando métodos de extensão para escrever as suas consultas. Se, por outro lado, você optou por chamar métodos na classe Enumerable de forma direta, terá um pouco mais de trabalho para mudar para PLINQ. Além da chamada a AsParallel, também será preciso fazer referência ao tipo ParallelEnumerable. Por exemplo, imagine que a consulta anterior tenha sido escrita com uma chamada direta a Enumerable:

IEnumerable<T> data = ...;
var q = Enumerable.Select(
            Enumerable.OrderBy(
                Enumerable.Where(data, (x) => p(x)),
            (x) => k(x)),
        (x) => f(x));
foreach (var e in q) a(e);

Para usar PLINQ, a consulta teria de ser reescrita desta forma:

IEnumerable<T> data = ...;
var q = ParallelEnumerable.Select(
            ParallelEnumerable.OrderBy(
                ParallelEnumerable.Where(data.AsParallel(), (x) =>    p(x)),
            (x) => k(x)),
        (x) => f(x));
foreach (var e in q) a(e);

Por motivos óbvios, a utilização de compreensões e de métodos de extensão é uma forma mais conveniente de escrever suas consultas e apresenta o benefício adicional de facilitar sobremaneira a mudança para PLINQ.

E isso é tudo! Essas são só as mudanças necessárias para a utilização do PLINQ no lugar de LINQ-to-Objects. Como LINQ-to-XML exibe documento XML como estruturadas de dados IEnumerable<T>, tudo o que foi dito até aqui também se aplica a conteúdo XML.

Processando a saída de consultas

Como já foi observado, devido à avaliação retardada, o paralelismo não é introduzido até que você inicie o processamento da saída da consulta. Se você já conhece IEnumerable<T>, isso é equivalente a chamar o método GetEnumerator. Existem três formas básicas de processar consultas PLINQ, cada uma levando a um modelo ligeiramente diferente de paralelismo.

A primeira é o processamento em pipeline, quando o segmento que faz a enumeração é separado dos segmentos dedicados à execução da consulta. O PLINQ usará muitos segmentos para a execução da consulta, mas reduzirá o grau de paralelismo por um para que o segmento de enumeração não sofra interferência. Por exemplo, se você tiver oito processadores disponíveis, sete deles executarão a consulta PLINQ, enquanto que o processador restante executará o loop foreach na saída da consulta PLINQ quando os elementos forem disponibilizados. Essa forma ainda possui a vantagem de permitir um processamento mais incremental da saída, reduzindo os requisitos de memória necessários para o armazenamento dos resultados; no entanto, ter muitos segmentos de produção e somente um único segmento de consumo quase sempre pode levar a uma distribuição desigual de trabalho, resultando na ineficiência de processadores.

O segundo modelo é o processamento stop-and-go. Nele, o segmento que inicia a enumeração agrupa todos os outros segmentos para a execução da consulta. Depois que todos os segmentos terminam de produzir o conjunto completo de saídas, o segmento continua a enumeração da saída. Isso traz a vantagem de que todo o poder de processamento passa a ficar dedicado à criação da saída o mais rápido possível. Também é ligeiramente mais eficiente do que o processamento em pipeline porque há menos sobrecarga de sincronização incremental na implementação: o PLINQ pode ser mais inteligente sobre esses aspectos porque sabe exatamente para onde vão os dados da saída e como eles serão acessados. Se a distribuição de trabalho for desigual entre os consumidores e o produtor, normalmente esse será o método de consumo mais apropriado.

Finalmente, existe a enumeração invertida. Uma função lambda é fornecida ao PLINQ que é executado em paralelo, uma vez para cada elemento da saída. Esse é o mecanismo mais eficiente já que haverá muito mais paralelismo exposto ao PLINQ e a implementação evita operações onerosas, como a mesclagem da saída a partir de vários segmentos. Mas existe a desvantagem de que você não pode simplesmente usar um loop foreach, mas deve usar uma API ForAll especial, além de ter cuidado com as funções lambda que não se baseiam no estado compartilhado. Caso contrário, a introdução do paralelismo causará o processamento incorreto de suas consultas e falhas imprevisíveis ou corrupção dos dados. No entanto, se você puder expressar o seu problema em termos dessa API, esse método sempre terá preferência.

Qual desses três modelos você deve usar dependerá do que está sendo feito com os resultados da consulta. O padrão é o primeiro £ o processamento em pipeline. Assim que MoveNext é chamado no enumerador da consulta resultante, um conjunto de segmentos de trabalho adicionais executará a consulta e os resultados serão retornados a partir daí e de todas as chamadas subseqüentes a MoveNext à medida que se tornarem disponíveis. Se houver uma chamada a MoveNext e nenhuma saída estiver pronta a partir dos segmentos do produtor da consulta, o segmento que estiver chamando será bloqueado até que um elemento esteja disponível. Se usar foreach somente para processar a saída da consulta PLINQ, você obterá isto:

var q = ... some query ...;
foreach (var e in q) {
    a(e); // this runs in parallel with the execution of 'q'
}

Na verdade, a interface IParallelEnumerable<T> oferece uma sobrecarga de GetEnumerator que obtém um argumento bool chamado pipelined, permitindo que você escolha o processamento stop-and-go (true significa pipelined e false significa stop-and-go). Na primeira chamada subseqüente a MoveNext, toda a consulta será executada e a chamada só será retornada quando toda a saída for disponibilizada. As chamadas subseqüentes a MoveNext simplesmente enumeram um buffer que contém a saída:

var q = ... some query ...;
using (var e = q.GetEnumerator(false)) {
    while (e.MoveNext()) {
        // after the 1st call, the query is finished executing
        // we then just enumerate the results from an in-memory list
        a(e.Current);
    }
}

Existem alguns casos especiais em que o processamento stop-and-go é usado como o padrão: se você usar os métodos ToArray ou ToList, esses operadores forçarão uma operação stop-and-go internamente. Se você tiver uma classificação em sua consulta, o stop-and-go será utilizado, já que o pipeline de uma classificação é inútil. Uma classificação apresenta latência extremamente alta (porque geralmente precisa classificar toda a entrada antes de produzir um único elemento de saída) e, portanto, o PLINQ prefere dedicar todo o poder de processamento à conclusão da classificação o mais rápido possível.

Para usar a enumeração invertida, você deve usar uma API diferente, específica do PLINQ:

public static class System.Linq.ParallelEnumerable {
    public static void ForAll<T>(
        this IParallelEnumerable<T> source, Action<T> action);
    ... the other standard query operators ...
}

A utilização da API ForAll se parece um pouco com o uso do loop foreach, como você acabou de ver:

var q = ... some query ...;
q.ForAll(e => a(e));

Exceções concorrentes
Cenários obrigatórios

Durante a leitura deste artigo, provavelmente você já começou a imaginar algumas formas de usar PLINQ em seus próprios aplicativos. Talvez você já esteja usando o LINQ e queira aumentar a escalabilidade do seu aplicativo em máquinas com vários processadores ou vários núcleos. É claro que o PLINQ pode fazer com que seus programas atuais sejam executados com mais rapidez, mas ele também permite que você trabalhe e opere com tamanhos de dados maiores no mesmo espaço de tempo, enquanto processa fluxos de dados a uma taxa mais rápida. Ao fazer tudo isso, a nova tecnologia PLINQ pode abrir novas possibilidades de aplicativos que antes você não poderia experimentar.

Vamos examinar algumas ilustrações de cenários nas quais os vários núcleos e o PLINQ abrem novas portas. Considere um produtor musical trabalhando em um estúdio de som e que deseja aplicar uma série de efeitos em sons brutos de instrumentos para produzir uma faixa mestre de qualidade e mais agradável esteticamente. A empresa que fornece seu software de mixagem poderia aplicar esses efeitos usando PLINQ. Normalmente, esses efeitos são compostos de filtros e de projeções feitas em grandes fluxos de dados (a música bruta). O PLINQ poderia agilizar bastante o tempo de produção e utilizar hardware mais poderoso quando ele se tornar disponível. Essa abordagem pode até permitir transformações musicais em tempo quase real, em vez de haver a necessidade do processamento após a produção.

Da mesma forma, considere um operador de câmbio de moeda estrangeira que procura condições de arbitragem de câmbio (ineficiências no mercado) para obter ganhos. Essas condições são efêmeras e desaparecem rapidamente, uma vez que o mercado está sempre procurando o equilíbrio, o que requer negócios extremamente rápidos. A consulta de informações sobre o mercado de ações usando o paralelismo via PLINQ poderia permitir a tomada de decisões quase em tempo real, embasada em grandes quantidades de dados e em análises e cálculos complicados.

Esses são somente alguns exemplos de formas em que a agilização fornecida pelo PLINQ em hardware de vários núcleos pode trazer uma vantagem comercial. Outros domínios oferecem oportunidades similares, como a saúde, economia, modelagem geológica, computação científica, controle e simulações de tráfego, jogos, inteligência artificial, aprendizado de máquina, análise lingüística, entre outros.

Embora as declarações anteriores sobre o processo em paralelo do PLINQ ser totalmente transparente terem sido em grande parte verdadeiras, existe um pequeno número de lugares onde o uso do paralelismo pode se estender além das simples abstrações apresentadas acima. Essas são as armadilhas mencionadas anteriormente. Felizmente, a maioria delas é secundária, mas é preciso saber que elas existem.

Quaisquer operadores lambdas ou de consulta que lançam uma exceção impedem que consultas LINQ seqüenciais sejam imediatamente executadas. Isso acontece porque quando somente um processador é usado na execução da consulta, os elementos são processados um após o outro, de forma seqüencial: se um operador falhar em um deles, a exceção será lançada imediatamente e os elementos subseqüentes não serão nem mesmo considerados. Isso não é verdade para o PLINQ.

Como ilustração, observe esta consulta (imaginária):

object[] data = new object[] { "foo", null, null, null };
var q = data.Select(x => x.ToString());
foreach (var e in q) Console.WriteLine(e);

Sempre que ela for executada em LINQ, conseguirá executar ToString no primeiro elemento da matriz e falhará com uma NullReferenceException tentando chamar ToString no segundo. Você nunca chega ao terceiro ou quarto elemento. Quando vários processadores estão envolvidos, no entanto, como no caso do PLINQ, você tem a possibilidade de haver várias exceções em paralelo. Dependendo de como o PLINQ decida subdividir o problema, talvez você veja falhas para 1, 2 e 3, todas simultâneas, ou qualquer combinação deles, incluindo possivelmente 3, mas não 1 ou 2.

Para lidar com isso, o PLINQ utiliza um modelo de exceção ligeiramente diferente do LINQ para comunicar as falhas. Quando há uma exceção em um dos segmentos do PLINQ, primeiro o sistema tenta interromper a execução de todos os outros segmentos o mais rápido possível. Esse processo acontece de forma totalmente transparente. Mas isso pode ou não ser concluído a tempo de evitar que outras exceções aconteçam de forma concorrente e, de fato, elas podem já ter acontecido no momento em que o PLINQ é envolvido. Depois que todos os segmentos forem fechados, o conjunto completo de exceções ocorridas será agregado a um objeto de System.Concurrency.MultipleFailuresException e esse novo objeto de exceção agregado será relançado. Cada exceção ocorrida estará subseqüencialmente acessível por meio da propriedade InnerExceptions, do tipo Exception[], incluindo os rastreamentos de pilha não perturbada.

Na verdade, o PLINQ sempre lança uma única MultipleFailuresException quando uma exceção não-manipulada encerra a execução de uma consulta, mesmo quando somente uma exceção é lançada. No exemplo anterior, isso significa que o PLINQ sempre colocará as NullReferenceExceptions em uma MultipleFailuresException. Se isso não aconteceu e você deseja capturar uma exceção de um tipo particular, terá de escrever várias cláusulas catch. É claro que normalmente você não captura certos tipos de exceção, mas se quiser fazer isso, poderá escrever o código a seguir e duplicar um pouco de lógica:

try
{
    // query...
}
catch (NullReferenceException)
{ ... }
catch (MultipleFailuresException)
{ ... }

Isso não só é deselegante como também os desenvolvedores estariam propensos a esquecer um ou outro, gerando bugs que acontecem somente sob algumas circunstâncias e configurações.

Infelizmente, isso pode tornar a depuração mais difícil. Para começar, se uma exceção não for manipulada e um depurador for associado, você entrará na chamada a GetEnumerator (se estiver chamando foreach nos resultados da consulta) em vez de no local onde a sua exceção foi originada. Isso é similar ao que ocorre hoje com o modelo de programação assíncrona (BeginXX/EndXX) no .NET Framework. Felizmente, o PLINQ preserva os rastreamentos da pilha original e, portanto, se você expandir o objeto de MultipleFailuresException e examinar sua propriedade InnerExceptions, verá o conjunto completo de exceções com os rastreamentos de pilha original completos.

Ordenando os resultados da saída

Digamos que você tenha escrito o seguinte código em LINQ:

int[] data  = new int[] { 0, 1, 2, 3 };
int[] data2 = (from x in data select x * 2).ToArray();

Você pode prever o conteúdo de data2? A questão parece tão simples que é bobagem até mesmo considerá-la. Todos diriam: { 0, 2, 4, 6 }. Mas se você alterar o código como mostrado aqui, o conteúdo possível de data2 é, na verdade, diferente:

int[] data  = new int[] { 0, 1, 2, 3 };
int[] data2 = (from x in data.AsParallel() select x * 2).ToArray();

Nesse caso, { 0, 2, 4, 6 } certamente é possível, mas { 6, 0, 2, 4 } também é, ou qualquer outra permutação desses quatro números.

Isso acontece porque o PLINQ executa a consulta em paralelo e os resultados podem ser vistos assim que se tornam disponíveis, não importa se você está fazendo uma iteração na consulta com foreach ou organizando os resultados em uma matriz com ToArray. A ordenação do LINQ é simplesmente um subproduto do fato de que sua implementação processa as entradas de forma seqüencial. Por outro lado, a ordenação PLINQ é determinada pelo agendamento não determinístico de unidades paralelas de trabalho, apta a ser totalmente alterada de uma execução para a outra.

Essa foi uma decisão de design explícita tomada pela equipe do PLINQ. Historicamente, as consultas não garantem nada sobre a ordenação. Se você considerar o SQL Server™, por exemplo, a menos que uma cláusula order by tenha sido especificada no texto da consulta, a ordenação dependerá de muitos fatores: se um índice será usado na consulta, o layout dos registros em discos e assim por diante. Na verdade, ele também pode ser não determinístico, já que o SQL também pode usar o paralelismo na avaliação das consultas!

Como os usuários freqüentemente precisam preservar a ordenação e superar alguns pequenos desafios enfrentados por pessoas tentando migrar do LINQ, o PLINQ oferece uma forma de optar pela preservação da ordem. A preservação da ordem simplesmente garante que, desde que não haja a interferência de operações de classificação, a ordenação relativa entre elementos de saída está intimamente ligada à ordenação relativa entre os elementos de entrada. Se você quisesse garantir que a saída da consulta anterior fosse sempre {0, 2, 4, 6 }, poderia usar a seguinte consulta:

int[] data  = new int[] { 0, 1, 2, 3 };
int[] data2 = (from x in data.AsParallel(QueryOptions.PreserveOrdering)
               select x * 2).ToArray();

A preservação da ordem tem seu preço. Na verdade, ela pode causar um impacto substancial no desempenho e no dimensionamento das suas consultas. Isso acontece porque o PLINQ insere logicamente uma operação de classificação no final e essa classificação é um operador que não é perfeitamente dimensionado com o aumento do número de processadores. Para ter uma idéia do que isso significa, a consulta anterior é logicamente equivalente à seguinte consulta:

int[] data  = new int[] { 0, 1, 2, 3 };
int[] data2 = data.AsParallel().
                  Select((x, i) => new { x, i }). // remember indices
                  Select((x) => new { x * 2, i }). // (in original)
                  OrderBy((x) => x.i). // ensure order is preserved
                  Select((x) => x.x). // get rid of indices from output
                  ToArray(); // (in original)

As etapas introduzidas pelo PLINQ para o suporte da preservação da ordem estão em vermelho. Como é possível observar, é bem trabalhoso! Obviamente, o PLINQ implementa essa funcionalidade de uma forma muito mais eficiente do que simplesmente escrever essa versão da consulta, mas as etapas são logicamente equivalentes.

Efeitos colaterais
Recursos do PLINQ
  • Postagens feitas no blog de Joe Duffy sobre PLINQ
  • Entrevista em vídeo com Anders Hejlsberg sobre LINQ, transmitida pelo Canal 9
  • O projeto LINQ no .NET Framework Developer Center
  • A evolução do LINQ e seu impacto no design do C#, de Anson Horton (MSDN Magazine, junho de 2007)
  • Discussões sobre LINQ nos Fóruns do MSDN

O PLINQ se baseia em algo chamado pureza estatística: a maioria das consultas do LINQ não muda estruturas de dados ou executa operações impuras na maior parte do tempo. Outra forma de dizer isso é que a maioria das consultas LINQ é estritamente funcional; elas obtêm alguns dados como entrada, executam alguns cálculos e criam uma cópia totalmente separada dos dados, com algumas alterações, como saída. Mas essa utilização, que é prática recomendada, não é imposta de nenhuma forma pelos compiladores ou pelo runtime. Se houver consultas que interrompam esse padrão comum de utilização, a mudança para PLINQ pode se tornar um pouco mais complicada.

Como exemplo, avalie esta consulta LINQ:

var q = from x in data where (x.f-- > 0) select x;

Observe que o predicado da cláusula where na verdade modifica um campo de um objeto. Isso significa que pode não ser seguro executar a consulta em paralelo sem expor as condições de corrida e os bugs de concorrência. Por padrão, você deve supor que consultas como essa não são seguras e não devem ser executadas com PLINQ. Mas quando ela estaria realmente segura? As corridas acontecem somente quando o predicado é executado em vários segmentos no mesmo objeto, o que só pode acontecer se os dados contiverem objetos duplicados. Assim, se os dados forem um conjunto, tudo funcionará perfeitamente.

Existem outros casos, como os em que as variáveis static são usadas, que nunca serão seguros:

class C { internal static int s_cCounter; }
// elsewhere...
var q = from x in data where (x.f == C.s_cCounter++) select x;

Se você executar essa consulta em paralelo, ficará bem desapontado. Você poderia terminar com muitos objetos com valores duplicados do campo f, o que só acontece devido à condição de corrida. Tenha cuidado: a sua reação e abordagem iniciais para a solução desse problema poderiam ser substituir C.s_cCounter++ por Interlocked.Increment(ref C.s_cCounter). Embora essa abordagem leve a uma solução correta, qualquer tipo de sincronização reduzirá muito a escalabilidade de suas consultas. Você deve se esforçar para eliminar qualquer dependência da mutabilidade e do estado compartilhado de suas consultas como um primeiro princípio.

A plataforma Windows® é bastante centrada em segmentos, e muito do estado do sistema pode terminar associado ao segmento que esteja executando alguma parte do próprio código. Isso pode ter a forma de armazenamento local de segmentos, informações de segurança como a representação, apartments COM (particularmente os Single Threaded Apartments) e frameworks de GUI, como o Windows Forms e o Windows Presentation Foundation. Tudo isso pode causar problemas na mudança do LINQ para o PLINQ.

Enquanto que no modelo LINQ todo o código da consulta é executado no mesmo segmento que iniciou a consulta, no PLINQ o código da consulta é distribuído por vários segmentos. Infelizmente, nem sempre é óbvio o momento em que você obtém uma dependência em afinidade de segmento, já que muitos serviços do .NET Framework se baseiam internamente neles de forma intrínseca e transparente. O melhor conselho que posso dar é que você fique atento para as fontes de riscos para a concorrência mencionadas anteriormente e evite-as sempre que possível em suas consultas.

Colocando o PLINQ em funcionamento

O PLINQ permite que desenvolvedores de LINQ aproveitem o hardware paralelo £ inclusive para obter um desempenho melhor e para criar software mais intrinsecamente escalonável £ sem a necessidade de que eles entendam de concorrência ou de paralelismo de dados. O modelo de programação é simples, permitindo que um número maior de desenvolvedores aproveite um hardware mais paralelo. A barra lateral "Recursos do PLINQ" lista algumas referências para que você obtenha mais informações.

Para aqueles que já estejam usando LINQ-to-Objects, LINQ-to-XML ou LINQ para agruparem fontes de dados heterogêneas, será um processo simples fazer com que suas consultas existentes utilizem o PLINQ. Dê uma olhada na barra lateral "Cenários obrigatórios" para conhecer outros usos possíveis para essa tecnologia. Para aqueles que ainda não adotaram o LINQ, os ganhos de velocidade paralela são outro motivo pelo qual sua utilização deveria ser considerada. Apesar de o PLINQ ainda não estar disponível, a utilização do LINQ em seus programas é um investimento para o futuro.

Joe Duffy é chefe de desenvolvimento da equipe do Parallel FX na Microsoft e publica regularmente no blog www.bluebytesoftware.com/blog. Ele está escrevendo um livro, Concurrent Programming on Windows (Programação concorrente no Windows), a ser publicado pela Addison-Wesley.

Ed Essey há cinco anos é gerente de programa da Microsoft e trabalha em modelos de computação paralela e em infra-estrutura.


Desempenho paralelo: Otimize o c&#243;digo gerenciado para m&#225;quinas de v&#225;rios n&#250;cleos

Desempenho paralelo

Otimize o código gerenciado para máquinas de vários núcleos

Daan Leijen and Judd Hall
Este artigo aborda:
  • Biblioteca paralela de tarefas
  • Parallel.For versus ThreadPool
  • Distribuição de trabalho estática
  • Futuros
Este artigo utiliza as seguintes tecnologias:
Biblioteca FX paralela
Conteúdo
Apresentando a TPL
Paralelismo estruturado
Superexposição do paralelismo
Um exemplo de ray tracer
Distribuição de trabalho dinâmica
Agregação
Paralelismo de bifurcação-junção
Tarefas e futuros
Tarefas replicáveis
O gerenciador de tarefas

A
s máquinas com vários processadores estão se transformando em padrão, enquanto os aumentos de velocidade de um único processador vêm diminuindo. Assim, a chave para os aprimoramentos de desempenho consiste em executar um programa em vários processadores em paralelo. Infelizmente, ainda é muito difícil escrever algoritmos que realmente tirem proveito desses vários processadores. Na verdade, a maioria dos aplicativos utiliza um só núcleo e não apresenta aumento de velocidade quando executada em uma máquina com vários núcleos. Precisamos escrever nossos programas de uma nova forma.

Apresentando a TPL

A TPL (biblioteca paralela de tarefas) foi criada para tornar muito mais fácil escrever código gerenciado capaz de utilizar vários processadores automaticamente. Usando a biblioteca, você pode expressar de forma conveniente o paralelismo potencial no código seqüencial já existente, de forma que as tarefas paralelas expostas sejam executadas simultaneamente em todos os processadores disponíveis. Geralmente, isso resulta em aumentos de velocidade significativos.

A TPL está sendo criada em uma iniciativa de colaboração entre o Microsoft® Research, a equipe de CLR (Common Language Runtime) da Microsoft e a equipe da plataforma de computação paralela. A TPL é um dos principais componentes da biblioteca Parallel FX, a nova geração de suporte à simultaneidade para o Microsoft .NET Framework. Embora ainda não tenha chegado à versão 1.0, o primeiro CTP (Community Tech Preview) da biblioteca Parallel FX estará disponível no MSDN® ainda no segundo semestre de 2007. Assista a http://blogs.msdn.com/somasegar para obter mais detalhes. A TPL não exige nenhuma extensão de linguagem e funciona com o .NET Framework 3.5 e versões superiores.

Há total suporte para o Visual Studio® 2008 e todo o paralelismo é expresso com o uso de chamadas de métodos normais. Por exemplo, suponha que você tem o seguinte loop for, que eleva ao quadrado os elementos de uma matriz:

for (int i = 0; i < 100; i++) {
  a[i] = a[i]*a[i];
}

Como as iterações são independentes entre si, ou seja, as iterações subseqüentes não lêem atualizações de estado feitas pelas iterações anteriores, é possível usar a TPL para expressar o paralelismo potencial com uma chamada para o método paralell for, da seguinte forma:

Parallel.For(0, 100, delegate(int i) {
  a[i] = a[i]*a[i];
});

Observe que Parallel.For é apenas um método estático normal com três argumentos, sendo que o último é uma expressão delegada. Esse delegado captura o corpo do loop inalterado do exemplo anterior, o que torna especialmente simples experimentar a introdução da simultaneidade em um programa.

A biblioteca contém algoritmos sofisticados para a distribuição de trabalho dinâmica, adaptando-se automaticamente à carga de trabalho e a uma máquina específica. Já os primitivos da biblioteca apenas expressam o paralelismo potencial, mas não o garantem. Por exemplo, em uma máquina com um só processador, os loops for paralelos são executados em seqüência, o que corresponde quase com exatidão ao desempenho do código estritamente seqüencial. Contudo, em uma máquina com dois núcleos, a biblioteca usa dois threads de trabalho para executar o loop em paralelo, dependendo da carga de trabalho e da configuração. Isso significa que é possível introduzir o paralelismo no seu código hoje mesmo e que, quando houver vários processadores disponíveis, seus aplicativos vão utilizá-los automaticamente. Ao mesmo tempo, o código continuará a apresentar bom desempenho em máquinas mais antigas, com um só processador.

Infelizmente, a biblioteca não ajuda a sincronizar corretamente código paralelo que utiliza memória compartilhada. Ainda é responsabilidade do programador garantir que certos tipos de código possam ser executados em paralelo com segurança. Ainda são necessários outros mecanismos, como os bloqueios, para proteger modificações simultâneas na memória compartilhada. No entanto, a TPL oferece algumas abstrações que ajudam na sincronização (como mostraremos em seguida).

Paralelismo estruturado

Uma das abstrações mais importantes do programador paralelo é o loop paralelo. Analise, por exemplo, a seguinte rotina (bastante simples) de multiplicação de matriz:

void SeqMatrixMult(int size, double[,] m1, double[,] m2, double[,] result)
{
  for (int i = 0; i < size; i++) {
    for (int j = 0; j < size; j++) {
      result[i, j] = 0;
      for (int k = 0; k < size; k++) {
        result[i, j] += m1[i, k] * m2[k, j];
      }
    }
  }
}

Nesse exemplo, as iterações externas são independentes entre si e, potencialmente, podem ser feitas em paralelo. É fácil expor esse paralelismo potencial com a TPL. Em primeiro lugar, é preciso fazer referência ao assembly System.Concurrency.dll durante a compilação. Então, será possível importar a biblioteca para o código com uma instrução using:

using System.Concurrency;

Quando o namespace estiver disponível, bastará substituir o loop for externo da multiplicação de matriz por uma chamada para o método estático Parallel.For:

void ParMatrixMult(int size, double[,] m1, double[,] m2, double[,] result)
{
  Parallel.For( 0, size, delegate(int i) {
    for (int j = 0; j < size; j++) {
      result[i, j] = 0;
      for (int k = 0; k < size; k++) {
        result[i, j] += m1[i, k] * m2[k, j];
      }
    }
  });
}

A construção Parallel.For é um método estático normal com três argumentos. Os dois primeiros especificam os limites de iteração (entre 0 e o tamanho). O último é uma função delegada chamada para cada iteração. Esse delegado utiliza o índice de iteração como seu primeiro argumento e executa o corpo do loop inalterado do exemplo anterior. Não são necessárias alterações no corpo do loop original, uma vez que os delegados capturam automaticamente as variáveis livres do corpo do loop (como result e m1). Para obter mais informações sobre expressões delegadas, consulte msdn.microsoft.com/msdnmag/issues/06/00/C20.

Por fim, se alguma exceção for emitida em qualquer uma das iterações, todas as iterações serão canceladas e a primeira exceção será emitida novamente no thread de chamada, garantindo que as exceções sejam propagadas adequadamente e nunca perdidas.

Sem a TPL, é muito mais difícil expressar o paralelismo potencial nesse loop. Mesmo com a ajuda da classe .NET ThreadPool, ainda é preciso avaliar o custo da sincronização e a divisão de trabalho. A Figura 1 mostra a rotina de multiplicação de matriz em paralelo usando o pool de threads.

Figure 1 Distribuição de trabalho estática e sincronização explícita (Close Figure 1)
void ThreadpoolMatrixMult(int size, double[,] m1, double[,] m2,
    double[,] result)
{
  int N = size;
  int P = 2 * Environment.ProcessorCount; // assume twice the procs for
                                          // good work distribution
  int Chunk = N / P;                      // size of a work chunk
  AutoResetEvent signal = new AutoResetEvent(false);
  int counter = P;                        // use a counter to reduce
                                          // kernel transitions
  for (int c = 0; c < P; c++) {           // for each chunk
    ThreadPool.QueueUserWorkItem(delegate(Object o)
    {
      int lc = (int)o;
      for (int i = lc * Chunk;           // iterate through a work chunk
           i < (lc + 1 == P ? N : (lc + 1) * Chunk); // respect upper
                                                     // bound
           i++) {
        // original inner loop body
        for (int j = 0; j < size; j++) {
          result[i, j] = 0;
          for (int k = 0; k < size; k++) {
            result[i, j] += m1[i, k] * m2[k, j];
          }
        }
      }
      if (Interlocked.Decrement(ref counter) == 0) { // use efficient
                                                     // interlocked
                                                     // instructions
        signal.Set();  // and kernel transition only when done
      }
    }, c);
  }
  signal.WaitOne();
}

O exemplo já é bastante sofisticado, usando o pool de threads para itens de trabalho e usando um contador juntamente com uma única manipulação de espera para minimizar o número de transições de kernel. Além disso, divide estaticamente o loop em partes com base no número de processadores, criando o dobro do necessário para uma melhor adaptação a cargas de trabalho dinâmicas. Contudo, ao contrário de Parallel.For, a abordagem mostrada na Figura 1 não propaga exceções no corpo do loop e não pode ser cancelada.

Logicamente, esse código é muito mais difícil de escrever e mais propenso a erros do que o método Parallel.For. Além disso, apesar de ser ajustado manualmente e de usar uma divisão de trabalho próxima do ideal, a abordagem do pool de threads geralmente apresenta um desempenho inferior ao do método Parallel.For. A Figura 2 mostra os resultados de alguns testes relatados. Os resultados representam o relativo aumento de velocidade quando se coloca em paralelo o loop externo de uma multiplicação de matriz com 750 x 750 elementos £ sendo que 1 representa o tempo de execução com um loop for normal. Os testes foram realizados em uma máquina com dois núcleos, quatro soquetes e 3 GB de memória executando o Windows Vista® Ultimate. Observe que, na máquina com um só núcleo, a versão Parallel.For apresenta praticamente o mesmo desempenho do loop for direto.

Figura 2 Desempenho de Parallel.For versus ThreadPool
Figura 2 Desempenho de Parallel.For versus ThreadPool

Superexposição do paralelismo

Você deve ter notado que é possível expor ainda mais o paralelismo colocando em paralelo o segundo loop for, como no exemplo a seguir:

Parallel.For( 0, size, delegate(int i) {
  Parallel.For( 0, size, delegate(int j) {
    result[i, j] = 0;
    for (int k = 0; k < size; k++) {
      result[i, j] += m1[i, k] * m2[k, j];
    }
  });
});

Embora não haja problema em aninhar loops paralelos, em geral essa abordagem tem pior desempenho por dois motivos: primeiro, nesse exemplo específico, o loop externo já expõe oportunidades mais do que suficientes para o paralelismo pois, em geral, temos muito menos núcleos disponíveis do que o tamanho da matriz; segundo, todas as expressões delegadas alocam alguma memória para manter as variáveis livres. É por isso que, no nosso exemplo inicial, o custo de apenas uma das alocações era amortizado pelas iterações. Infelizmente, no novo código, o Parallel.For interno realizará a alocação de heap em cada iteração do loop externo. A alocação é muito eficiente no CLR, mas ainda é um custo considerável em comparação com o volume de trabalho realizado em cada iteração.

Observe que não é possível colocar o loop interno em paralelo, uma vez que essas iterações não são independentes. Ocorre, especificamente, uma corrida, pois cada iteração é adicionada ao local result[i,j]. Se esse loop fosse colocado em paralelo, duas iterações poderiam ler simultaneamente o valor atual em um registro, adicionar a ele e gravar o resultado £ e uma adição seria perdida! A única forma de colocar o loop interno em paralelo é protegendo adequadamente a adição com um bloqueio. É claro que, na verdade, fazer isso não é aconselhável: mesmo que, por enquanto, você ignore as alocações extras, o desempenho sofrerá um forte impacto, uma vez que as iterações simultâneas competirão pelo mesmo bloqueio. Voltaremos a esse assunto mais adiante, quando abordarmos as operações de agregação. Para obter informações sobre corridas e bloqueios, consulte msdn.microsoft.com/msdnmag/issues/05/08/Concurrency/default.aspx.

Um exemplo de ray tracer

O ray tracing é uma forma simples mas altamente eficaz de fazer o processamento de gráficos com qualidade fotográfica. No entanto, essa técnica exige muitos recursos computacionais. O ray tracing é, na verdade, um ótimo candidato à nossa biblioteca, pois cada raio traçado pode ser computado em paralelo. Utilizamos um ray tracer já existente, escrito por Luke Hoban (consulte blogs.msdn.com/lukeh/archive/2007/04/03/a-ray-tracer-in-c-3-0.aspx), e o modificamos para ser executado em paralelo usando a TPL. O ray tracer gera imagens como a mostrada na Figura 3 e estará disponível como um exemplo na CTP da biblioteca Parallel FX. O loop principal do ray tracer original faz a iteração de todos os pixels da imagem resultante:

Figura 3 Ray tracer paralelo
Figura 3 Ray tracer paralelo(Clique na imagem para diminuir a exibição)
Figura 3 Ray tracer paralelo
Figura 3 Ray tracer paralelo (Clique na imagem para aumentar a exibição)

void Render(Scene scene, Color[,] rgb)
{
  for (int y = 0; y < screenHeight; y++)
  {
    for (int x = 0; x < screenWidth; x++) {
      rgb[x,y] = TraceRay(new Ray(scene,x,y));
    }
  }
}

Como cada raio pode ser traçado de forma independente, é preciso alterar uma única linha do código original para colocá-lo em paralelo:

void Render(Scene scene, Color[,] rgb)
{
  Parallel.For(0, screenHeight, delegate(int y)
  {
    for (int x = 0; x < screenWidth; x++) {
      rgb[x,y] = TraceRay(new Ray(scene,x,y));
    }
  });
}

Em uma máquina de oito núcleos, o código original geraria 1,7 frame por segundo para uma imagem de 350 x 350 pixels. Em comparação, a versão em paralelo, executada na mesma máquina com oito processadores, gera 12 frames por segundo. Isso representa um aumento de velocidade de 7 vezes em uma máquina com oito processadores, um resultado extremamente positivo para uma alteração tão pequena. Esses 12 frames por segundo proporcionam rapidez suficiente para produzir uma animação suave das bolas quicando no chão. E, por tratar-se de um ray tracer muito simples, talvez você consiga otimizá-lo para obter uma animação ainda mais suave.

Distribuição de trabalho dinâmica

Em geral, quando colocam loops em paralelo manualmente usando o pool de threads, os desenvolvedores acabam dividindo o trabalho estaticamente. Em um ray tracer, por exemplo, geralmente a imagem é dividida em partes iguais, sendo que cada parte é processada por um thread separado. Na maioria dos casos, essa não é uma boa idéia, pois a carga de trabalho real pode estar dividida de forma desigual. Por exemplo, se a parte inferior da imagem levar o dobro do tempo para ser computada devido à presença de reflexos, os threads que atendem à parte superior da imagem ficarão aguardando a maior parte do tempo até que os threads da parte inferior terminem. Mesmo se o trabalho for dividido em partes iguais, isso ainda poderá ocorrer, devido a falhas na página ou a outros processos do sistema executados simultaneamente.

Para ser dimensionada adequadamente em vários processadores, a TPL utiliza técnicas de "roubo de trabalho" para adaptar e distribuir dinamicamente os itens pelos threads de trabalho. A biblioteca tem um gerenciador de tarefas que usa, por padrão, um thread de trabalho por processador. Isso garante o mínimo de alternância de threads pelo sistema operacional. Cada thread de trabalho tem sua própria fila de tarefas locais com o trabalho a ser feito. Normalmente, cada thread de trabalho apenas envia novas tarefas para sua própria fila e busca mais trabalho sempre que uma tarefa é concluída. Quando a fila local está vazia, ele procura trabalho e tenta "roubar" tarefas das filas dos outros threads de trabalho.

A vantagem desse método é que quase não ocorre sincronização entre os threads de trabalho, uma vez que as filas são distribuídas e a maioria das operações é local £ algo fundamental para a escalabilidade. Além disso, o "roubo de trabalho" tem comprovadamente localidade de cache e propriedades de distribuição de trabalho eficientes. Por exemplo, se a carga de trabalho for desigual, um thread de trabalho pode levar muito tempo em uma tarefa específica, mas os demais "roubarão" trabalho da sua fila, mantendo todos os processadores ocupados. A distribuição de trabalho dinâmica é fundamental nos aplicativos típicos, pois é difícil prever quanto tempo levará determinada tarefa. Isso é verdade, especialmente, no caso de sistemas de desktop nos quais os processadores são compartilhados por vários processos distintos e não é possível prever que fração de tempo os threads de trabalho irão obter.

A Figura 4 demonstra a distribuição de trabalho dinâmica em ação, com o uso de quatro threads. Mostra a mesma imagem da Figura 3, produzida com ray tracing, mas agora cada thread de trabalho usa uma cor diferente para processar seus pixels. Você pode ver que a biblioteca distribuiu o trabalho igualmente entre os threads de trabalho, adaptando-se dinamicamente à carga de trabalho.

Figura 4 Distribuição de trabalho entre os threads de trabalho
Figura 4 Distribuição de trabalho entre os threads de trabalho(Clique na imagem para diminuir a exibição)
Figura 4 Distribuição de trabalho entre os threads de trabalho
Figura 4 Distribuição de trabalho entre os threads de trabalho (Clique na imagem para aumentar a exibição)

Além de realizar a distribuição de trabalho dinâmica, a biblioteca também ajusta dinamicamente o número de threads de trabalho, se alguns deles forem bloqueados. Entre os exemplos de bloqueio de operações estão leituras de arquivos, a espera pelo pressionamento de uma tecla e a recuperação do nome de usuário (capaz de acessar a rede em um domínio). Se uma tarefa está bloqueada e não há conhecimento disso, pode haver degradação do desempenho, pois o nível de simultaneidade cai (mas o programa continua a comportar-se corretamente). Para aumentar o desempenho, a biblioteca faz um controle automático para verificar se os threads de trabalho estão bloqueados e, se for necessário, injeta threads de trabalho extras para manter o nível de simultaneidade. Quando as operações estiverem desbloqueadas, alguns threads de trabalho poderão ser desativados para reduzir o custo da alternância de threads.

Agregação

Em geral, um loop for é usado para fazer a iteração em um domínio e agregar os valores em um só resultado. Observe, por exemplo, as iterações a seguir, que somam os números primos menores do que 100:

int sum = 0;
for(int i = 0; i < 100; i++) {
  if (isPrime(i)) sum += i;
}

Infelizmente, não seria possível colocar esse loop em paralelo, pois isso criaria uma corrida de dados. Cada iteração modifica a variável de soma compartilhada sem proteção de bloqueio. Se duas iterações simultâneas incrementarem a soma ao mesmo tempo, é possível que as duas leiam o mesmo valor em um registro, adicionem a ele e gravem o resultado £ e uma adição será perdida. Uma versão correta utilizaria um bloqueio para proteger a adição, da seguinte forma:

int sum = 0;
Parallel.For(0, 100, delegate(int i) {
  if (isPrime(i)) {
    lock (this) { sum += i; }
  }
});

Contudo, agora o programa apresenta um problema de desempenho, pois todas as iterações paralelas competem pelo mesmo bloqueio e pelo mesmo local da memória (soma). Seria melhor se cada thread de trabalho mantivesse uma soma local de thread e somente adicionasse à soma global no final do loop. Esse padrão é capturado pela operação Parallel.Aggregate e, assim, é possível reescrever o exemplo da seguinte forma:

int sum = Parallel.Aggregate(0, 100, // iteration domain
              0,                     // initial value
              delegate(int i) { return (isPrime(i) ? i : 0) }, // apply
                                                      // on each element
              delegate(int x, int y) { return x+y; }  // combine results
          );

A operação agregada utiliza cinco argumentos. Os dois primeiros especificam o domínio da iteração, que pode ser também um enumerador. O argumento seguinte é o valor inicial para o resultado. Os dois outros argumentos são funções delegadas. A primeira função é aplicada a cada elemento e a outra é usada para combinar os resultados dos elementos.

A biblioteca utiliza automaticamente uma variável local de thread para computar os resultados locais de thread sem qualquer bloqueio, usando somente um bloqueio para combinar os resultados locais de thread finais. Lembre-se de que, se a agregação for feita em paralelo, será possível combinar elementos em uma ordem diferente daquela usada em uma agregação seqüencial. Por isso, a função delegada de combinação deve ser associativa e o valor inicial deve ser um elemento de unidade.

Paralelismo de bifurcação-junção

Outro padrão paralelo comum é o paralelismo de bifurcação-junção. Analise, como exemplo, a seguinte implementação de classificação rápida seqüencial:

static void SeqQuickSort<T>(T[] domain, int lo, int hi) where T : IComparable<T>
{
  if (hi - lo <= Threshold) InsertionSort(domain, lo, hi);

  int pivot = Partition(domain, lo, hi);
  SeqQuickSort(domain, lo, pivot - 1);
  SeqQuickSort(domain, pivot + 1, hi);
}

O algoritmo é genérico do tipo de elemento T e requer somente que as instâncias de T possam ser comparadas. Dentro de um certo limite, o algoritmo faz fallback para a classificação de inserção, o que resulta em melhor desempenho para uma pequena quantidade de elementos. Caso contrário, é possível particionar a matriz de entrada em duas partes e fazer a classificação rápida de cada parte separadamente. Essas duas classificações podem ser realizadas em paralelo, pois cada uma delas atua em uma parte distinta da matriz. Isso pode ser expresso de forma conveniente com o método Parallel.Do:

static void ParQuickSort<T>(T[] domain, int lo, int hi) where T : IComparable<T>
{
  if (hi - lo <= Threshold) InsertionSort(domain, lo, hi);

  int pivot = Partition(domain, lo, hi);
  Parallel.Do(
    delegate { ParQuickSort(domain, lo, pivot - 1); },
    delegate { ParQuickSort(domain, pivot + 1, hi); }
  );
}

O método Parallel.Do é estático e utiliza dois ou mais delegados como argumentos, executando-os potencialmente em paralelo. Como a classificação rápida é recursiva, há muito paralelismo exposto, pois cada invocação introduz mais tarefas paralelas. Mais uma vez, como a biblioteca não garante a execução em paralelo, a maioria das tarefas é, na verdade, executada em seqüência, o que é essencial para o bom desempenho.

Tarefas e futuros

Todos os exemplos anteriores demonstram o paralelismo estruturado, sendo que o escopo do código paralelo é determinado pelo escopo léxico. Mas nem todos os algoritmos paralelos podem ser expressos dessa forma. Felizmente, a biblioteca também fornece suporte para tarefas paralelas gerais:

class Task
{
  Task( Action action );

  void Wait();
  void Cancel();
  bool IsCompleted { get; }
  ...
}

Uma tarefa é criada com o fornecimento de uma ação associada que, potencialmente, possa ser executada em paralelo. A ação é executada em algum ponto entre o momento da criação da tarefa e a primeira chamada para o método Wait. A ação associada pode ser executada em paralelo em outro thread, mas existe a garantia de que a ação não migrará entre os threads. Essa garantia é útil, uma vez que os programadores podem usar abstrações afins aos threads, como seções críticas do Windows, sem precisar preocupar-se, por exemplo, se LeaveCriticalSection será executada em um thread diferente de EnterCriticalSection. Se uma tarefa já está concluída, Wait retorna imediatamente.

Qualquer exceção gerada na ação associada é armazenada em uma tarefa e gerada novamente sempre que Wait é chamado. De forma semelhante, as funções Parallel.For e Parallel.Do acumulam todas as exceções emitidas e são geradas novamente quando todas as tarefas estão concluídas. Isso garante que as exceções nunca sejam perdidas e que sejam propagadas adequadamente para os dependentes.

Por fim, é possível cancelar a tarefa e todas as outras criadas em suas ações associadas (tarefas filho) chamando Cancel. O cancelamento não é preemptivo e uma tarefa em execução precisa ser encerrada de forma cooperativa durante o trabalho, fazendo a chamada de retorno para a biblioteca. Isso pode ser feito, por exemplo, criando novas tarefas ou chamando o método Wait. Se a tarefa pai foi cancelada, essas chamadas para a biblioteca irão gerar uma exceção OperationCanceled (síncrona) para interromper a ação.

Você pode considerar as tarefas como um pool de threads aprimorado em que os itens de trabalho retornam uma manipulação que pode ser cancelada ou aguardada, com as exceções sendo propagadas. Há também uma variação das tarefas, denominada futuros, em que a ação associada computa um resultado:

class Task<T> : Task
{
  Task ( Func<T> function );
  T Value { get; }            // does an implicit wait
}

Um futuro, que é uma tarefa que computa um resultado, não é construído com uma ação normal, mas sim com uma ação que retorna um resultado. Esse resultado é um delegado do tipo Func<T>, onde T é o tipo de valor do futuro.

O resultado do futuro é recuperado por meio da propriedade Value. A propriedade Value chama Wait internamente para garantir que a tarefa foi concluída e que o valor do resultado foi computado. Como Wait é chamado, chamar Value emite as exceções geradas durante a computação do valor. Uma forma de encarar isso é pensar em um futuro como algo que tem um valor ou um valor excepcional, o que é determinado pela computação.

Os futuros são um conceito antigo já implementado em multi-lisp. No entanto, observe que nossa noção de um futuro não é "segura", no sentido de que o programador é responsável por bloquear adequadamente a memória compartilhada. Isso contrasta com as abordagens nas quais a ação de um futuro é automaticamente encapsulada em uma transação de memória.

A abstração de futuro funciona bem com código simbólico, menos estruturado do que os loops. Analise, por exemplo, a seguinte definição de nós e folhas de árvores binárias:

class Node : Tree {
  int   depth;  // The depth of the tree
  Tree  left;   // The left sub tree
  Tree  right;  // The right sub tree
  ...
}

class Leaf : Tree {
  int value;  // values are stored in the leafs
  ...
}

Suponha, agora, que definimos um método Sum virtual em uma árvore que soma todos os valores das folhas. Uma folha simplesmente retorna seu valor. Os nós adicionam as somas de suas subárvores:

override int Sum() {
   int l = left.Sum();
   int r = right.Sum();
   return (r + l);
}

Nesse caso, cada computação filho pode ser feita em paralelo, pois são independentes. O paralelismo aqui tem escopo léxico e é possível usar Parallel.Do mas, a título de demonstração, utilizaremos futuros:

override int Sum()
{
   Task<int> l = new Task<int>( left.Sum );
   int r       = right.Sum();
   return (r + l.Value);
}

Para cada subárvore à esquerda, criamos um novo futuro do tipo int, passando um delegado como o argumento do construtor. Nesse exemplo, passamos o método Sum do filho à esquerda, left.Sum, sem chamá-lo. Continuamos, calculando a soma da subárvore à direita. Com a criação do futuro, outros processadores, potencialmente, poderiam começar a avaliar em paralelo a soma da subárvore à esquerda. No final, solicitamos o valor do futuro usando a propriedade Value.

Se a tarefa já tiver sido calculada por outro thread de trabalho, essa chamada retornará imediatamente o valor do resultado. Que ótimo! Se a tarefa ainda estiver em execução em outro thread de trabalho, será feito um bloqueio até que o resultado esteja disponível (mas outro thread de trabalho será programado para manter o nível de simultaneidade).

Há outro cenário muito comum, no qual a tarefa ainda não foi iniciada. Nesse caso, a chamada para Value executa a tarefa diretamente no thread de chamada. Isso é comum e muito eficiente, pois é feita apenas uma chamada de método indireta. Por outro lado, quando aguardamos os sinais fornecidos pelo sistema operacional, não é possível fazer com que o sinal ocorra e a única coisa que pode ser feita é bloquear o thread de chamada, o que em geral afeta negativamente o desempenho. Em nosso caso, fica claro como calcular o valor e a biblioteca executa a ação associada diretamente, em vez de bloquear o thread.

Como o volume de trabalho realizado nas folhas é muito pequeno, talvez seja melhor aumentar o volume de trabalho realizado por cada tarefa, fazendo a soma em seqüência em determinada profundidade da árvore. Podemos, por exemplo, pressupor um método de soma seqüencial SeqSum e escrever:

override int Sum()
{
   if (depth < 10) return SeqSum();

   Task<int> l = new Task<int>( left.Sum );
   int r         = right.Sum();
   return (r + l.Value);
}

Em geral, a determinação do limite correto depende do volume de trabalho realizado na ação de uma tarefa, em comparação com o custo da alocação de um objeto de tarefa. Segundo a nossa experiência, a alocação é econômica e o limite geralmente fica em torno de 100 multiplicações de ponto flutuante.

Como os futuros são verdadeiros valores de primeira classe, é possível usá-los para introduzir o paralelismo entre partes logicamente distintas de um programa. Você pode, por exemplo, armazenar futuros em estruturas de dados em que outra fase distinta realmente solicita os valores desses futuros. Um espaço adequado para isso seriam os aplicativos de jogos. Uma fase poderia, por exemplo, calcular a nova integridade de todos os personagens como um futuro, enquanto outras fases utilizariam depois os valores desses futuros. Em uma máquina com vários núcleos, esses futuros poderiam ser calculados em paralelo com o trabalho realizado em cada fase.

Tarefas replicáveis

A biblioteca se baseia em apenas dois conceitos primitivos: tarefas e tarefas replicáveis. Todas as demais abstrações, como os futuros e os loops for paralelos, são expressas em termos desses dois primitivos. Isso garante que as operações se comportem de forma regular, com uma semântica consistente. Por exemplo, as exceções são sempre propagadas de forma eficiente e todas as abstrações podem ser canceladas (inclusive os loops for paralelos).

Observe que, na verdade, as tarefas replicáveis se destinam a autores de bibliotecas que desejam estender as abstrações padrão oferecidas pela TPL, e somente devem ser utilizadas com moderação (se utilizadas) no código normal. Uma tarefa replicável deriva diretamente de uma tarefa normal, da seguinte forma:

class ReplicableTask : Task
{
  ReplicableTask( Action action );
}

Uma tarefa replicável representa uma tarefa que pode, por si só, ser executada por vários threads ao mesmo tempo. Essa tarefa captura o padrão de simultaneidade ubíquo aplicável a tudo, ao mesmo tempo em que abstrai a dinâmica da distribuição de trabalho. O construtor utiliza um delegado de ação potencialmente executado em paralelo em outro thread e potencialmente executado em vários threads ao mesmo tempo. Se uma exceção for gerada em qualquer uma dessas execuções, somente uma delas será armazenada e emitida novamente por Wait.

É possível usar uma tarefa replicável se outros threads, potencialmente, puderem participar da realização do trabalho. Todas as variações de Parallel.For e Parallel.ForEach são implementadas com o uso de tarefas replicáveis. Podemos, por exemplo, implementar de forma bastante simples o Parallel.For básico como:

static void For( int from, int to, Action<int> body )
{
  int index = from;
  ReplicableTask rtask = new ReplicableTask( delegate {
    int i;
    while ((i = Interlocked.Increment(ref index)-1) < to) {
      body(i);
    }
  });
  rtask.Wait();
}

Como todas as tarefas replicáveis compartilham a variável de índice, o delegado de ação efetivamente passado para o construtor da tarefa replicável pode ser executado por quantos threads forem necessários. Na implementação, isso pode ser usado para permitir que processadores ociosos participem do trabalho.

Nesta implementação, cada thread de trabalho declara um índice por vez. Isso corresponde à estratégia dynamic(1) de OpenMP e geralmente funciona bem quando a carga de trabalho de cada índice pode diferir significativamente (para obter mais informações sobre isso, consulte msdn.microsoft.com/msdnmag/issues/05/10/OpenMP). Mas essa estratégia pode levar à contenção de cache quando a carga de trabalho por índice é pequena. Nesse caso, seria melhor processar vários índices de uma só vez. Veja esta variação de Parallel.For, que corresponde à estratégia dynamic(n) de OpenMP e utiliza uma distância como argumento:

static void ForWithStride( int stride, int from, int to, Action<int> body )
{
  int index = from;
  if (stride <= 0) stride = 1;
  ReplicableTask rtask = new ReplicableTask( delegate {
    int i;
    while ((i = Interlocked.Add(ref index,stride)-stride) < to) {
      int end = Math.Min(i+stride, to);
      do {
        body(i);
        i++;
      }
      while (i < end)
    }
  });
  rtask.Wait();
}

As tarefas replicáveis são uma abstração eficaz para implementar diferentes estratégias de iteração paralela. No entanto, segundo nossa experiência, as implementações padrão de Parallel.For e Foreach funcionam bem em diversos cenários e, assim, esperamos que na prática não haja muita necessidade de recorrer à expressividade extra das tarefas replicáveis.

O gerenciador de tarefas

Todas as tarefas pertencem a um gerenciador de tarefas que, como o nome indica, gerencia as tarefas e supervisiona os threads de trabalho na execução delas. Embora haja sempre um gerenciador de tarefas padrão disponível, um aplicativo pode também criar explicitamente um gerenciador de tarefas. A interface do gerenciador de tarefas é definida como:

class TaskManager : IDisposable
{
  TaskManager();
  TaskManager( int maxConcurrentThreads );

  static TaskManager Current     { get; }
  int    MaxConcurrentThreads    { get; }
  ...
}

class Task
{
  Task( TaskManager taskManager, Action action )
  ...
}

Um gerenciador de tarefas tem um nível de simultaneidade associado, retornado pela propriedade MaxConcurrentThreads. Isso especifica para o gerenciador o número ideal de threads executando tarefas em qualquer momento. É uma indicação para que, se o gerente precisar usar mais para acelerar o andamento, faça isso dinamicamente. Ao criar um gerenciador de tarefas, você pode fornecer esse número explicitamente. Por padrão, é igual ao número de processadores.

Em geral, nunca é preciso criar explicitamente um gerenciador de tarefas, pois há sempre um padrão disponível. Contudo, é possível que você queira usar vários gerenciadores de tarefas, cada um com um nível de simultaneidade diferente, ou então cada um manipulando um conjunto de tarefas separado. Nesse caso, é possível criar um novo gerenciador de tarefas e usar o construtor Task especial, que utiliza um gerenciador de tarefas como seu primeiro argumento e executa a tarefa e todos os seus filhos nesse gerenciador. Analise, por exemplo, o código a seguir:

using (TaskManager tm = new TaskManager(2)) {  // only use 2 worker
                                               // threads for tasks
  new Task( tm, delegate {
    // tasks created in this delegate use the tm task manager by default
    ...
    // finally, show some statistics
    Console.WriteLine( "statistics: " + tm );
  }).Wait();
}

Outra utilização importante da interface do gerenciador de tarefas consiste em executar todo o código em seqüência, usando um só thread de trabalho. Isso significa que todas as tarefas e todos os loops for paralelos serão executados em seqüência. Isso é ótimo para fins de depuração, pois você pode verificar se o código funciona corretamente quando executado em seqüência antes de executá-lo em paralelo, em uma máquina com vários processadores.

Daan Leijen é pesquisador do Microsoft Research. Seus interesses atuais são programação declarativa e funcional, sistemas de inferência de tipos e computação paralela.

Judd Hall é gerente de programas II da Microsoft.


Aplicativos m&#243;veis: Ajuste o volume do seu toque de acordo com o som ambiente

Aplicativos móveis

Ajuste o volume do seu toque de acordo com o som ambiente

Chris Mitchell
Este artigo aborda:
  • Desenvolvimento para Pocket PC
  • Medindo a intensidade
  • Instaladores personalizados
  • Uso de P/Invoke
Este artigo utiliza as seguintes tecnologias:
.NET Compact Framework
Download do código disponível em: NoiseDetection2007_10.exe (193KB)
Conteúdo
A abordagem
Valores de referência
A implementação
Como lidar com a instalação
Resultados

V
ocê está sentado em um bar, à tarde, aproveitando um momento de tranqüilidade e bebendo um café, quando o seu telefone começa a tocar. É claro que o volume do celular foi definido no máximo, e o toque é tão alto que você quase derrama o café. Então, você abaixa o volume.

Mais tarde, no mesmo dia, você está esperando um amigo, que ainda não chegou. Ele esqueceu em que lugar vocês combinaram e liga para ver onde você está. Infelizmente, você não atende à ligação porque não ouve o telefone tocar. O problema: o volume do toque ainda estava tão baixo que você não ouviu por causa do barulho do local.

Isso é ridículo quando levamos em consideração que os celulares atuais possuem muito mais poder de processamento do que o primeiro PC que usei para a gravação de áudio em vários canais. Parece-me que, com os dispositivos móveis inteligentes de hoje, capazes de fazer tantas coisas e de acessar tantas informações, esse problema não deveria existir. No entanto, todos que possuem um celular já tiveram esse problema de uma forma ou de outra.

A maneira óbvia de resolver o problema é lembrar de mudar as configurações de volume. A opção de mudar o volume do toque é, presumivelmente, lidar com as diferenças acústicas de vários ambientes, mas a funcionalidade não faz isso. Os dispositivos deveriam ser capazes de agir com um pouco mais de bom senso. É simples: não quero que o meu celular toque muito alto em um ambiente silencioso e quero poder ouvi-lo em um local barulhento.

Este artigo trata das possibilidades e da viabilidade de um aplicativo do Windows Mobile® que ajusta o volume do toque de um Pocket PC automaticamente, com base no nível de ruído do ambiente. A funcionalidade desse aplicativo impede que o telefone tenha um toque muito alto ou muito baixo, evitando que eu passe vergonha ou perca as ligações. Neste artigo, usarei esse problema como uma introdução ao desenvolvimento do Windows Mobile usando o Visual Studio® 2005 e o C#.

Envergonhado porque o seu telefone toca muito alto?
Envergonhado porque o seu telefone toca muito alto?

A abordagem

O plano é usar os sensores de um celular para obter uma compreensão melhor do ambiente atual. Quero usar o microfone para medir o ruído do ambiente e ajustar o nível do toque de forma apropriada.

Preciso tratar de uma série de problemas específicos para que isso funcione, como indicado na Figura 1. Como posso obter as amostras de áudio do microfone interno do dispositivo? Que medida devo usar para definir a intensidade? Como mudo o nível de toque do dispositivo móvel em resposta a essas informações? Discutirei a primeira parte do sistema, a coleta de informações sobre o ambiente usando um microfone, em alguns instantes. No entanto, primeiro preciso definir uma medida de intensidade.

Figura 1 Etapas de alto nível para o ajuste do volume do toque
Figura 1 Etapas de alto nível para o ajuste do volume do toque

A intensidade é subjetiva e normalmente representada por um gráfico de curvas de intensidade igual (para obter mais informações sobre a percepção subjetiva do som, recomendo que você pesquise alguns artigos sobre o estudo da psicoacústica). Essas representações gráficas mostram a freqüência em um eixo e uma medida de pressão do som em outro, normalmente em decibéis (dB). Os gráficos exibem o relacionamento entre a freqüência e a nossa percepção da intensidade igual.

Para determinar os valores do relacionamento, estudos apresentaram indivíduos com tons com incrementos de 10 dB em várias freqüências. Os participantes do estudo deviam julgar quando os sons tinham uma intensidade igual em comparação a um sinal de referência de 1 KHz. Esses relacionamentos eram usados então na criação de tabelas e de representações gráficas da intensidade igual e que poderiam ser usados, junto com as informações de freqüência obtidas do microfone do telefone, na criação de uma medida de intensidade.

Outra abordagem é comparar uma medida do nível de pressão do som para fazer referência a valores e ajustar o nível do toque caso houvesse uma mudança significativa. Assim, o que constitui uma mudança poderia ser definido pelo perfil do telefone. Isso supera os problemas das variações entre microfones e alto-falantes dos diferentes telefones. Simplesmente alterar os valores de referência permite que o software opere com eficiência em vários modelos de telefone. Isso também significa que o sistema não precisa obter informações de freqüência do microfone, economizando a valiosa carga da bateria.

A complexidade do design do sistema é simplificada por meio da redundância da decomposição de freqüência e da medida de dB. No entanto, o meu aplicativo necessita de uma medida de intensidade relativa. No processamento de sinais de áudio, o RMS (valor eficaz) pode ser usado como medida rudimentar de intensidade (explicarei o RMS em mais detalhes quando estiver descrevendo o estágio de implementação). Embora os atributos de freqüência ainda afetem a intensidade percebida, os valores RMS devem ser suficientes para as necessidades de exatidão £ isso é especialmente verdadeiro considerando que os Pocket PCs possuem somente cinco configurações de volume. A função RMS pode ser usada na obtenção dos valores necessários ao ajuste do volume do toque.

Valores de referência

Depois que as amostras forem obtidas do microfone e inseridas na função RMS, a saída terá de ser mapeada para um dos cinco estados de volume. O que é a função de mapeamento e como as configurações se relacionam com os valores de referência discutidos anteriormente? Os valores são específicos de cada dispositivo e definem quando o volume do toque do celular será alterado. Por exemplo, o nível do toque deve ser ajustado apropriadamente quando a função RMS indicar que o ambiente é similar a uma cafeteria ou a um bar muito barulhento.

Se eu fosse ampliar esse aplicativo para que ele oferecesse suporte a vários perfis de dispositivo, criaria uma interface para a sua configuração. Neste artigo, entretanto, não vou implementá-la e deixarei que outra pessoa o faça.

O processo de geração de valores de referência para um perfil de dispositivo não é muito complicado. Primeiro, você utiliza os recursos de relatório de RMS do aplicativo para obter valores em diversas situações de referência adequadas às suas necessidades. Eu usei quatro ambientes comuns: uma área residencial à noite (45 db), um restaurante ou escritório silencioso (55 dB), um restaurante fechado típico (65 dB) e um congestionamento a 5 metros de distância (75 dB). Esses valores serão a referência para o mapeamento dos cinco estados de volume de um Pocket PC (é óbvio que será necessária uma ferramenta que possa ser usada na obtenção dos diversos valores RMS nesses locais).

Em seguida, depois de obter um valor médio para todos os seus locais, você usará esses valores como os limites superior e inferior no código do aplicativo.

A implementação

Eu optei por criar a minha solução no Visual C#® 2005 devido à sua plataforma de desenvolvimento móvel integrada. Isso facilitou muito o desenvolvimento para Pocket PC, embora ainda não haja um assistente interno para a criação de um instalador de desktop que possa implantar o seu aplicativo de Pocket PC por meio do desktop. Existem algumas ferramentas gratuitas boas que agem como instaladores e há métodos para a criação do seu próprio instalador personalizado (discutirei a criação de um instalador personalizado mais adiante neste artigo).

Por ora, quero voltar ao aplicativo, que chamarei de TooLoud. A criação do meu aplicativo será dividida em três partes:

  1. Para capturar e processar áudio, usarei o serviço P/Invoke do Microsoft® .NET Compact Framework.
  2. Escreverei uma função para calcular os valores RMS.
  3. Escreverei uma função para acessar e definir o volume usando o Registro do Pocket PC, dado um valor RMS médio.

Você pode usar algumas bibliotecas e controles ActiveX® para acessar o microfone e gravar o áudio de um dispositivo Pocket PC. Meu exemplo se baseia na biblioteca P/Invoke (consulte msdn2.microsoft.com/aa446550.aspx), que inclui uma classe WaveIn para o acesso ao microfone de um dispositivo. Essa classe contém uma estrutura capaz de armazenar amostras a uma determinada taxa e resolução de bits. A biblioteca P/Invoke também oferece uma classe auxiliar chamada Wave, que é uma interface comum ao formato de áudio .wav. O link anterior fornece uma lista de todas as classes e funções implementadas nas classes WaveIn e Wave originais.

A versão da biblioteca P/Invoke incluída em minha solução só contém as classes WaveIn, Memory e Registry. Todas as outras foram removidas para sermos mais breves. A biblioteca mostrada na Figura 2 foi estendida pelo fornecimento de funções extras: uma em WaveIn (TooLoud) £ AutoSetVolume £ e outra em Wave £ CalcRMS.

Figura 2 Parte da biblioteca P/Invoke exibida com o diagrama de classes de TooLoud
Figura 2 Parte da biblioteca P/Invoke exibida com o diagrama de classes de TooLoud(Clique na imagem para diminuir a exibição)
Figura 2 Parte da biblioteca P/Invoke exibida com o diagrama de classes de TooLoud
Figura 2 Parte da biblioteca P/Invoke exibida com o diagrama de classes de TooLoud (Clique na imagem para aumentar a exibição)

O aplicativo executa AutoSetVolume, que chama CalcRMS como parte de seu procedimento de definição do volume. TestProc pode ser chamado repetidamente ou somente uma vez, dependendo do modo selecionado na interface do usuário. TestProc chama seis funções:

  • AutoSetVolume é usado para atingir o objetivo final.
  • NumDevices obtém o número de dispositivos de entrada.
  • GetDeviceName obtém o nome do dispositivo de gravação especificado.
  • Preload pré-carrega os buffers para um determinado tamanho e valor máximo de áudio em milissegundos (Start inicia o tempo de gravação, atualmente definido como um segundo, e Stop interrompe a gravação).
  • AutoSetVolume calcula a nova configuração de volume.
  • Dispose limpa os buffers.

Preload configura uma instância de um waveformatex, um cabeçalho para a descrição dos dados com a marca de formatação PCM (modulação por código de pulso), resolução da amostra de 8 bits, um canal único e uma taxa de amostragem de 11025 Hz.

Agora que posso obter blocos de dados de áudio por um período, poderei usar esses dados no cálculo de um valor RMS. O cálculo do valor eficaz é definido na Figura 3.

Figura 3 Parte do cálculo da intensidade
Figura 3 Parte do cálculo da intensidade(Clique na imagem para diminuir a exibição)
Figura 3 Parte do cálculo da intensidade
Figura 3 Parte do cálculo da intensidade (Clique na imagem para aumentar a exibição)

O código correspondente, mostrado na Figura 4, agora pode ser apresentado para definir o método de cálculo dos valores RMS usando os valores de amostra obtidos com o microfone. Como os valores de amostra foram armazenados em um formato de bytes (0-255), o valor médio 127,5 será subtraído para oferecer sinais de áudio normalizados de -127,5 a +127,5.

Figure 4 Cálculo do RMS (Close Figure 4)
public double CalcRMS(MainTest.DisplayLineDelegate ShowLine)
{
    byte[] data = new byte[dwBytesRecorded];
    Marshal.Copy(lpData, data, 0, data.Length);
    double temp = 0, sum = 0;
    int j = 0;

    ShowLine(data.Length.ToString());

    for (int i = 0; i < data.Length; i++)
    {
        temp = (double)data[i] - 127.50;
        sum = sum + (temp * temp);
        j++;
    }
    return Math.Sqrt(sum / j);
 }

A função de mapeamento obtém o valor RMS e define o valor que será usado na definição do nível do toque atual do Pocket PC. Embora isso possa ser feito mais empiricamente, um perfil simples dos recursos de um celular servirá para a demonstração.

Como discutido, a desvantagem dessa técnica é que provavelmente será preciso haver um perfil para cada modelo de telefone que tenha recursos de alto-falante e de microfone significativamente diferentes. Você pode resolver esse problema criando uma interface de configuração que permita ao usuário definir valores de referência para vários locais, ou poderia oferecer um site que forneceria diversos perfis. Aqui, só incluirei o meio de inserir um conjunto de valores de referência. No entanto, com isso em mente, mostrarei como você pode obter os valores de mapeamento para que eles possam ser reproduzidos para qualquer perfil de telefone.

Os valores RMS foram gerados pela gravação de alguns ambientes comuns associados a certos níveis de dB. De preferência, isso deve ser feito ao longo de um período substancial (no caso de alguém, digamos, derrubar um prato na cafeteria no exato momento em que você estiver obtendo a amostra) e deve incluir os ambientes típicos onde o telefone será usado. A leitura média de cada ambiente poderá ser usada então como os limites superiores e inferiores da configuração do volume.

Após o cálculo dos valores de referência, um perfil adaptativo deverá ser criado. Ele simplesmente define como o celular adaptará seu nível de toque de acordo com os valores. Existem diversas estratégias diferentes que você pode considerar. Você poderia, por exemplo, até mesmo ativar a função de vibração caso o telefone estivesse em um ambiente muito barulhento. Em minha implementação, eu simplesmente farei com que os valores de referência mapeiem a entrada do microfone nos cinco volumes.

O nível do toque de um Pocket PC pode ser definido pelo Registro. A chave do Registro está em HKEY_CURRENT_USER, sob ControlPanel\SoundCategories\Ring, e o valor a ser alterado é InitVol, que possui um intervalo de 0 a 5.

O código da Figura 5 pode ser usado na definição dos valores do volume do toque por meio das funções padrão de gravação do Registro. O mapeamento decide qual é o valor correto para o toque dado um limite superior e inferior, implementado como diversas instruções condicionais.

Figure 5 Configurando o nível do toque atual (Close Figure 5)
public Wave.MMSYSERR AutoSetVolume(MainTest.DisplayLineDelegate ShowLine)
{
    double AverageRMS = 0.00;
    if (!m_inited)xreturn Wave.MMSYSERR.ERROR;

    if (m_recording) Stop();

    try
    {
        // Write the data recorded to each buffer.
        // Get RMS value for each block and take the average
        // across all blocks.
        for (int i = 0; i < m_numBlocks; i++)
        {
            if (m_whdr[i] != null)
            {
                AverageRMS = AverageRMS + m_whdr[i].CalcRMS(ShowLine);
            }
        }

        AverageRMS = AverageRMS / m_numBlocks;
        ShowLine("Average RMS: " + AverageRMS);

        // Select volume based on reference values.
        uint Volume = 0;
        if (AverageRMS > 35.00)
        {
            //Range of this value for the volume control is from
            //0x0000 (min) non to 0x0005 (max).
            Volume = 5;
        }
        else if ((AverageRMS <= 35.00) && (AverageRMS > 30.00))
        {
            //Range of this value for the volume control is from
            //0x0000 (min) non to 0x0005 (max).
            Volume = 4;
        }
        else if ((AverageRMS <= 30.00) && (AverageRMS > 20.00))
        {
            //Range of this value for the volume control is from
            //0x0000 (min) non to 0x0005 (max).
            Volume = 3;
        }
        else if ((AverageRMS <= 20.00) && (AverageRMS > 10.00))
        {
            //Range of this value for the volume control is from
            //0x0000 (min) non to 0x0005 (max).
            Volume = 2;
        }
        else
        {
           //Range of this value for the volume control is from
           //0x0000 (min) non to 0x0005 (max).
            Volume = 1;
        }

        ShowLine("Set Volume to: " + Volume.ToString());
        PInvokeLibrary.Registry.CreateValueDWORD(
            "ControlPanel\\SoundCategories\\Ring", "InitVol",
            Volume);

        return Wave.MMSYSERR.NOERROR;
    }
    catch { return Wave.MMSYSERR.ERROR; }
    finally { FreeWaveBuffers(); }
}

Como lidar com a instalação

Agora que já criei o código do aplicativo TooLoud, devo escolher entre várias opções diferentes de criação de instaladores para o Pocket PC. Você pode transferir o aplicativo usando o Windows Vista® Mobile Device Center se estiver impaciente e quiser brincar com ele imediatamente £ foi isso que eu fiz. No entanto, isso não é o ideal para o usuário final. Outra abordagem é criar um arquivo CAB e um instalador MSI (Microsoft Installer) que pode ser executado do PC para configurar o aplicativo no Pocket PC. Depois da geração do arquivo CAB, você pode usar um programa que permita gerar rapidamente um arquivo de instalação completo com licença de usuário final. Mas talvez você prefira criar um instalador personalizado para lidar com a implantação no celular a partir do desktop, oferecendo técnicas mais sofisticadas. O final desta seção examinará a criação de um instalador personalizado, de um arquivo CAB de dispositivo inteligente e de um projeto de implantação usando o Visual C# 2005. Se você tiver o Visual Studio 2003, poderá encontrar um guia para a implementação de funcionalidade similar em msdn2.micro- soft.com/ms838273.

O projeto completo, disponível para download no site da MSDN® Magazine, contém quatro componentes principais: o aplicativo TooLoud, o construtor SmartDeviceCab, CustomInstaller e o instalador Setup.

A criação de um CAB de dispositivo inteligente é simples no Visual Studio 2005. Para adicionar um projeto de CAB de dispositivo inteligente à solução, basta clicar nela no Solution Explorer, selecione Add | New Project, Other Project Types | Setup and Deployment e o modelo Smart Device CAB Project e clique em OK. No Solution Explorer, selecione o projeto de Cab de dispositivo inteligente recém-criado e adicione a saída do aplicativo TooLoud ao projeto principal.

Existem diversas propriedades que podem ser definidas no projeto SmartDeviceCab; a Figura 6 mostra a página de propriedades no Visual Studio 2005. Embora algumas propriedades sejam simples, outras não o são. Por exemplo, a propriedade OSversionMin define se o aplicativo oferecerá suporte ao reconhecimento de orientação de tela se for definida como 4.21. No entanto, esse valor impedirá que o aplicativo seja instalado em Pocket PCs com o Windows Mobile 2003 e anteriores. Para soluções de Pocket PC baseadas no Windows® 2003SE e anteriores, a propriedade Compress e a propriedade de implantação de dispositivo NoUninstall deverão ser falsas (observe que, se você tiver o .NET Compact Framework 2.0 instalado, a última propriedade poderá ser definida como True).

Figura 6 Propriedades de SmartDeviceCab
Figura 6 Propriedades de SmartDeviceCab(Clique na imagem para diminuir a exibição)
Figura 6 Propriedades de SmartDeviceCab
Figura 6 Propriedades de SmartDeviceCab (Clique na imagem para aumentar a exibição)

A DLL de instalação do CE especifica o arquivo que executará operações depois da instalação, como a configuração do Registro ou a instalação do aplicativo como um serviço. O segundo caso é útil, por exemplo, se você quiser que o aplicativo seja executado em segundo plano como um serviço, tomando decisões inteligentes sobre essa execução.

Após a criação do arquivo CAB, ele será capaz de implantar o aplicativo em um celular, mas ainda há o problema de como disparar a sua cópia e a sua instalação no celular. Dessa forma, criarei um projeto de implantação que instalará o arquivo CAB no desktop do sistema para depois disparar o gerenciador de aplicativos (consulte a Figura 7). O gerenciador de aplicativos, executado no Windows Mobile Device Center, é o programa do seu desktop que controla os aplicativos instalados no celular.

Figura 7 O Mobile Device Center e o Gerenciador de Aplicativos
Figura 7 O Mobile Device Center e o Gerenciador de Aplicativos(Clique na imagem para diminuir a exibição)
Figura 7 O Mobile Device Center e o Gerenciador de Aplicativos
Figura 7 O Mobile Device Center e o Gerenciador de Aplicativos (Clique na imagem para aumentar a exibição)

O meu projeto de implantação é igual ao projeto de implantação no desktop contido nos modelos do Visual Studio. No entanto, devo estender a funcionalidade para incluir a tarefa de lançamento do gerenciador de aplicativos para instalar o arquivo CAB. Posso fazer isso ao criar uma classe de instalador personalizado para estender o instalador normal. A classe do instalador personalizado substitui dois dos métodos da classe base, AfterInstall e AfterUninstall, e adiciona um método para abrir o gerenciador de aplicativos. É possível localizar o gerenciador de aplicativos acessando um valor do Registro na máquina desktop.

A Figura 7 mostra o gerenciador de aplicativos (a janela em primeiro plano) aberto pelo meu instalador personalizado. O código do meu instalador personalizado é mostrado na Figura 8.

Figure 8 Código do instalador personalizado (Close Figure 8)
private void Installer_AfterInstall(
    object sender, System.Configuration.Install.InstallEventArgs e)
{
    // Get fullpath to .ini file.
    string arg = Path.Combine(
        Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
        "Setup.ini");

    // Run WinCE App Manager to install .cab file on device.
    RunAppManager(arg);
}

private void Installer_AfterUninstall(
    object sender, System.Configuration.Install.InstallEventArgs e)
{
    // Run app manager in uninstall mode (without any arguments).
    RunAppManager(null);
}

private void RunAppManager(string arg)
{
    // Get path to the application manager.
    const string RegPath = @"Software\Microsoft\Windows\" +
        @"CurrentVersion\App Paths\CEAppMgr.exe";
    RegistryKey key = Registry.LocalMachine.OpenSubKey(RegPath);
    string appManager = key.GetValue("") as string;

    if (appManager != null)
    {
        // Launch the application manager.
        Process.Start(string.Format("\"{0}\"", appManager),
            (arg == null) ? "" : string.Format("\"{0}\"", arg));
    }
    else
    {
        // Could not locate application manager.
      MessageBox.Show(
          "Could not launch the WinCE Application Manager :-(");
    }
}
Figura 9 A solução completa
Figura 9 A solução completa

Agora o projeto de implantação será criado da forma usual, pela adição de um projeto padrão de instalação em desktop à solução. Depois da adição do modelo de implantação, adiciono a custominstaller.dll e o smartdevicecab.cab ao projeto de instalação. Para usar como referência o instalador personalizado, clico com o botão direito do mouse no projeto de instalação e seleciono View | Custom Actions. Em seguida, adiciono a custominstaller.dll às ações personalizadas Install e Uninstall do projeto de instalação.

O meu projeto de implantação precisa de um último arquivo £ setup.ini:

[CEAppManager]
Version      = 1.0
Component    = App

[App]
Description  = TooLoud Pocket PC
    application.
CabFiles     = SmartDeviceCab.cab

Isso mostra ao gerenciador de aplicativos qual é o CAB a ser carregado. A Figura 9 mostra o layout da minha solução (a ordem de compilação deve ser definida como TooLoud £ SmartDeviceCab £ CustomInstaller £ Setup). Agora o meu projeto pode ser compilado e implantado por meio de uma máquina desktop que contenha um gerenciador de aplicativos.

Resultados

Este artigo descreve um exemplo específico que escrevi para o Visual Studio 2005 usando a versão 1.x do .NET Compact Framework. Ele demonstra os princípios subjacentes e as técnicas gerais envolvidos na criação de um aplicativo que usará o hardware de um dispositivo com Windows Mobile para monitorar o ambiente e ajustar automaticamente o volume do toque de forma apropriada. É claro que esse código pode ser decomposto novamente para poder utilizar as tecnologias mais recentes, como a versão 2.0 do .NET Compact Framework.

Para ver como o meu aplicativo está funcionando, fiz testes em laboratório e testes ambientais. O teste em laboratório serviu para garantir que o telefone só tocaria de 2 a 5 dB mais alto do que o ambiente (6 dB é aproximadamente duas vezes mais alto). Os valores em dB foram medidos por um medidor de dB com um tom como o sinal de teste. O teste ambiental foi feito em dois locais £ em uma cafeteria e em um aeroporto de Los Angeles. Usei uma medida bastante científica nesse teste £ contei o número de pessoas que se viraram quando o meu telefone tocou.

O aplicativo oferece uma abordagem de som para o monitoramento e adaptação do nível do toque de um celular. Assim como todos os aplicativos, ele tem suas limitações, como a necessidade de configuração dos valores de referência para cada modelo de celular. Embora essa não seja uma tarefa impossível, a interface do usuário atual não oferece suporte a ela. A outra grande limitação é que essa abordagem consome muita energia. No entanto, isso pode ser facilmente resolvido, digamos, se ativarmos o processo somente quando o telefone estiver para tocar.

A não ser por esses problemas, o aplicativo faz muito sentido £ em minha opinião, mais do que a abordagem atual de termos níveis de toque que só podem ser alterados manualmente. Ainda há alguns aperfeiçoamentos a serem feitos e, portanto, sinta-se à vontade para adaptar o código e adicionar qualquer funcionalidade que considere útil. Tenho certeza de que você poderá descobrir um monte de recursos e funções que não foram discutidas aqui.

Gostaria de agradecer a Brian Richardson pelo trabalho gráfico do início do artigo.

Chris Mitchell concluiu seu doutorado em aprendizado de máquina e processamento de sinais de música/som e atualmente é membro Kauffman/NCGE. Foi conferencista em tempo parcial na Anglia Ruskin University de Cambridge, no Reino Unido, para ajudar a financiar os estudos, além de prestar consultoria em redes. Você pode entrar em contato com Chris pelo endereço c.mitchell@anglia.ac.uk. A paixão por tecnologia musical o levou a explorar alguns dos aspectos fundamentais da programação de áudio e ele sempre fica satisfeito em ouvir algo sobre esse assunto.


Nota do editor: A complexidade da complexidade

Nota do editor
A complexidade da complexidade
Howard Dierking

Como desenvolvedores, parece que estamos sob uma pressão cada vez maior para gerenciar as novas dimensões da complexidade. Somos solicitados a criar e a manter sistemas que dêem suporte a regras e processos comerciais que se tornam cada vez mais complicados progressivamente. Somos solicitados a criar experiências de usuário dinâmicas, interativas e integradas em uma grande variedade de mídias. E, como sempre, enfrentamos a pressão de compreender e aproveitar as nuances dos melhores e mais recentes projetos de software.

Qual é a solução para essa complexidade cada vez maior? Se a história puder ensinar alguma coisa, a solução será criar novas metáforas e abstrações. As metáforas permitem que usemos a linguagem e a experiência existentes para falar sobre assuntos específicos de maneira geral. Da mesma forma, a abstração facilita um projeto melhor, ao ocultar detalhes que atrapalhariam a nossa capacidade de efetivamente separar elementos uns dos outros. Por fim, a abstração permite que os especialistas em tecnologia criem elementos de software com dependências mínimas em outros elementos, enquanto a metáfora possibilita que generalistas ou especialistas em diferentes áreas analisem e apliquem esses elementos com inteligência para criar soluções elegantes.

Comecei a trabalhar há pouco tempo na MSDN® Magazine, depois de passar grande parte da minha carreira criando e desenvolvendo softwares para vários setores diferentes. E eu tenho certeza de que você consegue imaginar quantas partes trabalham no processo de publicação de uma revista – e, para um novato, é fácil se sentir sobrecarregado pela complexidade.

No entanto, assim como nos meus dias de desenvolvedor, descobri que a abstração e a metáfora são imprescindíveis para que tenha uma forma estruturada por meio da qual eu aprendo. Por exemplo, os detalhes dos processos editorial e de produção são efetivamente abstratos um em relação ao outro – os editores não precisam conhecer toda a mecânica de criação de um layout, e o pessoal da produção não precisa se preocupar com a forma como os manuscritos foram obtidos e processados. Metáforas como "jogar" descrevem o processo de transferência do texto editado para o layout. De qualquer forma, é bom saber que algumas das habilidades que adquiri como desenvolvedor não são totalmente inúteis no negócio das revistas!

Nesta edição da MSDN Magazine, exploramos como a complexidade que você talvez esteja enfrentando em tarefas diárias de desenvolvimento de software está sendo abordada por meio de novas ferramentas e metáforas. Para dar suporte a exigências comerciais cada vez mais complexas, Bill Horst aborda como a interferência de tipos aumenta o nível de abstração em código do sistema de tipos CLR até próximo do domínio da empresa. Bill Chiles leva essa noção de intencionalidade na programação adiante em meio à sua análise do DLR (Dynamic Language Runtime). Para possibilitar experiências de usuário mais avançadas, Dino Esposito estende a metáfora do serviço até os padrões da apresentação na Web com a segunda parte de sua coluna sobre o ASP.NET AJAX. Daan Leijen e Judd Hall descrevem como a biblioteca Parallel FX abstrai detalhes específicos de baixo nível relacionados à forma de aproveitamento das inovações em hardware como, por exemplo, vários processadores e processadores com vários núcleos. Por fim, Joe Duffy e Ed Essey descrevem como as metáforas de linguagem existentes estão sendo estendidas para dar suporte a essas abstrações em Parallel LINQ.

Ao final, a criação bem-sucedida de novas metáforas e abstrações certamente acarretarão novos níveis de complexidade e a necessidade de novas metáforas e abstrações. No entanto, a cada nova camada de abstração, a extensão dos problemas que somos capazes de resolver por meio de software cresce na mesma proporção. Então, que metáfora devemos criar em relação à MSDN Magazine? Eis algumas idéias: show de rock, martelo, espelho, globo espelhado.

E quanto a você? Envie um e-mail com suas idéias – quem sabe? A sua idéia poderia ajudar a formar idéia principal em torno da revista para os próximos anos! £Howard

Thanks to Obrigado aos seguintes especialistas técnicos da Microsoft pela ajuda nesta edição: Mahesh Prakriya, Kit George, Umachandar Jayachandran, Rob Earhart, Eric Li, Sandeep Ranade, Rob Shewan, Judd Hall, Ed Essey, Vance Morrison, JJ Johnston, Daniel Moth, Mike Hall, Jordan Parker, David Aiken, Joe Duffy e Stephen Toub.


Toolbox: Gerencie bancos de dados, FTP mais f&#225;cil e caches agrupados

Toolbox
Gerencie bancos de dados, FTP mais fácil e caches agrupados
Scott Mitchell

Conteúdo
Gerencie os projetos de bancos de dados de maneira efetiva
FTP mais fácil
Próximo nível do cache

Gerencie os projetos de bancos de dados de maneira efetiva

Há muitas ferramentas disponíveis para gerenciar bancos de dados, explorar e modificar esquemas, além de escrever e executar consultas. Uma dessas ferramentas comerciais que vale a pena analisar é o DatabaseSpy 2007 da Altova. O DatabaseSpy pode se conectar a vários armazenamentos de dados diferentes –Microsoft® Access®, Microsoft SQL Server™, Oracle, MySQL, Sybase, etc. – e oferece uma experiência do usuário uniforme, independentemente do banco de dados.

Durante a primeira a conexão com um banco de dados por meio do DatabaseSpy, um novo projeto é criado automaticamente. Os projetos funcionam como repositórios para detalhes da conexão, credenciais, consultas mais usadas etc. O conteúdo de um projeto pode ser pesquisado rapidamente, e os elementos mais usados podem ser armazenados na pasta Favorites (Favoritos). Além disso, os menus e as barras de ferramentas do gerente do projeto podem ser personalizados e configurados para iniciar ferramentas externas.

O SQL Editor do DatabaseSpy também oferece um editor de consultas moderno. Para iniciantes, o recurso Autocomplete (AutoCompletar) confere ao editor um comportamento semelhante ao do IntelliSense®. Durante a digitação de uma consulta, uma lista suspensa com possíveis palavras-chave SQL, funções e nomes de tabela é exibida. Durante a execução de uma consulta, os resultados são exibidos em uma grade. No entanto, diferentemente da grade de resultados do SQL Server Management Studio, a grade do DatabaseSpy pode ser classificada com um clique no cabeçalho de uma coluna. O SQL Editor também inclui ferramentas que exportam dados para vários formatos, inclusive XML, CSV, HTML e Excel®.

Além disso, o DatabaseSpy inclui um Design Editor com gráficos avançados que permite aos desenvolvedores criar e modificar tabelas com apenas alguns pontos e cliques. Enquanto a tabela está sendo criada ou modificada, o DatabaseSpy mantém um script de alteração da estrutura do banco de dados e o exibe na tela. Essas informações são úteis na verificação de quais alterações específicas foram feitas e na aplicação dessas mesmas alterações a outro banco de dados.

Caso você esteja procurando as melhores ferramentas para gerenciar bancos de dados e executar consultas, reserve um tempo para explorar opções de terceiros alternativas como, por exemplo, o DatabaseSpy 2007.

Preço: $129

www.altova.com/DatabaseSpy

O Design Editor do DatabaseSpy ajuda você a criar e a modificar tabelas
O Design Editor do DatabaseSpy ajuda você a criar e a modificar tabelas (Clique na imagem para diminuir a exibição)
O Design Editor do DatabaseSpy ajuda você a criar e a modificar tabelas
O Design Editor do DatabaseSpy ajuda você a criar e a modificar tabelas (Clique na imagem para aumentar a exibição)

FTP mais fácil

O FTP é um protocolo bastante conhecido e aberto para trocar arquivos entre um servidor e um cliente em uma rede. Os navegadores da Web são um dos aplicativos clientes FTP disponíveis mais conhecidos e mais usados, mas os recursos do FTP e a experiência do usuário – embora direta – normalmente não têm os recursos mais avançados encontrados na maioria dos aplicativos clientes FTP autônomos.

Um desses aplicativos clientes FTP autônomos é o FileZilla (versão 2.2), um projeto de código aberto iniciado por Tim Kosse. Iniciar o FileZilla exibe o Site Manager (Gerenciador de sites), que armazena as informações sobre a conta do servidor FTP. Aqui é possível adicionar e categorizar os servidores FTP que você mais acessa, armazenando as credenciais de autenticação, as opções de conexão e as demais informações sobre a configuração. O FileZilla dá suporte à implementação FTP nativa – que não é protegida, por enviar credenciais pela rede em texto sem formatação –, bem como às duas implementações seguras padronizadas: FTP em SSL e SFTP.

Uma vez conectado a um site FTP, a interface do usuário do FileZilla consiste em quatro painéis. O primeiro lista os comandos FTP enviados para o servidor FTP e as respostas correspondentes. Essas informações de baixo nível são muito úteis para desenvolvedores e outros usuários experientes durante a solução de problemas. O segundo painel lista os arquivos e as pastas do computador local, e o terceiro lista arquivos e pastas do servidor FTP. Para carregar arquivos ou pastas de um local para outro, basta selecionar os itens em um painel e arrastá-los para o outro painel. A atividade atual e a lista pendente de ações são listadas na fila de transferências, exibida no painel inferior.

Além do aplicativo cliente, o projeto FileZilla inclui uma implementação gratuita de servidor FTP com código aberto.

Preço: gratuito; código-fonte disponível (Visual C++)

sourceforge.net/projects/filezilla

Como ferramenta de diagnóstico, FileZilla lista comandos e respostas FTP
Como ferramenta de diagnóstico, FileZilla lista comandos e respostas FTP (Clique na imagem para diminuir a exibição)
Como ferramenta de diagnóstico, FileZilla lista comandos e respostas FTP
Como ferramenta de diagnóstico, FileZilla lista comandos e respostas FTP (Clique na imagem para aumentar a exibição)

Próximo nível do cache

O cache é uma técnica normalmente usada para aprimorar o desempenho do aplicativo copiando os dados mais usados de uma fonte de dados lenta para uma mais rápida. Para aplicativos controlados por dados, isso normalmente envolve o cache de dados recuperados de bancos de dados ou Web services na memória do computador local.

É mais simples de implementar o cache quando ele é específico de cada aplicativo, embora se torne muito mais desafiador caso muitos aplicativos precisem usar um mesmo cache. Por exemplo, grandes sites costumam usar um farm de servidores que consiste em vários computadores todos oferecendo o mesmo conteúdo. Assim que cada solicitação chega, ela é atribuída a um dos computadores no farm. No entanto, caso sejam armazenadas em cache na memória de um computador do farm, as informações não podem ser acessadas pelo cache de outro, o que reduz a eficiência do cache. Uma solução para esse problema é mover o cache para um armazenamento de dados centralizado como, por exemplo, um banco de dados ou designar um dos servidores do farm como sendo o servidor de cache, que armazena a única cópia do cache. O problema dos armazenamentos de cache centralizados é que eles introduzem um único ponto de falha, o que pode funcionar como um afunilamento.

Para superar as limitações de um único armazenamento de cache centralizado, considere o uso do NCache 3.1, da Alachisoft. O NCache é uma estrutura de caches agrupados que gerencia tranqüilamente um cache distribuído em vários computadores. Um administrador começa especificando as topologias de grupo e as políticas de cache por meio da ferramenta NCacheManager. O administrador pode especificar se o cache é armazenado na memória ou no disco, as políticas de remoção e o tamanho máximo do cache, dentre outras configurações; essas configurações, bem como a topologia, podem ser modificadas no runtime. Além disso, o NCacheManager inclui uma matriz das estatísticas que podem ser usadas para monitorar a integridade do cache e as métricas para ajustar as configurações do cache tendo em vista o desempenho ideal.

Durante a definição de um grupo de caches, é possível especificar se o cache deve ser replicado ou particionado em todo o grupo. Um grupo de caches replicados tem o mesmo conteúdo de cache em todas as máquinas do grupo. Como não há um único ponto de falha, um cache replicado aumenta a confiabilidade; no entanto, qualquer atualização feita no cache deve ser replicada para todos os grupos. Por isso, os grupos replicados são ideais para caches usados apenas para leitura ou pequenos. Já um cache particionado divide o conteúdo com os computadores do grupo e possibilita um melhor desempenho para atualizações e grupos grandes.

Ao avaliar estruturas como o NCache, eu sempre me interesso pela curva de aprendizado e pela facilidade de integração. É claro que levará tempo para dominar a ferramenta NCacheManager e compreender plenamente as várias definições de configuração, mas integrar a API do NCache aos aplicativos é um processo bastante simples. As classes na API do Ncache correspondem às classes da API de cache do Microsoft .NET Framework e incluem módulos HTTP que tornam a atualização em um aplicativo ASP.NET tão simples quanto a atualização do seu arquivo de configuração. O NCache também inclui um provedor de integração com o CAB (Caching Application Block) Microsoft.

Resumindo, aprimorar o cache dos seus aplicativos é um processo relativamente simples: o código existente pode ser atualizado usando o NCache com poucas ou nenhuma alteração feita no código.

Preço: 995 dólares por CPU para a Standard Edition.

www.alachisoft.com

Especifique políticas de cache usando o NCacheManager
Especifique políticas de cache usando o NCacheManager (Clique na imagem para diminuir a exibição)
Especifique políticas de cache usando o NCacheManager
Especifique políticas de cache usando o NCacheManager (Clique na imagem para aumentar a exibição)

Envie suas dúvidas e comentários ao Scott para toolsmm@microsoft.com.

Scott Mitchell, autor de diversos livros e fundador da 4GuysFromRolla.com, é um MVP que trabalha com tecnologias Web da Microsoft desde 1998. Scott é consultor autônomo, instrutor e escritor. Para falar com ele, envie um email para Mitchell@4guysfromrolla.com ou entre no blog ScottOnWriting.NET.

Todos os preços foram confirmados no momento da impressão e estão sujeitos a alterações. As opiniões expressas nesta coluna são exclusivas do autor e não refletem necessariamente as opiniões da Microsoft.

MSDN Magazine does not make any representation or warranty, express or implied with respect to any code or other information herein. MSDN Magazine disclaims any liability whatsoever for any use of such code or other information.


Tudo sobre o CLR: IronPython e o runtime de linguagem din&#226;mico

Tudo sobre o CLR
IronPython e o runtime de linguagem dinâmico
Bill Chiles

Conteúdo
Hello World em IronPython
Iniciando Ipy.exe
Introdução a ASTs
Observando a AST de IronPython
Observando a AST de DLR
Observando o código gerado
Compreendendo locais dinâmicos
Como os objetos DynamicSite funcionam
O grande benefício

O
Microsoft® .NET Framework foi projetado para dar suporte a uma ampla gama de linguagens de programação em um único runtime – o CLR (Common Language Runtime). O CLR oferece serviços compartilhados a essas linguagens, inclusive a coleta de lixo, a compilação JIT (just-in-time), um modelo de segurança protegido e o suporte à integração de ferramentas. Compartilhar esses recursos proporciona duas grandes vantagens aos implementadores de linguagem: é mais fácil implementar uma linguagem porque grande parte do trabalho de engenharia de nível inferior já foi feita; e as linguagens em execução no CLR podem ser integradas tranqüilamente. Compartilhar as bibliotecas e as estruturas permite que novas linguagens do CLR sejam criadas a partir do trabalho de outras.

O CLR também inclui o suporte a linguagens dinâmicas, como demonstra a IronPython 1.0 (www.codeplex.com/ironpython). As linguagens de programação dinâmicas dão mais eficiência e produtividade aos programadores, como explicou James Schementi no artigo publicado em outubro de 2006 "Tudo sobre o CLR" (msdn.microsoft.com/msdnmag/issues/06/10/CLRInsideOut). Agora a Microsoft está criando o DLR (Dynamic Language Runtime), que agrega ao CLR um conjunto de serviços projetados especificamente para atender às necessidades das linguagens dinâmicas. O DLR agrega funcionalidades como, por exemplo, um sistema de tipos dinâmico compartilhado, um modelo de hospedagem padrão e suporte para facilitar a geração rápida de códigos dinâmicos e tabelas de símbolos rápidos. Com esses recursos adicionais, é muito mais fácil criar implementações de linguagem dinâmica de alta qualidade para o .NET. Além disso, esses recursos permitem que as linguagens dinâmicas criadas a partir do DLR compartilhem bibliotecas escritas em outras linguagens dinâmicas ou em linguagens estáticas baseadas no CLR.

O DLR permite que você crie a melhor experiência para a sua linguagem dinâmica favorita. Isso significa que a experiência de codificação em sua linguagem deve ser a de sempre. Você deve se beneficiar de ferramentas excelentes, ótimo desempenho e bibliotecas compartilhadas. Como usar o DLR significa ser possível compartilhar a funcionalidade padrão, você tem mais tempo para se concentrar nos recursos que tornam a linguagem única. Por exemplo, é possível evitar precisar implementar a coleta de lixo, a geração de códigos e o cache de métodos.

O DLR usa o cache de métodos adaptivo para produzir programas dinâmicos rápidos, e todas as implementações de linguagem no DLR se beneficiam desse trabalho compartilhado. O código é executado mais rapidamente, evitando-se buscas repetidas de listas de precedências de classe. Isso também impede sobrecargas de resolução em métodos do .NET sempre que você chama um objeto (por exemplo, um método, foo, em um objeto, o).

O código-fonte de IronPython e DLR está disponível em Codeplex, sob a Licença Permissiva Microsoft ao estilo BSD (Berkeley Software Distribution) (consulte a versão alfa 2.0). Como se trata de uma versão inicial, ainda há muito trabalho significativo a ser feito. Há mecanismos de nível inferior a serem projetados, aprimoramentos de desempenho a serem implementados e documentação a ser escrita. Quando você estiver lendo isto, já haverá projetos em Codeplex disponíveis para o próprio código do DLR e para IronRuby, além de IronPython. Embora estejamos trabalhando em implementações do DLR JScript® e do VBX, elas talvez estejam disponíveis apenas na forma binária.

Hello World em IronPython

Examinarei um programa simples em Python (uma variação de Hello World) e abordarei o que acontece quando ele é executado em ipy.exe. O programa que examinarei é msdnmag.py:

def yo(yourname):
   text = "hello, "
   return text + yourname

print yo("bill")

Como eu acrescentei algo que vai um pouco além da clássica funcionalidade "print hello", posso abordar aspectos mais interessantes da implementação em IronPython. Por exemplo, poderei demonstrar a ligação do nome para referências do identificador e armazenar o significado de "+" na função yo.

Como estou usando o IronPython 2.0 alfa, reserve alguns minutos para baixá-lo e depois examiná-lo comigo.

Para começar, abra o arquivo ironpython.sln no Visual Studio® 2005 e crie a solução usando a configuração da solução Depuração. Em seguida, clique com o botão direito do mouse no projeto IronPythonConsole e selecione a guia Depurar na barra de navegação à esquerda. Defina este como sendo o projeto inicial (primeiro conjunto de botões de opção em Ação Inicial). Em seguida, em Opções Iniciais, insira msdnmag.py como argumento da linha de comando. Por fim, copie o código Python msdnmag.py listado acima para um arquivo chamado msdnmag.py no diretório \bin\debug\ que foi criado quando você elaborou a solução.

Iniciando Ipy.exe

Agora, vejamos como ipy.exe é iniciado. O DLR fornece algumas classes de console padrão para que os implementadores da linguagem possam facilmente manter um interpretador em execução (o implementador ainda precisa escrever analisadores e uma implementação de runtime para a linguagem). As classes de console tratam entrada e saída, opções de linha de comando e algumas opções de diagnóstico compartilhado implementadas pelo DLR. Isso poupa os implementadores de linguagem de manter alguns códigos iniciais em execução no DLR, além de oferecer uma ferramenta de teste.

Para começar, abra <installdir>\Src\IronPython\Compiler\Generation\PythonScriptCompiler.cs. Em seguida, defina um ponto de interrupção na função ParseFile na linha "using (Parser parser = ...."

Use F5 (Debug | Start Debugging [Depurar | Iniciar depuração]) para executar o programa. Inicialmente, você encontrará o ponto de interrupção durante a análise de um trecho de código que inicializará o mecanismo de execução do Python. O trecho de código carrega o arquivo Python site.py de inicialização específica da máquina. Mas como você chegou até aqui? Se observar a pilha de chamadas, você verá que ipy.exe é iniciado na função Main de PythonConsoleHost, chamada no modo de conveniência compartilhado do DLR. A execução retorna ao código específico do IronPython para processar a linha de comando, o que causa a execução do arquivo msdnmag.py.

A classe PythonConsoleHost é muito pequena. Ela tem um método Initialize muito curto que define LanguageProvider (e, dessa forma, o mecanismo de execução) como sendo a implementação de IronPython. O método Main de PythonConsoleHost apenas é chamado na classe padrão ConsoleHost fornecida pelo DLR.

ConsoleHost é uma classe auxiliar que permite aos implementadores de linguagem criar rapidamente um shell de interação ao estilo cmd.exe. A classe ConsoleHost obtém o objeto auxiliar CommandLine de LanguageProvider. Quando ConsoleHost executa o objeto CommandLine, que chama o método Initialize do objeto CommandLine, o código é inicialmente executado no objeto PythonCommandLine.

Em seu método Initialize, o objeto PythonCommandLine configura o caminho do Python para o carregamento dos módulos, configura alguns módulos internos e importa o arquivo site.py de IronPython. Trata-se de um comportamento padrão do Python. PythonCommandLine importa o arquivo site.py chamando IScriptEngine.Execute na cadeia de caracteres do trecho do código "import site.py".

Você encontrará o ponto de interrupção inicialmente na chamada para PythonScriptCompiler.ParseFile por conta da análise de "import site.py", mas continuemos, pois msdnmag.py ainda não está em execução.

Introdução a ASTs

Quando ipy.exe executa msdnmag.py, as implementações de IronPython e DLR trabalham juntas na compilação do código no arquivo. Basicamente, os compiladores são pipelines iniciados com um analisador que produz uma estrutura de dados representando o código. Essa estrutura de dados é uma árvore de sintaxe abstrata, ou AST na linguagem dos compiladores. O compilador transforma a AST ou cria novas estruturas de dados enquanto percorre os estágios e analisa o código. Em seguida, com base nas estruturas resultantes, o compilador gera o código por computador ou o código de IL (linguagem intermediária) para uma máquina virtual como o CLR.

O código em IronPython primeiro gera uma AST específica de IronPython e, em seguida, mapeia essa árvore para uma AST do DLR. Algumas linguagens têm sua própria árvore intermediária para que possam realizar análises ou ferramentas de suporte (editores de códigos) para suas ASTs. As ferramentas precisam de uma árvore que esteja próxima do código-fonte digitado pelos usuários em um editor. Muitas linguagens talvez tenham ASTs que sejam semelhantes às do DLR, mas as do DLR terão mais informações sobre a semântica representada de maneira explícita. Você pode pensar nas ASTs do DLR como sendo uma das várias transformações das quais os estágios posteriores de um compilador precisam.

É interessante observar que o DLR compila lentamente partes das ASTs durante a execução do código. Isso é semelhante à forma com que o CLR usa a compilação JIT apenas na execução do código. Você perde um pouco mais em termos de desempenho na primeira vez em que o código é executado, mas as execuções subseqüentes são mais rápidas. Posteriormente, você verá que as partes da AST não são compiladas até a execução de linhas específicas do código.

Observando a AST de IronPython

Voltemos à execução do programa para que possamos observar a AST de IronPython. Primeiro, aperte F5 (Debug | Continue [Depurar | Continuar]) para parar novamente no ponto de interrupção quando o mecanismo do IronPython começar a analisar o conteúdo de site.py para executá-lo. Caso você examine a janela Locais do Visual Studio, é possível expandir "cc", "SourceUnit" e observar Name ou DisplayName para ver o que ParseFile está processando.

Aperte F5 novamente. Na última vez, paramos neste ponto de interrupção. Se observar a janela Locais agora, você verá que cc.SourceUnit.DisplayName é msdnmag.py. Trata-se do arquivo acrescentado por mim como um argumento da linha de comando a ipy.exe. Na janela Locais, cc.SourceUnit.Name é "__main__" porque o Python nomeia o módulo de execução principal dessa forma.

Agora, aperte F10 (Debug | Step Over [Depurar | Ignorar]) três vezes para ignorar a linha

ast = parser.ParseFileInput()

Agora é possível observar a AST de IronPython no depurador, como mostra a Figura 1.

Figura 1 AST de IronPython no depurador
Figura 1 AST de IronPython no depurador

É possível ver que o nó raiz da AST é SuiteStatement, o que representa uma série de instruções, além de ter um membro de Statements. Ao expandir a primeira instrução (índice zero), você vê que ela mantém um nó FunctionDefinition. Ela tem um membro Body que é SuiteStatement para as instruções de definição da função, além de ter um membro Name que será o nome a ser ligado ao objeto da função resultante.

De maneira ilustrada, com alguns detalhes omitidos, a AST de IronPython do arquivo msdnmag.py é semelhante à Figura 2.

Figura 2 Árvore de sintaxe abstrata de IronPython para msdnmag.py
Figura 2 Árvore de sintaxe abstrata de IronPython para msdnmag.py(Clique na imagem para diminuir a exibição)
Figura 2 Árvore de sintaxe abstrata de IronPython para msdnmag.py
Figura 2 Árvore de sintaxe abstrata de IronPython para msdnmag.py (Clique na imagem para aumentar a exibição)

Há uma mistura na representação visual da árvore. Em termos gerais, caso um nó da AST tenha um membro que aponte para outro nó da AST, a figura mostra uma seta apontando para o subnó. Normalmente, o subnó é nomeado como sendo o tipo de um membro no nó pai. Por exemplo, o nó FunctionDefinition tem um membro do tipo SuiteStatement, e o nó do texto é SuiteStatement. Às vezes, um nó contém uma descrição dos subnós que usa recuos e chaves. Eu optei por mostrar esses nós para simplificar o diagrama.

Observando a AST de DLR

Como agora o IronPython está pronto para criar a AST do DLR, os estágios finais da compilação podem ser executados. Abaixo do nosso ponto de interrupção em ParseFile é possível ver a chamada para BindAndTransform, que transforma a AST do IronPython em uma árvore do DLR. Durante esse processo, IronPython resolve todos os identificadores para que sua representação na árvore do DLR aponte para as informações de declaração/alocação das variáveis que elas representam. É claro que, para alguns identificadores, elas precisam ser compiladas em um nó da AST que represente a pesquisa de um valor no runtime; por exemplo, o identificador poderia ser resolvido para um membro do módulo adicionado ao runtime ou a um valor a ser ligado posteriormente e fornecido por um host por meio do dicionário do módulo.

Há muitos detalhes nesta seção que talvez só sejam interessantes caso você esteja procurando portar uma linguagem no DLR. Caso você veja apenas a figura da árvore do DLR, observe que há nós MethodCallExpression e ActionExpression. Os nós MethodCallExpression são compilados em chamadas para uma função de runtime do IronPython que realiza uma operação. Sempre que é chamada, essa função realiza o mesmo trabalho, dados os argumentos passados a ela.

O nó ActionExpression é especialmente interessante por representar uma operação ou ação abstrata. Entre as ações abstratas estão, por exemplo, buscar um membro, indexar um objeto, chamar um objeto, realizar adições em objetos etc. O DLR compila nós ActionExpression em locais dinâmicos de chamada. Locais dinâmicos são um mecanismo de runtime que armazena em cache métodos apropriados a combinações específicas de tipos de ações e argumentos. Eles permitem que o código dinâmico seja executado mais rapidamente com pouca ou nenhuma informação estática sobre o tipo no momento da compilação. Explicarei com mais detalhes os locais dinâmicos posteriormente.

Voltemos à execução do programa. Aperte Shift+F11 (Debug | Step Out [Depurar | Sair]) duas vezes para parar em SourceUnit.Compile e use F10 (Ignorar) para realizar a atribuição à variável "block", que manterá a raiz da AST do DLR de todo o arquivo.

Agora é possível observar a AST do DLR. Na janela Locais do Visual Studio, expanda o nó chamado block, que é um nó da AST de CodeBlock. Trata-se do nó raiz do arquivo de código do Python. Parte da tradução de uma árvore de sintaxe específica de uma linguagem na AST do DLR está designando claramente onde as variáveis são declaradas (explícita ou implicitamente) e a quais variáveis os identificadores se referem. Se expandir o membro variables do bloco, você verá duas variáveis declaradas no escopo do módulo, "__name__" e "yo". (Expanda os elementos 0 e 1 para ver os membros name do objeto Variable.) A primeira variável é um nome de ligação exigido e criado pelo Python para os módulos. A segunda é a variável module que mantém a definição da nossa função.

Antes de analisar o corpo, observe rapidamente uma expansão parcial da AST do DLR na Figura 3, que reflete uma representação semelhante à AST do IronPython que você viu anteriormente.

Figura 3 A AST do DLR
Figura 3 A AST do DLR(Clique na imagem para diminuir a exibição)
Figura 3 A AST do DLR
Figura 3 A AST do DLR (Clique na imagem para aumentar a exibição)

Expanda a propriedade Body no local do bloco. Por conta das características de exibição do depurador do Visual Studio, você precisa expandir o primeiro nó de Body para efetivamente ver o conteúdo de BlockStatement. O nó BlockStatement da AST do DLR corresponde ao nó SuiteStatement da AST do IronPython, e cada um representa uma série de instruções. Expanda a propriedade das instruções.

O primeiro elemento (índice 0) é EmptyStatement, que é um artefato da tradução do IronPython na árvore do DLR. O IronPython aloca duas instruções caso haja uma cadeia de caracteres da documentação do módulo, que causaria a criação de uma atribuição a __doc__. Nesse caso, o primeiro elemento permanece sendo um espaço reservado EmptyStatement, uma vez que não há nenhuma cadeia de caracteres da documentação.

A segunda instrução (índice 1) é o que importa porque se trata de BlockStatement do restante do arquivo, que é o código escrito por mim. BlockStatement contém ExpressionStatement seguido de BlockStatement (analisado posteriormente). O nó ExpressionStatement encapsula um nó BoundAssignment para que o valor resultante da atribuição seja indicado na pilha; as atribuições do Python não retornam valores. BoundAssignment tem membros name e value. Eles representam a definição da variável local yo (local no módulo em que o código do Python é executado) como o resultado de uma chamada de função. A chamada de função, representada por MethodCallExpression, chama uma função auxiliar do runtime que cria um objeto function do IronPython. MethodCallExpression tem membros que indicam que a chamada é para um método chamado MakeFunction e os argumentos obtidos pela chamada. Um argumento é um nó da AST de CodeBlockExpression para o corpo da função e outro, uma matriz dos parâmetros a serem usados pela função criada.

Expanda o nó da AST de MethodCallExpression, que é o membro value de BoundAssignment. Observe o terceiro elemento (índice 2) da propriedade Arguments, e você verá CodeBlockExpression do corpo de yo. Ele tem uma propriedade Block, que é CodeBlock. CodeBlockExpression faz com que o CodeBlock encapsulado se transforme em um valor que pode ser chamado e atribuído a uma variável module. Antes de abordarmos o corpo da AST da função yo, observe a Figura 4, que é um diagrama de toda a AST do DLR de msdnmag.py.

Figura 4 Toda a AST do DLR de msdnmag.py
Figura 4 Toda a AST do DLR de msdnmag.py(Clique na imagem para diminuir a exibição)
Figura 4 Toda a AST do DLR de msdnmag.py
Figura 4 Toda a AST do DLR de msdnmag.py (Clique na imagem para aumentar a exibição)

Há algumas observações sobre o diagrama. Assim como o diagrama anterior, há uma mistura na representação visual da árvore. Em termos gerais, caso um nó da AST tenha um membro que aponte para outro nó da AST, a figura mostra uma seta apontando para o subnó. Às vezes, um nó contém uma descrição dos subnós que usa recuos e chaves, e não mais setas e caixas. O instantâneo da janela Locais no Visual Studio mostra a AST abaixo do nó MethodCallExpression no lado esquerdo do diagrama. A primeira seta é pontilhada porque a figura ignora BlockStatement com EmptyExpression. É possível expandir as estruturas de dados na janela Locais para ver toda a estrutura da árvore, como mostra o diafragma.

O nó CodeBlock que representa o corpo da função yo tem membros de variáveis, parâmetros, além da AST do corpo. Isso é semelhante a CodeBlock que representou todo o arquivo msdnmag.py. Como você verá posteriormente, CodeBlock do arquivo se transforma em um método Initialize do módulo. A função yo tem uma variável local, text, que é possível ver caso você expanda o membro variables, expanda o elemento 0 e o objeto Variable. Após uma expansão semelhante, é possível ver que a função yo tem um parâmetro, yourname.

O corpo de CodeBlock é BlockStatement, que tem duas instruções: uma atribuição e um retorno. Como você viu anteriormente em relação à atribuição a yo, BlockStatement primeiro tem ExpressionStatement, que mantém BoundAssignment. Essa atribuição representa a definição de text para "hello, ". O objeto BoundAssignment tem um membro value ConstantExpression e um membro Variable. O objeto Variable descreve text como sendo uma variável local e tem um membro block que aponta novamente para CodeBlock como sendo o escopo da variável local.

A segunda instrução em BlockStatement é ReturnStatement cujo membro expression é ActionExpression. Esse nó da AST é especialmente interessante, como eu expliquei anteriormente. Esse nó ActionExpression tem um membro action, DoOperationAction, que representa uma operação Add, no caso. ActionExpression tem um membro arguments com dois elementos que são objetos BoundExpression. A primeira BoundExpression tem um membro variable que representa o texto local, definido como "hello, ". A segunda tem um membro variable que representa o parâmetro yourname.

Agora passemos ao backup da árvore em BlockStatement, que contém duas ExpressionStatments. Uma é da BoundAssignment de yo a um objeto function. Também há uma segunda ExpressionStatement para que o compilador emita um código e indique o valor de sua expressão na pilha. ExpressionStatement tem um membro expression que é MethodCallExpression.

MethodCallExpression representa a chamada para a função do runtime de IronPython que implementa a instrução print. Ela tem um membro method que é um RuntimeMethodInfo do .NET descrevendo print. A MethodCallExpression tem um membro arguments que é ActionExpression. Conforme abordamos anteriormente, ele é compilado em um local dinâmico tendo em vista chamadas rápidas. ActionExpression tem um membro action que é uma CallAction da chamada para yo. Esse nó também tem um membro arguments com dois elementos. Um é BoundExpression com um membro variable que representa se a expressão tem o valor de yo local (yo é local no módulo no qual é definido). O outro argumento é ConstantExpression com um membro value que mantém a cadeia de caracteres "bill".

Isso encerra a nossa passagem pela AST do DLR. É possível ver como a árvore do IronPython representa diretamente o código do Python que você vê. A AST do DLR tem informações mais explícitas. Ela contém informações sobre a ligação do identificador e blocos explícitos para representar os escopos. Ela contém uma representação explícita das invocações do método runtime (a chamada para Print), e não as ações compiladas em locais dinâmicos (o nó de "text + yourname"). A AST do DLR também contém nós que forçam os resultados das expressões a serem indicados na pilha para que o código se adapte à semântica do Python.

Observando o código gerado

Agora que você viu como o processo é iniciado e realiza a análise, observemos alguns códigos gerados. A forma mais simples de fazer isso é usar opções de ipy.exe e do programa Lutz Roeder's Reflector. As opções fazem com que ipy.exe gere um código estático (ao contrário dos métodos Lightweight Code Generation ou dinâmico) e salve-o no disco. Baixe o Reflector em aisto.com/roeder/dotnet/ e invoque a seguinte linha de comando ainda no diretório de depuração do compartimento em que você criou ironpython.sln:

ipy.exe -D -X:SaveAssemblies -X:StaticMethods msdnmag.py

Depois disso, você encontrará dois arquivos no diretório, snippets1.dll e msdnmag.exe. Agora inicie o Reflector nesses arquivos com essa linha de comando (preenchendo o caminho absoluto se necessário):

C:\where\you\put\reflector.exe snippets1.dll, msdnmag.py

É possível examinar a lista de DLLs e EXEs para localizar msdn e snippets1. Expanda msdnmag, msdnmag.exe, "{} -" e, por fim, "__main__$mod_2". Agora clique duas vezes no método Initialize. Trata-se do corpo do arquivo msdnmag.py. Você deve ver o código-fonte desmontado da IL gerada pelo DLR. Caso já tenha usado o Reflector antes, você sabe que se trata de uma ótima ferramenta, mas que também apresenta algumas falhas. Como ela é capaz de gerar variáveis temporárias extra, por exemplo, você precisa tomar cuidado com o código do C#.

Não falarei sobre a função Initialize, mas é possível ver que yo está sendo definido como sendo o resultado da chamada para MakeFunction, como você viu na AST. É possível ver que, em seguida, Initialize chama o auxiliar do runtime PythonOps.Print no resultado yo de invocação. Observe o fragmento "Call-Simple-1.Invoke". O objeto Call-Simple-1 faz parte do mecanismo de cache do método. Explicarei ainda mais o cache de método, explorando a forma como ele aparece na função yo.

Observemos a função yo. Clique duas vezes em yo$1 no painel à esquerda. É possível ver onde ela define o texto como sendo "hello" e onde ela retorna o resultado de "DoOperation-Add-0.Invoke(text, yourname)". Trata-se da essência do código que escrevi em Python. Daqui a pouco, abordarei DoOperation-Add-0, que é o objeto que armazena em cache os resultados de pesquisa do método. Também observarei algumas das funções geradas pelo runtime em snippets1, enquanto analiso DoOperation-Add-0.

Compreendendo locais dinâmicos

Os locais dinâmicos permitem que o código de linguagem dinâmica seja executado rapidamente. Eles gerenciam o cache de métodos para a realização de operações específicas em determinados tipos de objetos. O mecanismo de locais dinâmicos usado pelo DLR baseia-se na pesquisa e na experiência com implementações de linguagem dinâmica testadas e aprovadas. É possível ver objetos no código gerado como, por exemplo, Call-Simple-1 e DoOperation-Add-0, que são instâncias de tipos derivados de DynamicSite<Targ1, Targ2, Tresult>. Explicarei brevemente a teoria do cache aqui, mas é possível pesquisar na Web "cache de métodos da linguagem dinâmica" ou "cache de métodos embutidos polimórficos" para obter mais informações.

O desempenho da linguagem dinâmica é afetado pelas verificações extra e pelas pesquisas que ocorrem em cada instalação da chamada. As implementações diretas têm que pesquisar repetidamente listas de precedências de classe em busca de membros e potencialmente resolver sobrecargas em tipos de argumento do método sempre que você executar uma determinada linha de código. Em uma expressão como, por exemplo, o.m(x, y) ou x + y, as linguagens dinâmicas precisam verificar exatamente o tipo de objeto de o, qual é m vinculado a o, o tipo de x, o tipo de y ou o que "+" significa para o tipo do runtime real de x e y. Em uma linguagem digitada estaticamente (ou com dicas de tipo suficientes no código e interferência de tipo), é possível emitir exatamente as instruções ou as chamadas de função do runtime apropriadas em cada local da chamada. É possível fazer isso porque você sabe, por meio dos tipos estáticos, o que é necessário no momento da compilação.

As linguagens dinâmicas proporcionam grandes ganhos de produtividade e expressões concisas e eficientes por conta de seus recursos dinâmicos. No entanto, na prática, o código tende a ser executado sempre nos mesmos tipos de objeto. Isso significa que é possível melhorar o desempenho lembrando os resultados das pesquisas de método na primeira vez em que uma seção de código é executada. Por exemplo, com x + y, caso x e y sejam inteiros na primeira vez em que uma expressão é executada, podemos lembrar uma seqüência de códigos ou o que a função de runtime realiza, além dos dois inteiros. Dessa forma, sempre que uma expressão é executada, não há nenhuma pesquisa envolvida. O código só verifica novamente se x e y são inteiros e expede no código correto sem pesquisa. O resultado pode ser literalmente reduzido a uma geração de código embutido com algumas verificações de tipo e uma instrução add, dependendo da semântica de uma operação dos mecanismos do cache de métodos usados.

Como os objetos DynamicSite funcionam

Os compiladores de linguagem estática podem selecionar métodos ou emitir códigos específicos no momento da compilação para realizar operações como, por exemplo, de adição, de busca de membros ou de indexação. Como nem sempre sabe o que emitir no momento da compilação, o DLR emite códigos para chamar um método Invoke em um objeto DynamicSite. Esse objeto captura a operação e um delegado (chamado por Invoke) que contém a lógica de cache. Essa lógica de cache é atualizada sempre que o delegado vê uma nova combinação entre os tipos de argumento no momento da execução. (Observe que, na verdade, os delegados encapsulam ponteiros para funções, mas, por questões de brevidade, eu uso o delegado como se ele fosse a função.) Esta é a história simplificada; a seguir, uma abordagem mais detalhada.

O delegado começa com uma chamada para UpdateBindingAndInvoke. Na primeira vez em que um local de chamada é executado (consideremos apenas x + y), UpdateBindingAndInvoke consulta os argumentos a respeito da implementação de uma operação Add para o tipo x e para o tipo y. Caso encontre uma, ele gera um novo delegado que codifica uma verificação de x e y tendo os mesmos tipos que você acabou de ver. Em uma chamada posterior, caso x e y tenham os mesmos tipos, o código do novo delegado chama a implementação que obtivemos. Caso x ou y tenham um tipo diferente em uma chamada posterior, o delegado passa por todas as verificações do cache e chama UpdateBindingAndInvoke novamente, que gera um novo delegado. O código do delegado mais recente irá capturar os primeiros testes de x e y, bem como os novos testes, chamando a implementação de destino que corresponder aos tipos.

É hora de observarmos o delegado DoOperation-Add-0 gerado pelo DLR a partir da execução do código em msdnmag.py. Retorne ao Reflector e, desta vez, expanda os nós snippets1, snippets.dll, "{} -" e "Type$_stub_$4" no painel à esquerda. Clique no método Handle, e você verá no painel à direita o código visto na Figura 5.

Figure 5 Cache "+" desmontado (Close Figure 5)
public static object Handle(object[],
    FastDynamicSite<object, object, object> site1,
    object obj1, object obj2)
{
    if (((obj1 != null) && (obj1.GetType() == typeof(string)))
        && ((obj2 != null) && (obj2.GetType() == typeof(string))))
    {
        string text;
        string text1 = text =
            StringOps.Add(Converter.ConvertToString(obj1),
                          Converter.ConvertToString(obj2));
        return text;
     }
     return site1.UpdateBindingAndInvoke(obj1, obj2);
 }

Trata-se do código desmontado da IL gerado pelo DLR para o delegado DoOperation-Add-0 após a execução de "text + yourname". Se nós chamássemos a função yo novamente, esse delegado seria executado. Esse código tem alguns problemas de desempenho que podem ser apontados por você, mas há mais sobre isso à frente. É possível ver que o código testa se os dois argumentos são cadeias de caracteres e chama a implementação da adição por parte do runtime do IronPython dadas duas cadeias.

Caso um dos argumentos não seja uma cadeia, o delegado chama UpdateBindingAndInvoke para gerar um novo delegado. O novo delegado substituiria o atual (acima) de forma que, na próxima vez em que fosse executado, DoOperation-Add-0.Invoke chamasse o delegado atualizado. Eu alterei o código msdnmag.py para que "text + yourname" fosse executado em dois inteiros em uma segunda chamada para yo. Em seguida, encontrei o segundo delegado gerado pelo DLR após a segunda chamada. Ele era semelhante ao da Figura 6.

Figure 6 Cache dois "+" desmontado (Close Figure 6)
      public static object Handle(object[]
    FastDynamicSite<object, object, object> site1,
    object obj1, object obj2)
{
    if (((obj1 != null) && (obj1.GetType() == typeof(int)))
        && ((obj2 != null) && (obj2.GetType() == typeof(int))))
    {
        return Int32Ops.Add(Converter.ConvertToInt32(obj1),
                            Converter.ConvertToInt32(obj3));
    }
    if (((obj1 != null) && (obj1.GetType() == typeof(string)))
        && ((obj2 != null) && (obj2.GetType() == typeof(string))))
    {
        return = StringOps.Add(Converter.ConvertToString(obj1),
                               Converter.ConvertToString(obj2));
    }
    return site1.UpdateBindingAndInvoke(obj1, obj3);
  }

Eu limpei as variáveis temporárias falsas inseridas pelo Reflector. Se fizer a tentativa extra que eu fiz, você obterá um código um pouco diferente. Você verá que agora o código testa se os argumentos são inteiros. Caso sejam, ele chama a função auxiliar do runtime que implementa Add em inteiros. O mesmo código que eu tinha antes surge após o teste de inteiro e do código de destino. Optei por organizar os testes com os tipos testados mais recentemente primeiro. Com execuções do programa tendo perfis, você poderia pensar na geração de todos os testes de argumentos certos na ordem ideal. É claro que o delegado volta a chamar UpdateBindingAndInvoke caso os argumentos tenham uma nova combinação de tipos jamais vista.

Você deverá observar problemas de desempenho em relação ao código. O DLR poderia gerar códigos melhores atualmente, mas, como acontece com qualquer versão alfa, o DLR faz concessões tendo em vista a funcionalidade e a geração de códigos. Você pode observar que o código é testado em relação a um tipo, cadeia de caracteres e, em seguida, converte o argumento em uma cadeia. Há dois problemas nisso. O primeiro é que o JIT do .NET Framework 2.0 não propaga informações sobre o tipo de testes "if" para o bloco de códigos seguinte. Como a implementação de Add é um método digitado estaticamente, os argumentos precisam ser exibidos como cadeias de caracteres para ele. O segundo problema é que o código gerado pelo DLR faz uma chamada lenta para uma função de conversão do runtime. O DLR poderia ter emitido uma operação de conversão aqui.

O último problema de desempenho a ser mencionado é que o DLR atualmente não estrutura testes para evitar a repetição. Em meu delegado atualizado que sabe como adicionar rapidamente dois inteiros ou duas cadeias de caracteres, eu testo os argumentos em busca de nulos. Faço isso várias vezes enquanto passo pelos testes quando eu preciso fazer isso. Quando procura implementações de operação, UpdateBindingAndInvoke, na verdade, pergunta aos objetos como realizar a operação. Ele é chamado nos objetos para fornecer uma regra sobre como realizar a operação representada por DynamicSite. A regra contém uma AST do teste a ser realizado e uma AST do código de destino a ser executado caso o teste seja verdadeiro. O DLR poderia realizar transformações na árvore em todos os testes para otimizar ainda mais o código do delegado.

Por fim, como usei locais dinâmicos para buscar membros dos objetos, eu tenho que tratar alterações feitas nas instâncias e nos tipos dinâmicos. O DLR não pode presumir para o.m() que, caso o aponte para o mesmo objeto, ele saiba exatamente qual m invocar. Algumas linguagens permitem que os tipos sejam alterados no runtime, e elas substituem os membros, basicamente substituindo um em uma instância. O DLR usa um contador de versão global incrementado em caso de novos tipos ou de alterações nos tipos. Há várias mudanças que fazem com que a versão de um tipo seja alterada como, por exemplo, alterar membros. Todos os caches DynamicSite que se referirem a objetos em que a versão do tipo tenha sido alterada apresentarão falha no cache. Dessa forma, DynamicSite pesquisará novamente e atualizará o delegado.

O grande benefício

Os tipos DynamicSite gerados são dinâmicos. Usar tipos genéricos não ajuda muito no código de exemplo do Python em msdnmag.py. No entanto, em uma expressão como, por exemplo, "if x < 1:" é possível gerar DynamicSite<Object, Int, Bool>. O método Invoke do local será digitado estaticamente para considerar um argumento como sendo um inteiro e retornar um Booleano. O delegado dentro do local dinâmico também é digitado estaticamente dessa forma, o que torna o código resultante mais rápido. O delegado só precisa testar um argumento em seu cache e, à medida que a pilha se desdobra, o JIT sabe que um valor Booleano está flutuando para o local correto. Isso elimina todas as conversões ou verificações próximas ao teste if.

Algumas linguagens dinâmicas têm uma digitação explícita opcional. Mesmo que não tenham, as implementações podem inferir os tipos de chamadas para os métodos estáticos do .NET, de constantes literais etc. Elas podem propagar essas informações sobre o tipo durante o tempo de vida das variáveis, ramificações de condicionais e similares. Quando têm informações sobre o tipo, as linguagens podem gerar a AST do DLR que, por sua vez, é compilada em tipos DynamicSite com todas as informações sobre o tipo. Isso pode resultar na geração de códigos embutidos para a chamada do método Invoke e a chamada do código de destino no cache.

Atualmente, o DLR gera tipos DynamicSite genéricos com seis ou menos parâmetros de tipo genéricos. Os primeiros N-1 parâmetros representam os tipos de argumentos, e Nth representa o tipo de resultado da operação. Depois de seis tipos, você precisa gerar a AST que acabou de realizar chamadas de método para funções auxiliares do runtime. Essas funções devem fazer o trabalho lentamente para computar tudo no runtime. Futuramente, o DLR permitirá que você use tuplas genéricas para fornecer números maiores de argumentos digitados estaticamente. Também podemos aumentar o número em seis vezes, à medida que identificamos o ponto principal da maioria dos programas.

Agora você deve ter uma boa idéia sobre como explorar ASTs de linguagens específicas e ASTs do DLR. Com algumas informações básicas sobre árvores simples, é possível examinar a forma como determinadas linguagens foram criadas no DLR. Caso você esteja tentando implementar uma linguagem no próprio DLR, é possível usar essa técnica. Caso não esteja, talvez seja divertido apenas observar o que está acontecendo nos bastidores.

Você também deve ver algumas das infra-estruturas compartilhadas oferecidas pelo DLR aos implementadores de linguagem. Há muito o que melhorar em relação ao CLR e ao DLR – coleta de lixo, compilação JIT, locais dinâmicos, modelo de tipo dinâmico básico, etc. Implementar linguagens no runtime do .NET produz ótimos resultados agora – e isso só deverá melhorar. A versão 1 do DLR será excelente. Em breve, a implementação de linguagens dinâmicas no .NET será surpreendentemente simples, e os resultados serão excelentes.

Envie suas dúvidas e seus comentários para clrinout@microsoft.com.

Bill Chiles é gerente sênior de programas na Microsoft e já trabalhou em projetos de linguagem dinâmica (CMU Common Lisp, Dylan) e ferramentas em grande parte de sua carreira. Bill atuou durante nove anos na equipe Visual Studio Core Technologies e trabalha atualmente em IronPython e runtime de linguagem dinâmico.


Instintos b&#225;sicos: Infer&#234;ncia de tipos no Visual Basic 2008

Instintos básicos
Inferência de tipos no Visual Basic 2008
Bill Horst

Conteúdo
Por que a inferência de tipos é necessária?
A inferência de tipos e as expressões Lambda
Outras implicações
Go To Type Definition
Habilitando e desabilitando a inferência de tipos

P
ara oferecer suporte a LINQ (Language Integrated Query), um conjunto de novas tecnologias foi adicionado à próxima versão do Visual Basic® 2008, incluindo a inferência de tipos. Nas versões anteriores do Visual Basic, o tipo de um membro supostamente seria Object, a menos que um tipo diferente fosse explicitamente declarado em código, como mostra a Figura 1. Uma desvantagem de declarar uma variável dessa forma é que o compilador só é capaz de supor que o valor da variável será do tipo Object. A experiência do desenvolvedor é que o IntelliSense® só é capaz de exibir informações para os membros comuns a todos os Objects, embora possa haver outros membros disponíveis na instância real em runtime. Com a vinculação tardia, os outros membros são vinculados em runtime, o que significa que eles não serão verificados de forma alguma até a hora da execução da linha. Assim, membros não existentes (como o cust.Name da Figura 1) podem ser referenciados em código e o problema não será encontrado pelo compilador. Isso pode resultar facilmente em uma MissingMemberException e geralmente requer que você controle mais informações por conta própria.

Figure 1 A falta da inferência de tipos no Visual Basic 2005 (Close Figure 1)
Class Form1
    Sub Main()
        Dim a = 3                   ' Type: Object    Value: 3 {Integer}
        Dim b = DateTime.Now        ' Type: Object    Value: Current Time

        Dim cust = GetCustomer(someID)
        MsgBox(cust.Name) ' Customers have FirstName and LastName fields.
                          ' This code generates an exception at Run-Time.

        Dim procs = Process.GetProcesses
        For Each g In procs   ' Compile Error: 'g' is not declared
            Console.WriteLine(g.Id)
        Next
    End Sub
End Class

No entanto, com a inferência de tipos do Visual Basic 2008, os tipos de membros de nível de método são inferidos com base no lado direito de qualquer atribuição das instruções da declaração. Você pode encontrar diversos exemplos na Figura 2. Aqui, o compilador captura a referência ao membro não existente, cust.Name, mostrado na Figura 1 e as informações do IntelliSense serão fornecidas por todos os membros disponíveis, como se o tipo tivesse sido especificado de forma explícita. No loop For mostrado na Figura 2, a variável g é reconhecida como uma declaração, com o tipo de expressão "procs".

Figure 2 Inferência de tipos no Visual Basic 2008 (Close Figure 2)
Class Form1
    Sub Main()
        Dim a = 3                   ' Type: Integer   Value: 3
        Dim b = DateTime.Now        ' Type: Date      Value: Current Time

        Dim cust = GetCustomer(someID)
        MsgBox(cust.Name) ' Here we get a compiler error, catching the
                          ' problem quickly

        Dim procs = Process.GetProcesses
        For Each g In procs         ' Type: Process
            Console.WriteLine(g.Id) ' No compile errors
        Next
    End Sub
End Class

Entretanto, existem ocasiões em que a inferência de tipos não acontecerá. Para instruções Dim locais, a inferência de tipos só ocorrerá quando houver uma atribuição na linha da declaração. Dessa forma, as atribuições feitas fora da declaração da variável farão com que o compilador suponha que o tipo é Object. Object também será inferido como o tipo para membros de nível de classe e, portanto, a inferência de tipos não se aplica a funções, subs, propriedades, campos de classe/estrutura e assim por diante. Alguns exemplos específicos são mostrados na Figura 3.

Por que a inferência de tipos é necessária?

A inferência de tipos é necessária no Visual Basic 2008 por causa da construção da nova linguagem, que permite a criação de um tipo que não é definido explicitamente em nenhum lugar. Para atribuir uma variável a uma expressão com um tipo definido de forma não explícita, é necessário inferi-lo, já que seria impossível especificá-lo em código.

Esses tipos definidos não explícitos são chamados de tipos anônimos. Um objeto pode ser criado e certos membros inicializados sem terem sido previamente definidos. Isso permite que instruções LINQ especifiquem um número variável de campos (e em ordens diferentes) sem precisar definir, com antecedência, objetos para todas as variantes possíveis. Como o objeto não é necessariamente igual a um tipo já definido, o compilador gera um tipo implícito (como o nome desse tipo é irrelevante e não é exibido ao desenvolvedor para uso em código, ele é chamado de tipo anônimo). Usando a funcionalidade de inferência de tipos, o compilador é capaz de tratar esse objeto de um tipo anônimo como trataria um objeto de um tipo definido explicitamente. Os tipos anônimos serão muito úteis aos usuários de LINQ que escrevem consultas do estilo SQL em Visual Basic.

A seguir, um exemplo de como os tipos anônimos podem ajudá-lo em seu desenvolvimento LINQ. Suponha que você tenha um banco de dados que inclua registros dos pedidos da sua empresa e deseja exibir certas informações sobre todos os pedidos enviados para o estado de Washington. O código SQL a seguir supõe a existência de uma tabela chamada Orders, cujos registros contêm vários campos, incluindo ID, Date, Addresss, City, State, Zip e Shipped. A consulta retornaria todos os registros da tabela, onde o campo Shipped tem o valor True e o campo State possui o valor Washington:

SELECT Orders.ID, Orders.Date AS OrderDate, Orders.Address,
       Orders.City, Orders.State, Orders.Zip
FROM Orders
WHERE Shipped = 'True' AND State = 'Washington'

Com o desenvolvimento do LINQ, uma operação similar pode ser executada no Visual Basic. O código a seguir executa essa mesma consulta em Orders e atribui o resultado à variável OrdersShippedToWA. Orders poderia ser uma tabela de banco de dados, uma matriz ou coleção em memória ou qualquer tipo que implemente a interface IEnumerable:

Dim OrdersShippedToWA = From Ord In Orders _
                        Where Ord.Shipped _
                            And Ord.State = "Washington" _
                                Select Ord.ID, OrderDate = Ord.Date, _
                                Ord.Address, Ord.City, _
                                Ord.State, Ord.Zip

O tipo inferido para OrdersShippedToWA será IEnumerable(Of <tipo anônimo>). Cada elemento de IEnumerable terá então os membros selecionados na consulta £ ID, OrderDate, Address, City, State e Zip. Como você já deve imaginar, LINQ é muito mais fácil de usar e mais ágil se não for preciso definir um tipo para cada combinação de membros usados na parte Select das expressões da sua consulta. Por esse motivo, o compilador do Visual Basic gera um tipo anônimo que contém os membros apropriados e infere esse tipo se um elemento for referenciado mais adiante no código. No loop For a seguir, o tipo da variável Ord é inferido como o mesmo tipo anônimo discutido para OrdersShippedToWA:

For Each Ord In OrdersShippedToWA
    Dim City As String = Ord.City
Next

Nesse exemplo, a variável Ord é de um tipo que não foi declarado explicitamente, mas cujos nomes de membro são conhecidos. O compilador em segundo plano é capaz de inferir o tipo e você obtém o mesmo suporte do Intellisense com o qual está acostumado. Esse é o principal motivo pelo qual a inferência de tipos foi inserida no Visual Basic 2008.

Imagine outro cenário em que você deseja lidar com todo o objeto Order. Se somente uma expressão for selecionada pela consulta, o tipo específico será inferido. Se houver métodos ou outros membros definidos para o tipo usado na coleção, eles também poderão ser chamados por meio da inferência de tipos:

Dim SortedWAOrders = From Ord In Orders _
                     Select Ord _
                     Order By Ord.ID Ascending

Dim FirstShipTime = SortedWAOrders(0).CalculateShipTime()

Na verdade, graças à inferência de tipos, você poderia usar seus métodos na própria consulta. Considere o exemplo a seguir, em que os resultados são mostrados na ordem do horário de envio calculado por um método:

Dim SortedShipTimes = From Ord In Orders _
                      Select Ord _
                      Order By Ord.CalculateShipTime() Ascending

Pode haver ocasiões onde será conveniente para você inicializar um tipo anônimo em código e, portanto, isso também é suportado no Visual Basic. Na Figura 4, a Parte A mostra a variável NewCustomer criada sem um tipo explícito. Observe que os tipos dos membros na definição também estão sendo inferidos, já que eles não são definidos em lugar algum. A Parte B mostra outra atribuição, onde um membro do tipo anônimo é reconhecido pelo compilador. A funcionalidade IntelliSense será suportada na variável quando for referenciada na atribuição NewCustomerCity. Na Parte C, uma consulta chamada SeattleCustomers, que seleciona membros iguais aos membros do tipo anônimo, é inicializada. Nesse caso, o compilador reconhece que um tipo anônimo já existe e que é igual ao necessário a essa instrução. Em vez de criar um novo tipo anônimo, o mesmo tipo de NewCustomer é inferido para SeattleCustomers. Na Parte E, o objeto atribuído a NewCustomer pode ser adicionado aos dados da consulta SeattleCustomers porque a inferência de tipos reconheceu que o mesmo tipo anônimo poderia ser usado em ambos os locais.

Figure 4 Tipos anônimos combinados a partir de consultas e de variáveis (Close Figure 4)
'[Part A] Anonymous Type variable declared
Dim NewCustomer = New With {.Name = "Bill Horst", _
                            .Age = 25, _
                            .City = "Seattle"}

'[Part B] Member recognized by Compiler/IntelliSense
Dim NewCustomerCity = NewCustomer.City

'[Part C] Name, Age and City selected from records in
'         Customers database table
Dim SeattleCustomers = From cust In db.Customers _
                       Where cust.City = "Seattle" _
                       Select cust.Name, cust.Age, cust.City

'[Part D] Seattle Customers stored in a variable with
'         type List(Of <anonymous type>)
Dim SCList = SeattleCustomers.ToList()

'[Part E] Variable Type and Query Result Type match
SCList.Add(NewCustomer)

A inferência de tipos e as expressões Lambda

A inferência de tipos também é útil na acomodação de expressões Lambda. Uma expressão Lambda é um novo recurso de linguagem do Visual Basic que permite a utilização de delegados para funções simples de uma forma embutida. Isso é particularmente útil para passar delegados simples como parâmetros. Sem nem mesmo ter de nomear uma função, uma expressão Lambda pode ser passada declarando parâmetros e a expressão de retorno. No exemplo a seguir, será declarada uma função que aceita um Customer e uma String e retorna um Boolean mostrando se a cidade de Customer é ou não igual a String. Essa função será passada como um delegado para o método Filter, presumivelmente para filtrar com base nesse valor de Boolean:

Orders.Filter( _
    Function(cust As Customer, city As String) cust.City = city)

Uma vez que isso é basicamente uma definição de função sem um tipo de retorno, fica claro como a inferência de tipos pode ser útil na determinação do tipo da expressão para a declaração do delegado apropriado e na verificação da validade do tipo.

Outras implicações

Quando Option Explicit estiver desativado, uma variável local poderá ser usada em código sem uma declaração explícita. Assim como no caso descrito anteriormente, supõe-se que a variável seja do tipo Object e que todas as chamadas sejam de vinculação tardia. Vale a pena observar que a inferência de tipos não ocorre em variáveis definidas implicitamente e, portanto, o comportamento não mudou. O código a seguir mostra um exemplo:

Sub SwapTwoInts(ByVal x As Integer, ByVal y As Integer)
    z = x
    x = y
    y = z
End Sub

Usando o Visual Basic 2005, foi impossível declarar uma variável temporária de forma implícita na Janela Imediata durante a depuração ao atribuir alguma expressão a um identificador não definido no escopo atual. Com isso, a variável do tipo Object foi criada e atribuída ao valor especificado, como se essa atribuição tivesse sido feita em código com Option Explicit Off. No Visual Basic 2008, o comportamento que descrevi para a Janela Imediata e o comportamento com Option Explicit Off serão iguais aos do Visual Basic 2005, fazendo com que a inferência de tipos não aconteça. Se eu inserir os dois comandos a seguir na Janela Imediata, o seguinte resultado será exibido, mostrando uma variável Object dado o valor 12 de Integer:

intVar = 12
?intVar
12 {Integer}

Go To Type Definition

Quando tipos recebem explicitamente uma definição de variável, a ação "Go To Definition" do Visual Studio® pode ser chamada com facilidade do menu de contexto do identificador de tipo para determinar onde o tipo será definido. No caso a seguir, o usuário pode clicar em IEnumerable e o resultado será IEnumerable(Of T) no Pesquisador de Objetos:

Dim customers As IEnumerable(Of Customer) = GetCustomers()

Com os novos recursos de linguagem discutidos aqui, espero que seja cada vez mais freqüente o caso em que o tipo é difícil de discernir rapidamente. Por causa da inferência de tipos, os tipos não precisam ser explícitos e, com as novas construções de linguagem oferecendo suporte ao LINQ, ele pode ser bem obscuro. A variável a seguir também é do tipo IEnumerable(Of T), mas é muito menos óbvia aqui, além de não haver um local para chamar Go To Definition para a navegação até seu tipo:

Dim customers = From cust In _
                    (From c In db.Customers Select c) _
                Where cust.FirstName = "Danae" _
                Select cust.LastName, cust.Address

For Each c In customers
...

Esse problema motivou a introdução do comando Go To Type Definition no Visual Studio 2008. Você também pode chamá-lo do menu de contexto para localizar a definição do tipo de uma variável, mesmo se o código só contiver o identificador da variável. Você pode executar o comando Go To Type Definition na definição de uma variável ou de uma referência. No trecho de código anterior, você poderia simplesmente chamar Go To Type Definition na variável de clientes na definição ou no loop For e poderia ser levado ao Pesquisador de Objetos com IEnumerable(Of T) selecionado (consulte a Figura 5). Caso contrário, se o tipo tivesse sido definido explicitamente em código, você navegaria até esse local.

Figura 5 Comando Go To Type Definition para um loop For
Figura 5 Comando Go To Type Definition para um loop For

Habilitando e desabilitando a inferência de tipos

Uma das maiores implicações do suporte à inferência de tipos é que alguns códigos compilados em uma versão anterior do Visual Basic podem não ser compilados em um projeto onde a inferência de tipos estiver habilitada. Isso acontece porque os projetos atualizados terão a inferência de tipos desativada por padrão. A principal área onde tais erros de compilação podem ocorrer é o código que utiliza a vinculação tardia.

Nas versões anteriores do Visual Basic, a vinculação tardia ocorria quando Option Strict estava desativada e uma variável sem um tipo explícito era inicializada para uma expressão. A variável recebe o tipo Object e qualquer chamada a membros ou a métodos nessa variável seria vinculada em runtime:

' Late Binding with Visual Basic 2005
Dim x = 5
x = New List(Of Integer)
x.Sort()
x = New Queue(Of Integer)
x.Enqueue(6)

Com a inferência de tipos, tais chamadas resultariam em erros de compilador. Em vez de dar à variável o tipo Object, o compilador oferece a ela um tipo igual à expressão para a qual ela foi inicializada. Ainda é possível fazer a vinculação tardia com a inferência de tipos habilitada, mas Object deve ser oferecido explicitamente como o tipo:

' Late Binding with Visual Basic 2008
Dim x As Object = 5
x = New List(Of Integer)
x.Sort()
x = New Queue(Of Integer)
x.Enqueue(6)

Existem outras ocasiões em que a inferência de tipos também deverá ser desativada. Felizmente, há uma opção de compilador chamada Option Infer que, como Option Strict ou Option Explicit, permite que você a ative ou a desative. Ela é colocada no início de um código como qualquer outra opção de compilador no Visual Basic:

Option Infer On    ' This enables type inference for the file

Option Infer Off   ' This disables type inference, so declarations
                   ' with no type specified will use Object

Se Option Infer não for especificada para um arquivo de código em particular, a configuração de nível de projeto será usada. A configuração de nível de projeto padrão para Option Infer pode ser definida na guia Compile do Project Designer. O Project Designer pode ser acessado no menu Project ou no nó My Project do Solution Explorer.

Option Infer está ativado por padrão nos projetos novos do Visual Basic 2008 e desativado por padrão nos projetos atualizados para evitar quebras no código pré-existente. As configurações de nível de arquivo substituem essa opção em arquivos específicos.

Embora haja diversas vantagens na utilização da inferência de tipos, como você verá quando começar a usar o LINQ e outras tecnologias que se beneficiam dela, você terá a capacidade de especificar quando ela será chamada e quando não será, garantindo a flexibilidade máxima.

Envie as suas dúvidas e os seus comentários para Bill em instinct@microsoft.com.

Bill Horst é engenheiro de design de software de testes da equipe do Microsoft Visual Basic. Ele realizou grande parte dos testes de Consultas LINQ e testa a depuração do Visual Basic.


Pontos de dados: Express&#245;es de tabela comuns

Pontos de dados
Expressões de tabela comuns
John Papa

Download do código disponível em: DataPoints2007_10.exe (150KB)
Conteúdo
Exibições, tabelas derivadas e CTEs
A estrutura de uma CTE
Compreendendo as CTEs
Regras de recursão
Aplicação da recursão
Resumindo

M
uitos projetos nos quais os desenvolvedores trabalham envolvem a escrita de instruções SQL complexas que deixam de lado os tipos básicos de instrução SELECT/FROM/WHERE. Um cenário como esse envolve a escrita de consultas Transact-SQL (T-SQL) que usam tabelas derivadas (também conhecidas como exibição embutida) em uma cláusula FROM. Essa prática comum permite que um desenvolvedor obtenha um conjunto de linhas e o vincule imediatamente a outras tabelas, exibições e funções definidas pelo usuário em uma instrução SELECT. Outra opção é usar uma exibição em lugar de uma tabela derivada. As duas opções têm suas vantagens e desvantagens.

Ao trabalhar com o SQL Server™ 2005, eu prefiro uma terceira opção, a de usar CTEs. As CTEs podem ajudar a aprimorar a legibilidade (e, dessa forma, a manutenção) do código sem comprometer o desempenho. Elas também tornam a escrita de código recursivo em T-SQL significativamente mais simples em relação às versões anteriores do SQL Server.

Na coluna deste mês, estou me concentrando no uso das CTEs para abordar cenários de desenvolvimento comuns. Começarei descrevendo como as CTEs funcionam e em quais cenários elas podem ser usadas. Em seguida, abordarei as vantagens de usar as CTEs em comparação com o uso de construções T-SQL tradicionais como, por exemplo, tabelas derivadas, exibições e procedimentos personalizados. Ao longo de toda a coluna, darei exemplos e explicarei como e onde eles podem ser usados. Também demonstrarei como as CTEs tratam a lógica recursiva e definirei como funciona a CTE recursiva. Observe que todos os códigos abordados por mim nesta coluna estão disponíveis para download no site da MSDN® Magazine e usam os bancos de dados Northwind e AdventureWorks que acompanham o SQL Server 2005.

Exibições, tabelas derivadas e CTEs

As CTEs podem ser úteis quando as consultas precisam selecionar em um conjunto de dados que não existe como tabela dentro do banco de dados. Por exemplo, você talvez queira escrever uma consulta referente a um conjunto de dados agregados que calcula os valores com base nos clientes e em seus pedidos. Os dados agregados podem ser vinculados às tabelas Customers, Orders e Order Details simultaneamente e calcular os valores de soma e média dos pedidos. E você talvez queira consultar esse conjunto de linhas agregado. Uma solução é criar uma exibição que colete os dados agregados antes de escrever uma consulta referente à exibição. Outra opção é consultar os dados agregados usando uma tabela derivada. É possível fazer isso movendo a instrução SQL para a cláusula FROM e consultando-a.

As exibições costumam ser usadas para dividir consultas grandes para que elas possam ser consultadas de maneira muito mais legível. Por exemplo, uma exibição pode representar uma instrução SELECT que vincula 10 tabelas simultaneamente, seleciona dezenas de colunas e filtra as linhas com base em um conjunto de lógica envolvido. Dessa forma, a exibição pode ser consultada por outras instruções SELECT em todo o banco de dados. Essa abstração proporciona um acesso simplificado ao conjunto de linhas representado pela exibição, não havendo a necessidade de duplicar os dados ou de armazená-los em uma tabela temporária.

A exibição também pode ser reutilizada em todo o banco de dados, presumindo-se que as permissões permitam. Na Figura 1, por exemplo, uma exibição é criada e usada por outra instrução T-SQL. No entanto, em situações em que você deseja coletar os dados e só usá-los uma vez, as exibições talvez não sejam a melhor solução. Como uma exibição é um objeto de banco de dados existente e disponível em todo o banco para todos os lotes, criar uma exibição que só seja usada em um único lote T-SQL é um desperdício.

Figure 1 Exibição sendo consultada (Close Figure 1)
CREATE VIEW vwMyView AS
SELECT
    EmployeeID, COUNT(*) AS NumOrders,  MAX(OrderDate) AS MaxDate
FROM Orders
GROUP BY EmployeeID
GO

SELECT
    e.EmployeeID, oe.NumOrders, oe.MaxDate, e.ReportsTo AS ManagerID,
    om.NumOrders, om.MaxDate
FROM
    Employees AS e
    INNER JOIN vwMyView AS oe ON e.EmployeeID = oe.EmployeeID
    INNER JOIN vwMyView AS om ON e.ReportsTo = om.EmployeeID

Outra opção é criar uma tabela derivada (também conhecida como exibição embutida). Uma tabela derivada pode ser criada apenas movendo-se a instrução SELECT para uma cláusula FROM entre parênteses. Dessa forma, ela pode ser consultada ou vinculada assim como acontece com uma tabela ou uma exibição. O código na Figura 2 resolve a mesma consulta da Figura 1, mas usa tabelas derivadas em lugar de uma exibição. Enquanto as tabelas derivadas só podem ser acessadas na instrução em que elas existem, as tabelas costumam dificultar muito a leitura de uma consulta e, assim, sua manutenção. Esse problema pode ser multiplicado caso você queira usar a tabela derivada várias vezes dentro do mesmo lote, uma vez que é preciso copiar e colar a tabela derivada para poder reutilizá-la.

Figure 2 Consulta usando tabelas derivadas (Close Figure 2)
SELECT
    e.EmployeeID, oe.NumOrders, oe.MaxDate,  e.ReportsTo AS ManagerID,
    om.NumOrders, om.MaxDate
FROM
    Employees AS e
    INNER JOIN
        (SELECT EmployeeID, COUNT(*), MAX(OrderDate)
         FROM Orders
         GROUP BY EmployeeID) AS oe(EmployeeID, NumOrders, MaxDate)
        ON e.EmployeeID = oe.EmployeeID
    LEFT JOIN
        (SELECT EmployeeID, COUNT(*), MAX(OrderDate)
         FROM Orders
         GROUP BY EmployeeID) AS om(EmployeeID, NumOrders, MaxDate)
        ON e.ReportsTo = om.EmployeeID

Uma CTE adapta-se bem a esse tipo de cenário, por tornar o T-SQL muito mais legível (como uma exibição), e ainda pode ser usada mais de uma vez em uma consulta imediatamente posterior no mesmo lote. É claro que isso não está disponível além desse escopo. Além disso, a CTE é uma construção em nível de linguagem – o que significa que o SQL Server não cria tabelas temporárias ou virtuais internamente. A consulta subjacente da CTE será chamada sempre que for referenciada na consulta imediatamente posterior.

Por isso, esse mesmo cenário também pode ser escrito usando uma CTE, como mostra a Figura 3. EmpOrdersCTE coleta os dados agregados e é usado pela consulta imediatamente superior à CTE. Usando a CTE, o código na Figura 3 torna a consulta muito legível (como uma exibição), embora não crie um objeto de sistema para armazenar os metadados.

Figure 3 Consultando com uma CTE (Close Figure 3)
;WITH EmpOrdersCTE (EmployeeID, NumOrders, MaxDate) AS
(
  SELECT EmployeeID, COUNT(*), MAX(OrderDate)
  FROM Orders
  GROUP BY EmployeeID
)

SELECT
    e.EmployeeID,  oe.NumOrders, oe.MaxDate,
    e.ReportsTo AS ManagerID,  om.NumOrders, om.MaxDate
FROM
    Employees AS e
    INNER JOIN EmpOrdersCTE oe ON e.EmployeeID = oe.EmployeeID
    LEFT JOIN EmpOrdersCTE om ON e.ReportsTo = om.EmployeeID

A estrutura de uma CTE

Agora eu demonstrarei como construir uma CTE usando uma CTE simples como exemplo. Uma CTE começa com a palavra-chave WITH. No entanto, caso a CTE não seja a primeira instrução no lote, você deve colocar um ponto-e-vírgula antes da palavra-chave WITH. Como prática recomendada, eu prefiro prefixar todas as minhas CTEs com um ponto-e-vírgula – considero esta abordagem consistente mais fácil do que precisar lembrar se eu preciso ou não de um ponto-e-vírgula.

A palavra-chave WITH é seguida do nome da CTE, que é seguida de uma lista opcional dos aliases de coluna. Os aliases de coluna correspondem às colunas retornadas pela instrução SELECT dentro da CTE. Após os aliases de coluna opcionais está a palavra-chave AS, obrigatória. E a palavra-chave AS é seguida pela expressão da consulta que define a CTE entre parênteses.

Vejamos este exemplo:

;WITH myCTE (CustID, Co) AS
(
  SELECT CustomerID, CompanyName FROM Customers
)
SELECT CustID, Co FROM myCTE

As colunas CustomerID e CompanyName têm os aliases CustID e Co. Logo após a CTE há uma instrução SELECT que referencia a CTE usando seus aliases de coluna.

Compreendendo as CTEs

Antes de criar uma CTE, você deve compreender como ela funciona e quais regras ela segue. Esta seção descreve onde as CTEs podem ser usadas, bem como o que pode e o que não pode ser usado dentro de uma CTE. Para iniciantes, as CTEs podem ser criadas e usadas dentro de um lote T-SQL, uma função definida pelo usuário, um procedimento armazenado, um disparador ou uma exibição.

Uma CTE só pode ser referenciada pela instrução imediatamente posterior à CTE. Isso significa que, caso queira usar uma CTE, você deve escrever a consulta referente à CTE logo após a CTE no lote T-SQL. Por exemplo, o seguinte lote produzirá um erro:

;WITH myCTE (CustID, Co) AS
(
  SELECT CustomerID, CompanyName FROM Customers
)
SELECT CompanyName FROM Customers WHERE CustomerID = 'ALFKI'
SELECT CustID, Co FROM myCTE

Nesse código, myCTE só está disponível para a primeira consulta imediatamente posterior. Quando a segunda consulta se refere a myCTE, a CTE não está no escopo e uma exceção é lançada (nome de objeto 'myCTE' inválido).

Também observe que, como a CTE não deve ser referenciada por outra consulta, o que pode reprocessar os dados de uma forma ou de outra, a consulta de uma CTE não pode conter instruções como, por exemplo, ORDER e COMPUTE. No entanto, instruções complexas como, por exemplo, FOR XML ainda podem ser usadas na definição e na operação em uma CTE. Por exemplo, você poderia consultar uma CTE e retornar seus resultados usando a cláusula FOR XML, como mostrado aqui.

;WITH myCTE AS
(
  SELECT c.CustomerID, c.CompanyName, o.OrderID, o.OrderDate
  FROM Customers c
  INNER JOIN Orders o ON c.CustomerID = o.CustomerID
)

SELECT CustomerID, CompanyName, OrderID, OrderDate
FROM myCTE FOR XML AUTO

Assim que é definida, a CTE pode ser referenciada várias vezes pela primeira consulta logo após dela. Isso é especificamente útil quando uma consulta precisa se referir à CTE mais de uma vez. O exemplo de código na Figura 3 demonstra como EmpOrdersCTE é referenciado duas vezes pela consulta para que ele possa obter os funcionários e seus supervisores. Isso é muito útil quando você precisa referenciar o mesmo conjunto de linhas mais de uma vez; é mais fácil se referir à CTE duas vezes do que duplicar a consulta.

Uma CTE não precisa ser usada por uma instrução SELECT; ela está disponível para uso por parte de qualquer instrução que referencie o conjunto de linhas gerado pela CTE. Isso significa que uma CTE pode ser seguida de instruções SELECT, INSERT, UPDATE ou DELETE que usem a CTE. Também é possível usar cursores de somente avanço e instantâneo em consultas que usam CTEs.

Além disso, uma CTE pode ser seguida de outra CTE. Essa técnica pode ser usada na criação de uma CTE a partir de outra quando você deseja coletar os resultados intermediários em um conjunto de linhas. Durante a criação de uma CTE que gera outra CTE, você separa as definições de CTE com uma vírgula.

O exemplo mostrado na Figura 4 define EmpOrdersCTE, que coleta uma lista dos funcionários e o número total de pedidos de cada funcionário. A segunda CTE, chamada MinMaxOrdersCTE, consulta o primeiro EmpOrdersCTE e realiza as funções agregadas no conjunto de linhas para determinar o número médio, mínimo e máximo de pedidos por parte dos funcionários.

Figure 4 CTE referenciando outra CTE (Close Figure 4)
;WITH
EmpOrdersCTE (EmployeeID, NumOrders)
AS
(
  SELECT EmployeeID, COUNT(*)
  FROM Orders
  GROUP BY EmployeeID
),
MinMaxOrdersCTE (Mn, Mx, Diff)
AS
(
  SELECT MIN(NumOrders), MAX(NumOrders), AVG(NumOrders)
  FROM EmpOrdersCTE
)
SELECT Mn, Mx,    Diff
FROM MinMaxOrdersCTE

Várias CTEs podem ser definidas após a palavra-chave WITH, separando-as com uma vírgula. Cada CTE só pode ser referenciada pela CTE imediatamente posterior a ela, sendo criada uma a partir da outra. A instrução DML (linguagem de manipulação de dados) posterior às definições da CTE também podem se referir a qualquer uma das CTEs definidas dentro da instrução WITH.

Regras de recursão

As CTEs também podem ser usadas para implementar algoritmos recursivos. A lógica recursiva é muito útil quando você precisa escrever um algoritmo que chame a si mesmo – isso costuma ser feito para atravessar um conjunto de dados aninhado. Escrever lógica recursiva pode ser complexo, especialmente em uma linguagem como T-SQL. No entanto, esse é um dos problemas específicos para os quais as CTEs foram desenvolvidas. A fórmula básica para a criação de uma CTE recursiva é a seguinte:

  1. Crie a consulta que retorna o nível superior (trata-se do membro de ancoragem).
  2. Escreva uma consulta recursiva (trata-se do membro recursivo).
  3. Use UNION na primeira consulta em relação à consulta recursiva.
  4. Verifique se você tem um caso em que nenhuma linha será retornada (trata-se da verificação de término).

A CTE recursiva será semelhante a:

;WITH myRecursiveCTE(col1, col2, ... coln) AS
(
  -- Anchor Member Query
  UNION ALL
  -- Recursive Member Query that references myRecursiveCTE
)

Ao escrever um procedimento de recursão personalizado que não envolva CTEs, você deve incluir uma cláusula de término explícita. A cláusula de término é responsável por verificar se o algoritmo recursivo acaba sendo terminado e se recorre à pilha de chamadas recursivas. Sem essa cláusula, o código poderia acabar em um loop infinito.

No entanto, as CTEs têm dois aspectos que ajudam no tratamento de cláusulas de término. O primeiro é uma cláusula de término implícita que ocorre quando o membro recursivo retorna zero registro. Quando isso acontece, a consulta do membro recursivo não invoca a CTE recursivamente, e sim recorre à pilha de chamadas. O outro é que você pode definir explicitamente o nível de MAXRECURSION.

O nível de MAXRECURSION pode ser definido explicitamente em um lote que contém a CTE ou por meio de uma configuração do lado do servidor (a configuração em todo o servidor define 100 como padrão, a menos que você o altere). Essa configuração limita o número de vezes em que uma CTE pode se chamar recursivamente. Se o limite for atingido, uma exceção será lançada. A sintaxe para definir o nível de MAXRECURSION é usar a cláusula OPTION na instrução SELECT posterior à CTE, da seguinte forma:

-- DEFINE YOUR CTE HERE
SELECT * FROM EmpCTE
OPTION (MAXRECURSION 7)

Há outras regras das quais você deve se lembrar ao criar CTEs recursivas. Uma CTE recursiva deve conter tanto um membro de ancoragem quanto um membro recursivo. Ambos os membros devem ter o mesmo número de colunas, e as colunas que pertencem aos dois devem ter tipos de dados correspondentes. O membro recursivo só pode se referir à CTE uma vez e os membros não podem usar as seguintes cláusulas ou palavras-chave:

  • SELECT DISTINCT
  • GROUP BY
  • HAVING
  • TOP
  • LEFT/RIGHT OUTER JOIN

Aplicação da recursão

Em termos de dados e conjuntos de linhas, a recursão é usada para resolver um problema quando você precisa realizar a mesma lógica repetidamente em um mesmo conjunto de dados, em termos diferentes. Por exemplo, digamos que você seja solicitado a localizar todos os vendedores e para quem eles trabalham, além de retornar os dados em ordem hierárquica. A Figura 5 demonstra uma solução que usa uma CTE aplicando a recursão para coletar a lista dos funcionários que trabalham para o vice-presidente de vendas.

Figure 5 Captação recursiva dos vendedores (Close Figure 5)
;WITH EmpCTE(EmployeeID, EmployeeFirstName,
 EmployeeLastName, MgrID, SalesLevel)
AS
(
  -- Anchor Member
  SELECT EmployeeID, FirstName, LastName, ReportsTo, 0
  FROM Employees
  WHERE EmployeeID = 2 -- Start with the VP of Sales

  UNION ALL

  -- Recursive Member
  SELECT
    e.EmployeeID, e.FirstName, e.LastName, e.ReportsTo, m.SalesLevel+1
  FROM
    Employees AS e
    INNER JOIN EmpCTE m ON e.ReportsTo = m.EmployeeID
)

 -- Using the CTE
SELECT EmployeeID, EmployeeFirstName, EmployeeLastName,
       MgrID, SalesLevel
FROM EmpCTE

Exceção feita a alguns poucos aspectos adicionais, a CTE recursiva mostrada na Figura 5 é muito semelhante a uma CTE padrão. Onde uma CTE padrão contém uma consulta que define um conjunto de linhas, uma CTE recursiva estabelece duas definições de consulta. A primeira definição de consulta, o membro de ancoragem, estabelece uma consulta a ser executada quando a CTE for chamada. A segunda definição de consulta, o membro recursivo, determina uma consulta que retorna as mesmas colunas e tipos de dados do membro de ancoragem. O membro recursivo também recupera os valores a serem usados na chamada de retorno para a CTE recursivamente. Os resultados das consultas são obtidos simultaneamente usando uma instrução UNION.

EmpCTE na Figura 5 mostra um membro de ancoragem que capta o registro de funcionário do vice-presidente de vendas (EmployeeID = 2). A última coluna da consulta do membro de ancoragem retorna um valor igual a zero, o que representa o 0º nível da hierarquia, ou seja, a parte superior. A consulta do membro recursivo capta a lista dos funcionários que se dirigem ao funcionário anterior. Isso é feito por meio da vinculação da tabela Employees a EmpCTE.

As mesmas colunas são recuperadas do membro recursivo, mas a coluna SalesLevel é calculada usando o supervisor atual do funcionário, captando SalesLevel do supervisor e incrementando-o em um. A expressão m.SalesLevel+1 atribui SalesLevel de nível um a todas as pessoas que se dirigem diretamente ao vice-presidente de vendas (recuperado do nosso membro de ancoragem). Todos os funcionários que se dirigem a essas pessoas têm SalesLevel de nível dois. Dessa forma, SalesLevel continua sendo incrementado a cada nível subseqüente da hierarquia da organização de venda.

Resumindo

As CTEs oferecem uma forma de tornar o T-SQL muito mais legível quando comparado a cenários que usam tabelas derivadas complexas dentro de uma consulta ou ao se referir a uma exibição cuja definição é externa ao lote T-SQL. As CTEs também fornecem uma ferramenta muito aprimorada para abordar as dificuldades envolvidas com o uso de algoritmos recursivos. Independentemente de estar usando CTEs recursivas ou não, você notará que elas podem ajudá-lo a abordar muitos cenários de desenvolvimento comuns e a melhorar a legibilidade sem sacrificar o desempenho.

Envie suas dúvidas e seus comentários para John em mmdata@microsoft.com.

John Papa é consultor .NET sênior da ASPSOFT (aspsoft.com) e fanático por beisebol, passando a maioria das noites de verão torcendo pelos Yankees com sua família e Kadi, seu fiel cachorro. John, um MVP C#, é autor de vários livros sobre ADO, XML e SQL Server. Muitas vezes, ele pode ser encontrado palestrando em congressos do setor, como VSLive, ou escrevendo em seu blog em codebetter.com/blogs/john.papa.


Cutting Edge: Arquitetura de aplicativos AJAX, parte 2

Cutting Edge
Arquitetura de aplicativos AJAX, parte 2
Dino Esposito

Download do código disponível em: CuttingEdge2007_10.exe (151KB)
Conteúdo
Fazendo o logon com AJAX
Tipos de serviço
O que os serviços AJAX retornam?
JSON versus XML
Preocupações com a segurança
Serviços no ASP.NET AJAX
Serviços ASP.NET AJAX e SOAP
Invocando serviços AJAX
Criando a interface do usuário
O que tudo isso significa

Q
uando você usa as Extensões do ASP.NET AJAX para realizar a transição do site para uma experiência em AJAX, há dois modelos de programação principais para serem escolhidos: o processamento parcial e os serviços de script. Na coluna do mês passado, abordei o processamento parcial de uma perspectiva essencialmente da arquitetura. Em síntese, com o processamento parcial, você não precisa alterar a arquitetura subjacente de um aplicativo ASP.NET – trata-se de uma rota simples de implementação de alguns dos melhores elementos do AJAX como, por exemplo, atualizações sem cintilação nas páginas do site. Esse comportamento aprimorado é obtido com a simples adição de alguns novos controles do servidor (especificamente, ScriptManager e UpdatePanel) e fazendo com que eles realizem alguns truques silenciosamente para transformar um postback tradicional em uma execução de solicitação assíncrona por meio do objeto XMLHttpRequest. Essa abordagem é bem simples, uma vez que apenas aplica a funcionalidade do AJAX ao modelo de desenvolvimento da Web existente.

Caso esteja preparado para uma mudança completa de paradigmas na criação de aplicativos, você deve observar a abordagem dos serviços de script. Em termos gerais, a arquitetura típica do AJAX é bem fácil de compreender. A Figura 1 ilustra uma exibição em alto nível de como ela funciona. Há um back-end feito de serviços específicos do aplicativo, os quais normalmente são apenas uma fachada que pode ser chamada por script do AJAX na parte superior da camada intermediária em que a lógica comercial vive e funciona. Os serviços e o front-end trocam dados via HTTP usando vários formatos para passar parâmetros e retornar valores. Depois de receber e processar os dados, o front-end, que é feito de códigos JavaScript em execução no cliente, enfrenta a tarefa fora dos padrões convencionais de criar uma interface do usuário gráfica usando HTML e JavaScript. A confiança em JavaScript é uma restrição estrutural do navegador, e isso só será alterado quando ele der suporte a recursos de programação mais eficientes.

Figura 1 Arquitetura AJAX típica
Figura 1 Arquitetura AJAX típica

Fazendo o logon com AJAX

Deixar de lado o modelo clássico do ASP.NET tem inúmeras repercussões práticas. Considere o processo de logon e veja como ele seria alterado em uma solução integral em AJAX.

Atualmente com o ASP.NET, habilitar o processo de logon consiste na configuração da página de logon com o controle Login, na configuração de páginas protegidas com o controle LoginView e na definição do provedor de associação. As páginas protegidas usarão modelos anônimos ou de logon para refletir graficamente o resultado do processo de autenticação. No ASP.NET 2.0, é possível fazer grande parte do trabalho sem escrever nenhum código. No entanto, para que haja um logon bem-sucedido, há um redirecionamento (HTTP 302) quando o usuário acessa uma página protegida, um postback da página de logon para autenticar as credenciais, e outro redirecionamento mais uma vez para a página solicitada originalmente.

Não é necessariamente esse o caso com as páginas em AJAX. Caso o usuário solicite uma página protegida na barra de endereços, não há muito o que fazer senão repetir a experiência do ASP.NET. No entanto, caso você tenha links na página que apontem para páginas protegidas, é possível adicionar um script ao evento onclick do link para verificar se o usuário está autenticado. Do contrário, é possível exibir uma caixa de alerta em pop-up para avisar o usuário, como esta:

function checkFirst()
{
    var loggedIn = Sys.Services.AuthenticationService.get_isLoggedIn();
    if (!loggedIn)
    {
        alert("You must be logged in to follow this link");
        return false;
    }
    return true;
}

Como é possível ver, essa abordagem usa uma estrutura do cliente que, dentre outras coisas, permite que você verifique se o usuário atual fez o logon. Com as Extensões do ASP.NET AJAX, você faz isso por meio da classe Sys.Services.AuthenticationService.

O mesmo serviço pode ser usado para autenticar um usuário depois que ele especifica credenciais válidas. Nesse caso, não há nenhum redirecionamento ou postback. Tudo acontece no contexto da mesma página, como demonstra o seguinte trecho do código:

function OnLogin()
{
   Sys.Services.AuthenticationService.login(
      $get("UserName").value,
      $get("Password").value,
      false,
      null,
      null,
      OnLoginCompleted,
      OnLoginFailed);
}

Verificar as credenciais se torna uma operação de script padrão que usa um serviço de script na autenticação. Cabe ao desenvolvedor atualizar a interface do usuário da página do cliente para refletir se o usuário fez o logon assim que há êxito na autenticação. A menos que a estrutura usada por você forneça um modelo de objeto para encapsular HTML em controles, você tem que seguir a rota muito mais longa do script DOM (Document Object Model). Observe que as Extensões do ASP.NET AJAX atualmente não fornecem um modelo de objeto para representar blocos de marcação como controles.

Tipos de serviço

No AJAX, um serviço indica uma parte do código residente no domínio do aplicativo e expõe a funcionalidade ao código do script de cliente. Um serviço usado no AJAX exige certa criação para elaborar uma implementação que proteja o back-end real do aplicativo e a camada intermediária da interação direta com o usuário final. Trata-se de um serviço como o Web service WS-*? Ele pode ser um serviço SOA (arquitetura orientada a serviços)?

O serviço ideal para aplicativos AJAX está essencialmente voltado à exposição de dados e recursos a clientes Web. Ele pode ser acessado via HTTP e exige que o cliente use URLs (e, opcionalmente, cabeçalhos HTTP) para acessar dados e operações de comando. Os clientes interagem com o serviço usando verbos HTTP como, por exemplo, GET, POST, PUT e DELETE. Em outras palavras, a URL representa um recurso e o verbo HTTP descreve a ação que você deseja realizar em relação ao recurso. Os dados trocados nessas interações são representados em formatos simples como, por exemplo, JSON e XML sem formatação, bem como em formatos de distribuição como, por exemplo, RSS e ATOM.

Um serviço com essas características é um serviço REST (Representational State Transfer). Para obter mais informações sobre a definição de REST, você deve ler o documento original que descreve a visão por trás de REST (disponível em ics.uci.edu/~fielding/pubs/dissertation/top.htm).

Ao final, os serviços usados pelos aplicativos AJAX tendem a não usar o SOAP para se comunicar e não são necessariamente serviços autônomos em relação à SOA. Na verdade, eles estão ligados à plataforma e ao domínio em que estão hospedados. Com base nisso, eles mal podem ser chamados de Web services WS-* ou de serviços SOA.

Além disso, esses serviços têm a característica de não contar com uma documentação pública ou um esquema de descoberta – diferentemente, por exemplo, da linguagem WSDL (Web Services Description Language) dos Web services WS-*. Isso reduz o número de dependências no serviço e possibilita uma evolução mais rápida do código de serviço. Em termos gerais, o paradigma recomendado para serviços AJAX é menos ambicioso do que o paradigma por trás dos serviços Web e SOA, embora ainda seja efetivo no contexto em que ele deve ser usado.

Observe que, no restante desta coluna, eu usarei a expressão "serviços AJAX" para indicar os serviços usados na implementação do back-end de um aplicativo AJAX usando a abordagem dos serviços de script.

O que os serviços AJAX retornam?

Como os serviços AJAX são expostos exclusivamente via HTTP, é possível usar praticamente qualquer formato de texto para empacotar o corpo de solicitações e respostas. JSON (JavaScript Object Notation) é o formato mais usado, embora outros como, por exemplo, XML e texto sem formatação também possam ser usados.

JSON é um formato baseado em texto projetado para mover o estado de um objeto em todas as camadas de um aplicativo. Uma cadeia de caracteres JSON pode ser facilmente avaliada em relação a um objeto JavaScript por meio da conhecida função eval. O formato JSON descreve o objeto, como mostrado aqui:

{"ID":"ALFKI", "Company":"Alfred Futterkiste"}

A cadeia indica um objeto com duas propriedades, ID e Company, e seus respectivos valores serializados em texto. Caso uma propriedade seja atribuída a um valor não primitivo, digamos, a um objeto personalizado, o valor é serializado recursivamente em JSON desta forma:

 "ID":"ALFKI",
 "Company":"Alfreds Futterkiste",
 "Location":
      "{"City":"Berlin", "Country":"Germany"}",
}

Quando processadas com a função eval, as cadeias JSON se tornam uma matriz associativa – uma classificação da coleção nome/valor – em que cada entrada tem um nome e um valor. Caso a cadeia JSON queira representar o estado de um objeto personalizado, digamos, Customer, é de sua responsabilidade assegurar que a definição da classe correspondente esteja disponível no cliente. Em outras palavras, a função eval do JavaScript, sozinha, extrai as informações da cadeia JSON para um contêiner genérico. Caso você precise que essas informações sejam expostas como um objeto personalizado, digamos, um objeto Customer, a tarefa de fornecer a definição da classe e de carregar os dados nela cabe integralmente a você ou à estrutura usada. Para obter mais informações sobre a sintaxe e o propósito de JSON, acesse www.json.org.

JSON versus XML

Durante anos, a XML foi considerada a língua franca da Web. Agora que o AJAX está mudando a Web como nós a conhecemos, a XML está sendo deixada de lado em favor de JSON, naquilo no que se refere à representação de dados.

JSON é muito mais simples e mais apropriada para ser usada com a linguagem JavaScript. Embora seja possível discutir qual é a mais fácil de compreender por parte dos humanos, a JSON é certamente mais simples do que a XML em termos de processamento do navegador da Web. Com a JSON, você não precisa de nada como um analisador XML. Tudo o que for necessário para analisar o texto é interno da linguagem JavaScript. Como a JSON é menos ambiciosa do que a XML, ela também é menos detalhada. Não significa que a JSON seja perfeita; a grande quantidade de vírgulas e aspas exigida pela JSON faz dela um formato peculiar.

Com a JSON, você também ganha um benefício de arquitetura essencial a um custo relativamente baixo. A sua justificativa em termos de objetos está em todos os lugares. No servidor, você define e implementa as entidades como classes na linguagem gerenciada favorita. Quando um método de serviço precisa retornar uma instância de uma classe, o estado do objeto é serializado na JSON e transmitido pelo fio. No cliente, a cadeia de JSON é recebida e processada, e o conteúdo é carregado em uma matriz ou em um tipo de espelho do objeto JavaScript com a mesma interface da classe do servidor. A interface da classe é inferida do fluxo da JSON. Dessa forma, os códigos da página do cliente e do serviço usam a mesma definição lógica de uma entidade.

De um ponto de vista essencialmente técnico, os serviços AJAX não exigem que a JSON seja implementada como sendo o formato de representação dos dados. Você obteria o mesmo resultado usando a XML, mas precisaria de um analisador XML que pudesse ser usado no JavaScript. Analisar um texto XML simples em JavaScript talvez não seja um problema, mas usar um analisador completo é uma outra história. Os problemas relacionados ao desempenho e à funcionalidade deverão levar à existência de inúmeros componentes aparentemente semelhantes, mas que têm pouca coisa em comum. E, dessa forma, resta a dúvida se o analisador XML do JavaScript dará suporte a coisas como namespaces, esquemas, espaços em branco, comentários, instruções de processamento etc.

Na minha opinião, tendo em vista a compatibilidade, você acabaria com um subconjunto de XMLs limitado a nós e atributos. Nesse ponto, é meramente uma questão de optar entre colchetes de XML e as chaves de JSON.

Preocupações com a segurança

O AJAX possibilita uma experiência de navegação interativa e mais dinâmica. No entanto, isso aumenta a área da superfície para tipos comuns de ataques como, por exemplo, XSS (script em diferentes locais) e CSRF (falsificação de solicitação em diferentes locais). Esses tipos de ataque são causados por um invasor que insere um código de script em uma página da Web, normalmente por meio de uma URL, o que permite ao invasor controlar o navegador da Web – realizando ações como, por exemplo, roubar nomes de usuário e senhas ou executando solicitações HTTP sem o conhecimento do usuário.

Um invasor poderia, por exemplo, inserir um script mal-intencionado no cliente usando uma marca <script> criada dinamicamente, o que possibilita a importação dos dados para o site do invasor. No caso de um ataque CSRF, o invasor poderia inserir um script no cliente, permitindo que ele executasse métodos de serviço não autorizados em outro site usando as informações salvas sobre autenticação (como, por exemplo, cookies) no cliente.

JSON é uma forma eficiente de empacotar e distribuir dados ao cliente. No entanto, como a JSON é considerada um subconjunto seguro da linguagem JavaScript (ela exclui as operações de atribuição e de invocação), muitos aplicativos AJAX apenas passam as cadeias de JSON diretamente para a função eval do JavaScript para criar objetos JavaScript. Dessa forma, uma cadeia de JSON malformada oferece um novo meio para que um invasor execute o script não autorizado no cliente.

Grande parte desses tipos de ataque depende da exploração do verbo GET em uma solicitação HTTP. Felizmente, os Serviços ASP.NET AJAX têm o verbo GET desabilitado por padrão nos serviços remotos. Além disso, é possível exigir que um tipo de conteúdo especial seja definido na solicitação: em qualquer outro caso, um serviço ASP.NET AJAX recusará a chamada. Por design, qualquer chamada direta feita de uma marca <script> para um serviço ASP.NET AJAX tem o tipo de conteúdo errado e falha.

Serviços no ASP.NET AJAX

Com as Extensões do ASP.NET AJAX, é possível implementar serviços de script de duas formas – usando um tipo especial de Web services do ASP.NET e via métodos de página. No caso anterior, você apenas projeta e cria uma classe vinculada a um recurso ASMX:

<%@ WebService Language="C#"
    CodeBehind="~/App_Code/TimeService.cs"
    Class="IntroAjax.WebServices.TimeService" %>

Opcionalmente, a classe pode ser herdada da classe WebService e deve ser decorada com o novo atributo ScriptService:

[ScriptService]
public class TimeService : System.Web.Services.WebService
{
   ...
}

Cada método chamado por script é declarado público e marcado com o atributo WebMethod usual.

Os métodos de página são apenas métodos públicos, estáticos – cada um marcado como sendo WebMethod – definidos no contexto de uma página única do ASP.NET. Eles só podem ser chamados na página host. Além das diferenças de armazenamento, uma chamada para um Web service ou método de página é processado da mesma forma pelo ambiente do ASP.NET AJAX.

Você precisa compreender que os serviços AJAX representam uma parte do back-end. Eles não são Web services públicos em termos de um Web service WS-*, que são totalmente documentados por um script WSDL e acessados por meio de comandos POST que transportam dados SOAP. Os serviços AJAX são, na realidade, serviços locais, normalmente definidos no mesmo aplicativo que os chama. No entanto, eles também podem estar em um aplicativo Web diferente ou mesmo em um site, desde que estejam no mesmo domínio.

O atributo ScriptService desempenha um papel essencial ao permitir que o runtime do ASP.NET AJAX aceite chamadas para o serviço. Sem o atributo ScriptService, uma exceção é lançada no servidor quando você tenta fazer as chamadas. A Figura 2 mostra a mensagem retornada quando uma página do AJAX é vinculada a um serviço não sinalizado com o atributo. Observe que a página mostrada na figura jamais é exibida a um usuário. Você recebe toda a marcação ao se conectar a uma página do ASP.NET AJAX que declara usar um serviço como esse. A marcação retorna um código de erro HTTP 500 porque o mecanismo interno do ASP.NET se recusa a processar chamadas de script direcionadas em Web services do ASP.NET que não tenham o atributo.

Figura 2 Página fazendo referência a um serviço não programável
Figura 2 Página fazendo referência a um serviço não programável(Clique na imagem para diminuir a exibição)
Figura 2 Página fazendo referência a um serviço não programável
Figura 2 Página fazendo referência a um serviço não programável (Clique na imagem para aumentar a exibição)

Por padrão, os métodos de serviço AJAX são invocados usando apenas o verbo POST e retornam dados formatados em JSON. No entanto, usando o atributo ScriptMethod opcional em métodos de serviço individuais, é possível alterar essas configurações. A Figura 3 detalha os parâmetros para os quais o atributo ScriptMethod dá suporte. Os dados retornados ao cliente podem ser alterados para XML, sendo possível até mesmo adicionar suporte a solicitações GET. Mas como mencionei, essa alteração aparentemente inocente pode abrir novas possibilidades para invasores e expõe o potencial de chamadas em diferentes locais para o método. O seguinte trecho do código mostra a definição do método de um Web service:

Figure 3 Parâmetros do atributo ScriptMethod (Close Figure 3)

Parâmetro | Descrição
ResponseFormat | Especifica se a resposta será serializada como sendo JSON ou XML. Embora o padrão seja JSON, o formato XML pode ser útil quando o valor de retorno do método é XmlDocument.
UseHttpGet | Indica se um verbo GET HTTP pode ser usado para invocar o método de Web service. Por razões de segurança, ele é definido como falso por padrão.
XmlSerializeString | Indica se todos os tipos de retorno, inclusive as cadeias, são serializados como XML. O padrão é falso. O valor da propriedade é ignorado quando o formato de resposta está definido como JSON.

[WebMethod]
[ScriptMethod]
public DateTime GetTime()
{
   ...
}

Caso você não pretenda alterar nenhuma das configurações padrão (que é o que eu recomendo na maioria dos casos), o atributo ScriptMethod pode ser omitido. No entanto, o atributo WebMethod é obrigatório.

Serviços ASP.NET AJAX e SOAP

Assim que for criado, um serviço Web AJAX será publicado como sendo um recurso ASMX. Por padrão, trata-se de uma URL pública e que pode ser usada por clientes AJAX, bem como descoberta e usada por clientes e ferramentas SOAP. Mas é possível optar por desabilitar clientes e ferramentas SOAP simultaneamente. Basta inserir as seguintes definições de configuração no arquivo web.config do aplicativo ASP.NET que hospeda o serviço:

<webServices>
   <protocols>
      <clear />
   </protocols>
</webServices>

Essa configuração simples desabilita todos os protocolos definidos para os Web services ASP.NET 2.0 (em especial, SOAP) e permite que o serviço responda apenas a solicitações de JSON. Observe que, com essas configurações, não é mais possível chamar o Web service usando a barra de endereços do navegador tendo em vista um teste rápido. Da mesma forma, não é possível solicitar à WSDL a inclusão do sufixo ?wsdl à URL.

Para poder habilitar o ASP.NET AJAX em um aplicativo Web, você deve incluir as seguintes configurações no arquivo web.config:

<httpHandlers>
  <remove verb="*" path="*.asmx"/>
  <add verb="*" path="*.asmx" validate="false"
       type="System.Web.Script.ScriptHandlerFactory, System.Web.Extensions" />
</httpHandlers>

O nó <remove> descarta o manipulador HTTP padrão dos recursos ASMX – o que trata solicitações via SOAP. O nó <add> adiciona um novo manipulador HTTP que basicamente verifica o tipo de conteúdo de cada solicitação ASMX de entrada e a atende via JSON caso o cabeçalho do tipo de conteúdo esteja definido como application/json. Do contrário, o manipulador HTTP presume que a solicitação baseia-se em SOAP e a encaminha para o manipulador do Web service ASP.NET 2.0 padrão. A solicitação será rejeitada se o protocolo SOAP estiver desabilitado.

Ao final, e apesar das aparências, não há nenhum SOAP necessariamente envolvido com um serviço ASP.NET AJAX. Mas o suporte a clientes SOAP é garantido, a menos que esteja explicitamente desabilitado no arquivo web.config.

Para que um serviço ASP.NET AJAX funcione conforme o esperado, a solicitação de entrada deve ter o cabeçalho HTTP do tipo conteúdo definido como application/json. Também há uma excelente solução contra ataques em diferentes locais realizados por meio da marca <script>.

Invocando serviços AJAX

Para invocar um serviço AJAX, um cliente AJAX segue o mesmo padrão usado para referenciar Web services em aplicativos Windows e ASP.NET clássicos. Uma classe de proxy oferece a mesma interface localmente como sendo o serviço remoto. Em aplicativos ASP.NET AJAX, esse proxy é uma classe JavaScript gerada pelo runtime quando a página é baixada.

A classe de proxy do JavaScript tem o mesmo nome do serviço de script, além de algumas propriedades adicionais. Ela apresenta o mesmo conjunto de métodos, muito embora com uma assinatura um pouco estendida. Em geral, não há nenhuma necessidade de rastrear o código-fonte da classe de proxy. Mas, caso você queira observar sua estrutura, tente invocar a seguinte URL na barra de endereços do navegador:

http://.../service.asmx/js

O navegador baixará um arquivo JavaScript que é possível salvar no disco local para uma leitura minuciosa posterior.

A classe de proxy do JavaScript é herdada de uma classe base chamada Sys.Net.WebServiceProxy. Isso proporciona os recursos básicos para que haja chamadas JSON. O código baixado referente a esta coluna oferece uma classe de proxy para um Web service com a seguinte interface:

interface ITimeService
{
    DateTime GetTime();
    string GetTimeFormat(string format);
}

A classe de proxy do JavaScript apresenta as propriedades listadas na Figura 4. Cada método espelhado tem três parâmetros, além do conjunto regular de argumentos. Eles formam a função de retorno de chamada a ser chamada caso o método seja bem-sucedido, a função de retorno de chamada a ser chamada caso haja falha no método ou ele expire e o objeto de contexto a ser passado para ambas as chamadas de retorno. As três propriedades relacionadas ao padrão mostradas na Figura 4 permitem que você reutilize a mesma função em várias chamadas – uma função exclusiva do JavaScript para tratar erros, por exemplo. Aqui está um código de exemplo que invoca um serviço AJAX remoto em uma página do ASP.NET AJAX:

Figure 4 Propriedades da classe de proxy do JavaScript (Close Figure 4)

Propriedade | Descrição
Path | Indica a URL do Web service subjacente.
Timeout | Indica por quantos milissegundos o método tem permissão para ser executado antes da chamada de timeout.
defaultSucceededCallback | Indica a função de retorno de chamada JavaScript padrão a ser invocada em caso de uma chamada bem-sucedida.
defaultFailedCallback | Indica a função de retorno de chamada JavaScript padrão, caso haja alguma, a ser invocada em caso de falha ou expiração de uma chamada.
defaultUserContext | Indica o objeto JavaScript padrão, caso haja algum, a ser passado para retornos de chamada bem-sucedidos e malsucedidos.

function getTime()
{
    IntroAjax.WebServices.TimeService.GetTimeFormat(
        "ddd, dd MMMM yyyy [hh:mm:ss]", onMethodComplete);
}

function onMethodComplete(results)
{
    $get("Label1").innerHTML = results;
}

As chamadas de retorno solicitadas ao final de uma chamada de método (independentemente do resultado) têm o seguinte protótipo:

function method(results, context, methodName)

O parâmetro context representa o objeto context especificado quando a chamada foi feita. O parâmetro methodName é um conjunto de cadeias para o nome do método de serviço. Por fim, para um retorno de chamada invocado para uma chamada bem-sucedida, o parâmetro results é um objeto que contém a versão do JavaScript do valor de retorno do método. Já para a chamada de retorno malsucedida, isso representa um objeto Sys.Net.WebServiceError.

Criando a interface do usuário

O AJAX está relacionado a praticamente toda a experiência do usuário – aparência contínua, atualizações sem cintilação, recursos de interface, mashups, dados dinâmicos etc. Mas é possível utilizar apenas o navegador e seu conjunto de recursos de programação – essencialmente, o modelo de objeto do navegador, a implementação DOM, o suporte às extensões DHTML, a CSS, o JavaScript e os plug-ins.

JavaScript é a principal ferramenta para criar e manipular a interface do usuário. O padrão típico das tarefas de interface do usuário exige que o cliente que esteja usando o JavaScript invoque serviços remotos, recebendo dados JSON ou talvez XML e, em seguida, reorganizando a página para mostrar as alterações.

Um modelo simples como esse não é necessariamente efetivo quando projetado de acordo com o tamanho e a complexidade de um aplicativo real. Reorganizar a página para incorporar dados atualizados após uma chamada remota apresenta problemas fora dos padrões convencionais, na medida em que a estrutura da interface do usuário fica cada vez mais sofisticada. A questão é: onde está o limite no qual uma interface do usuário sofisticada se torna um problema?

Há essencialmente três recursos da interface do usuário dos quais dependem todos os aplicativos realistas, especialmente os de linha de negócios: layout, ligação de dados e estilo. O ambiente JavaScript não dá suporte para nenhum deles, senão para alguns estilos por meio da CSS. Além disso, JavaScript é uma linguagem interpretada em que, por exemplo, a programação intensiva costuma resultar em problemas de desempenho por conta de pequenos bugs no navegador e da má programação.

Considere um cenário relativamente comum e simples – a paginação de uma grade de dados. Em última análise, uma grade é uma tabela bastante complexa que o navegador precisa analisar e processar para cada solicitação. É difícil para um usuário avaliar o custo dessa operação de processamento na primeira vez em que a página é atendida, uma vez que ela é incorporada ao download de toda a página. Mas assim que uma solicitação de postback AJAX é feita para uma nova página de grade, o custo de atualização da janela do navegador se torna logo evidente. O navegador recebe uma cadeia de JSON que contém uma coleção dos objetos e deve transformá-la em uma nova tabela. Uma grande cadeia HTML deve ser criada no cliente, redigindo o texto em algum buffer de memória. A mesma cadeia, caso esteja correta, deve ser processada graficamente.

Quando essas operações intensivas são realizadas, você talvez observe um tempo de resposta inferior ao ideal. Para cenários como esse, são necessárias habilidades acima da média em JavaScript e DHTML para apresentar soluções efetivas.

Uma abordagem alternativa é gerar previamente algumas marcações no servidor. Dessa forma, o serviço remoto não apenas retorna os dados, mas também incorpora informações sobre a marcação. No servidor, a marcação é criada por meio de código compilado, que pode ser mais facilmente depurado e testado, e você tem acesso a ferramentas de programação mais eficientes para adicionar recursos de acessibilidade. Por outro lado, mais dados são transmitidos (mas não se esqueça de que a quantidade de dados ainda é muito menor do que com postbacks regulares do ASP.NET).

O resultado final é que são necessárias mais ferramentas eficientes no cliente para que seja possível criar interfaces do usuário reais efetivas das perspectivas de desempenho e desenvolvimento. Boas bibliotecas completas de widgets e controles são essenciais para qualquer desenvolvedor. Mas mesmo as melhores e mais otimizadas bibliotecas não podem fazer muito em relação à limitação inerente de uma linguagem interpretada como, por exemplo, JavaScript. Talvez o Silverlight, um plug-in de navegador entre plataformas Microsoft que incorpora um subconjunto da estrutura WPF (Windows® Presentation Foundation), venha a oferecer o ambiente do lado do cliente desejado pelos desenvolvedores da Web para a próxima geração dos aplicativos Web e AJAX. Mas Silverlight é um plug-in externo; ele exige um download separado e ainda está em desenvolvimento. A versão 1.0 acabou de ser lançada, embora sejam necessários recursos mais sofisticados para que ela seja usada na camada de apresentação de aplicativos Web reais.

O que tudo isso significa

O processamento parcial é a abordagem mais simples para o AJAX. Isso é especialmente apropriado à adição de recursos do AJAX a aplicativos herdados para os quais você não tem tempo, orçamento ou vontade de refazer. De um ponto de vista da arquitetura, o processamento parcial é uma extensão inteligente do ASP.NET atual que preserva o mesmo modelo de aplicativo e o mecanismo subjacente.

Uma arquitetura totalmente em AJAX baseia-se na menor rigidez entre cliente e servidor – essencialmente, dois mundos separados ligados via HTTP a mensagens de troca JSON. Em uma arquitetura AJAX pura, você tem um back-end com base no serviço e um front-end com JavaScript. O trabalho de criar uma interface do usuário efetiva em HTML é todo seu ou da biblioteca de controles escolhida por você. No entanto, essa menor rigidez possibilitará que tecnologias emergentes como, por exemplo, Silverlight, continuem permitindo a desenvolvedores da Web criar interfaces do usuário mais interativas sem que sejam restringidos pela plataforma do servidor.

Envie suas dúvidas e seus comentários para Dino em cutting@microsoft.com.

Dino Esposito é um dos mentores da Solid Quality Learning e o autor do próximo Programming ASP.NET 3.5 Core Reference (Referências Básicas de Programação do ASP.NET 3.5; Microsoft Press, 2007). Residente na Itália, Dino é um palestrante assíduo em eventos do setor no mundo inteiro. É possível visitar seu blog em weblogs.asp.net/despos.


Execu&#231;&#227;o de teste: A automa&#231;&#227;o de teste com o Windows XP Embedded

Execução de teste
A automação de teste com o Windows XP Embedded
Dr. James McCaffrey and Mike Hall

Download do código disponível em: TestRun2007_10.exe (1402KB)
Conteúdo
O aplicativo em teste
A automação de teste
Portando a automação de teste de interface do usuário para um sistema Windows XP Embedded
Criando componentes
Componentes de sistema operacional
Crie a sua imagem
Implante a sua imagem
Uma recapitulação rápida

V
ocê já trabalhou com o sistema operacional Windows® XP Embedded? Essencialmente, ele é uma versão personalizada e reduzida por desenvolvedores do sistema operacional Windows XP para ser usada em dispositivos semelhantes ao PC £ consoles de jogos, quiosques de informações, dispositivos de rede e afins. Com a utilização cada vez maior de dispositivos móveis e incorporados, essa plataforma vai se tornando mais importante.

Descobri que há um tipo de lacuna de conhecimento entre os engenheiros de teste de software e os desenvolvedores que trabalham com os sistemas de software baseados no Windows XP Embedded. Para ajudar a diminuir essa lacuna, juntei-me a Mike Hall, um gerente de produto sênior do grupo Mobile and Embedded Devices da Microsoft® e especialista em Windows XP Embedded, para escrever esta coluna. Juntos, explicaremos como você pode criar automações de teste de interface do usuário leves mas poderosas para sistemas de software executados no Windows XP Embedded.

A melhor maneira de mostrar a você o que pretendemos é exibindo uma captura de tela. A Figura 1 mostra uma automação de teste de interface do usuário leve em ação. Mas o que não é evidente na captura de tela é que a automação está sendo executada em um dispositivo com o Windows XP Embedded.

Figura 1 A automação de interface do usuário no Windows XP Embedded
Figura 1 A automação de interface do usuário no Windows XP Embedded(Clique na imagem para diminuir a exibição)
Figura 1 A automação de interface do usuário no Windows XP Embedded
Figura 1 A automação de interface do usuário no Windows XP Embedded (Clique na imagem para aumentar a exibição)

Se você examinar a Figura 1 com cuidado, verá que a automação de teste da interface do usuário está mostrando um aplicativo baseado em um formulário do Windows representativo, similar ao que você pode encontrar em um dispositivo de quiosque com o Windows XP Embedded. A automação de teste é um aplicativo de console que:

  • executa o aplicativo baseado no formulário em teste e manipula o aplicativo
  • simula uma pesquisa de um produto com o texto "idg" em seu nome
  • examina o estado resultante do aplicativo para procurar por "111" nos resultados
  • determina o resultado de um cenário de teste

Nos bastidores, a automação de teste está manipulando o aplicativo em teste usando o mecanismo P/Invoke do Microsoft .NET Framework para chamar funções de API do Win32®, como FindWindow, SendMessage e GetMenu.

Na coluna deste mês, discutiremos rapidamente o aplicativo em teste e examinaremos a automação de teste que produziu os resultados mostrados na Figura 1. Em seguida, descreveremos em detalhes como usar as ferramentas do Windows Embedded Studio para criar um sistema Windows XP Embedded que contenha o aplicativo e a automação de teste de uma forma relativamente simples. Examinaremos também uma técnica mais sofisticada que cria a automação de teste como um shell personalizado do Windows XP Embedded. Concluiremos com uma breve descrição de como adaptar e ampliar as idéias aqui apresentados para atender às suas necessidades específicas.

Se você ainda não conhece o Windows XP Embedded, encontrará nesta coluna uma ótima introdução a uma tecnologia interessante. E se ainda não conhece a automação de interface do usuário baseada em Win32 que utiliza a interface do .NET Framework, encontrará nesta coluna uma boa introdução à técnica.

O aplicativo em teste

Vamos dar uma olhada no código principal do aplicativo baseado em formulário do Windows que está sendo testado £ o coração de um típico sistema de software do Windows XP Embedded. Todo o código do aplicativo e da automação de teste está incluído no download de código que acompanha este artigo. Como você pode ver na Figura 1, o aplicativo fictício possui controles de usuário básicos, incluindo um controle TextBox, um controle ComboBox, um controle Button e um controle ListBox. Em um cenário realista, um aplicativo de pesquisa Windows XP Embedded provavelmente recuperaria resultados de pesquisa de um servidor de banco de dados ou talvez de um Web service. Aqui, o aplicativo mantém um armazenamento de dados local na forma de uma classe Product simples:

public class Product
{
    public string id, name, price;

    public Product(string id, string name, string price)
    {
        this.id = id; this.name = name; this.price = price;
    }
}

Os dados para as instâncias de Product estão armazenados em uma coleção ArrayList e são fornecidos quando o objeto principal de Form é carregado:

private ArrayList al = new ArrayList();
private void Form1_Load(object sender, System.EventArgs e)
{
    Product p1 = new Product("111", "Widget", "$11.11");
    Product p2 = new Product("222", "Gadget", "$22.22");
    Product p3 = new Product("333", "Thingy", "$33.33");
    al.Add(p1); al.Add(p2); al.Add(p3);
}

Como estamos executando a automação de interface do usuário, não importa muito para nós de onde ou como o aplicativo de pesquisa recupera seus dados £ não importa de onde vêm os dados resultantes, eles se refletirão na interface do usuário do aplicativo, que pode ser examinada para a determinação do resultado de êxito/falha do cenário de teste. Toda a lógica do aplicativo acontece no método click do controle Button de pesquisa. O aplicativo começa ao verificar, de forma bruta, se o usuário especificou um critério de pesquisa £ por identificação de produto ou por sua descrição. Em caso negativo, o aplicativo dispara uma janela MessageBox com uma mensagem de erro:

if (comboBox1.Text == "" || textBox1.Text == "") {
    MessageBox.Show("You must enter search criteria and term",
                    "Error");
}
listBox1.Items.Clear();

Depois de limpar o controle ListBox, que armazena os resultados da pesquisa, o aplicativo recupera a cadeia de caracteres de critérios/destino da pesquisa e atravessa o armazenamento de dados de ArrayList procurando por ocorrências:

string filter = textBox1.Text;
if (comboBox1.Text == "Product ID") {
    foreach (Product p in al) {
        if (p.id.IndexOf(filter) >= 0) {
            listBox1.Items.Add(p.id + " " + p.name + " " + p.price);
        }
    }
}
else if (comboBox1.Text == "Product Name") {
    // search by Name similarly
}

Vamos enfatizar que, deliberadamente, não estamos usando um bom estilo ou boas práticas de codificação para mantermos o código curto e inteligível e também para simularmos a natureza relativamente não refinada de um aplicativo nos primeiros estágios de desenvolvimento. Na verdade, o código de exemplo possui diversas falhas de lógica significativas que talvez você se divirta em procurar.

Se você ainda não conhece o Windows XP Embedded, pode estar se perguntando "o que esse aplicativo tem a ver com o Windows XP Embedded? Ele se parece com um aplicativo baseado em Windows Forms totalmente comum". A sua observação está correta £ e esse é um ponto importante. Como o Windows XP Embedded é, essencialmente, uma versão dividida em componentes do Windows XP, todas as suas habilidades são diretamente transferidas. Isso confere ao Windows XP Embedded uma enorme vantagem sobre plataformas de desenvolvimento alternativas para dispositivos incorporados. Não é preciso aprender uma plataforma e um conjunto de ferramentas totalmente novos, você terá acesso a uma grande comunidade de suporte e poderá usar o poder da biblioteca do .NET Framework e, como esta coluna explicará, poderá usar técnicas de automação de teste poderosas.

A automação de teste

Existem várias formas de criar a automação de teste da interface do usuário. A técnica que usaremos aqui será escrever um equipamento de teste de aplicativo de console em C# que manipule o aplicativo baseado em formulário em teste ao chamar funções nativas da API do Win32 usando o mecanismo P/Invoke. A coluna Execução de teste, de setembro de 2005, "Automação de teste de interface do usuário de baixo nível" (disponível em msdn.microsoft.com/msdnmag/issues/05/09/testrun) discute esse tópico em detalhes.

Começamos a automação de teste ao executarmos o aplicativo em teste usando o método estático Start da classe Process do namespace System.Diagnostics:

Console.WriteLine("\nLaunching Windows application");
Process p = Process.Start(".\\TheAppToTest.exe");

Observe que o código de exemplo supõe que o executável do aplicativo em teste está no mesmo diretório do executável do seu equipamento de teste. Como você verá em breve, esse design é especialmente conveniente para a automação de teste nos sistemas Windows XP Embedded. Depois de executar o aplicativo em teste, usamos a função FindWindow do Win32 para obter um identificador do objeto de Form do aplicativo em teste, como mostrado na Figura 2.

Figure 2 Obtendo um identificador (Close Figure 2)
Console.WriteLine("\nLooking for Form1");
IntPtr ptrToForm;
bool formFound = false;
int numTries = 0;
while (!formFound && numTries < 100) {
    ++numTries;
    ptrToForm = FindWindow(null, "Form1");
    if (ptrToForm == IntPtr.Zero) {
        Console.WriteLine("Form1 not found yet. . . ");
        Thread.Sleep(100);
    }
    else {
        formFound = true;
        Console.WriteLine("Form1 found with ptr = " +
            ptrToForm.ToString());
    }
}
if (numTries >= 100) throw new Exception("Form was not found");

O coração da técnica P/Invoke é importar a função FindWindow do Win32. Fazemos isso ao colocarmos um atributo DllImport no equipamento de teste:

[DllImport("user32.dll", EntryPoint = "FindWindow",
           CharSet = CharSet.Auto)]
static extern IntPtr FindWindow(
    string lpClassName, string lpWindowName);

Você pode interpretar livremente esse atributo como: "importe uma função do Win32 da biblioteca user32.dll. A função do Win32 é chamada de FindWindow (embora o sistema também deva verificar a existência do mesmo nome terminando com 'A' ou 'W'). E lide automaticamente com problemas de codificação de caracteres ASCII e Unicode. O alias C# é um método estático que retorna um tipo IntPtr do .NET e que representa o identificador de uma janela. O alias C# chama-se FindWindow e aceita dois argumentos de cadeia de caracteres £ o nome da classe de controle da janela e o nome amigável do controle".

O código da Figura 2 tenta encontrar a janela Form; se ela não for encontrada (normalmente porque o objeto de Form ainda não foi inicializado), o código aguardará 100 milissegundos e tentará novamente. Se o objeto de Form não for encontrado após 100 tentativas, a automação de teste lançará uma exceção. Depois que o equipamento de automação executar o aplicativo em teste, manipulará o objeto de Form redimensionando e movendo o Form. Por exemplo:

Console.WriteLine("\nMoving Form1 around");
SetWindowPos(ptrToForm, IntPtr.Zero, 100, 200, 336, 404, 0);

Isso utiliza a função SetWindowPos do Win32 para posicionar o canto superior esquerdo do objeto de Form nas coordenadas da tela (100,200) com uma largura de 336 pixels e uma altura de 404 pixels.

O atributo DllImport associado para o nosso alias C# é:

[DllImport("user32.dll", EntryPoint = "SetWindowPos")]
static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter,
    int x, int y, int cx, int cy, uint uFlags);

Depois de manipular o objeto de Form, obteremos identificadores para os controles de janela filhos de Form. Por exemplo, o código do equipamento de teste usado na obtenção de um identificador para o controle button1 é:

Console.WriteLine("Looking for button1");
IntPtr btn = FindWindowEx(ptrToForm, IntPtr.Zero, null, "Search");
if (btn == IntPtr.Zero)
    throw new Exception("Could not find search button");
else
    Console.WriteLine("button1 found with ptr = " + butt.ToString());

Isso utiliza a função FindWindowEx do Win32 para obter um identificador de um controle filho com o atributo Name chamado "Search".

O código usado na obtenção de um identificador para o controle TextBox é um pouco mais complicado:

Console.WriteLine("\nLooking for textBox1");
IntPtr tb = FindWindowByIndex(ptrToForm, 3);
if (tb == IntPtr.Zero)
    throw new Exception("Could not find textBox");
else
    Console.WriteLine("textBox1 found with ptr = " + tb.ToString());

Aqui, estamos usando um método definido pelo programa e que é extremamente conveniente, chamado FindWindowByIndex.

Embora alguns controles de janela tenham atributos Name (como Form1), muitos outros não os possuem. Para obter um identificador de um controle de janela não-nomeado, use FindWindowByIndex, definido como:

static IntPtr FindWindowByIndex(IntPtr hwndParent, int index)
{
    int ct = 0;
    IntPtr result = IntPtr.Zero;
    do {
        result = FindWindowEx(hwndParent, result, null, null);
        if (result != IntPtr.Zero) ++ct;
    } while (ct < index && result != IntPtr.Zero);
    return result;
}

A idéia é de que, mesmo que alguns controles de janela não tenham atributos Name, todos eles têm um número de índice implícito. Você pode usar a função FindWindowEx do Win32 e o número de índice implícito de um controle para obter um identificador do controle. O número do índice implícito de um controle de janela não é o mesmo da ordem do controle de tabulação. O número do índice implícito é determinado pela ordem em que o controle de janela é adicionado a uma janela pai. Assim, em nosso exemplo, especificamos FindWindowByIndex(ptrToForm, 3) porque o código do aplicativo em teste contém:

this.label1 = new System.Windows.Forms.Label();       // index 0
this.label2 = new System.Windows.Forms.Label();       // 1
this.comboBox1 = new System.Windows.Forms.ComboBox(); // 2
this.textBox1 = new System.Windows.Forms.TextBox();   // 3 <-
this.button1 = new System.Windows.Forms.Button();
// etc.

O código restante do equipamento de teste manipula o aplicativo em teste ao enviar caracteres para o controle TextBox, clicar no controle Button, aguardar a exibição de uma MessageBox de erro, clicar em seu botão OK e assim por diante. O equipamento de teste termina examinando o conteúdo do controle ListBox para determinar um resultado de cenário de êxito ou de falha.

Portando a automação de teste de interface do usuário para um sistema Windows XP Embedded

Um conceito fundamental em sistemas Windows XP Embedded é a noção do shell do sistema operacional, em vez de um aplicativo do shell em execução em um sistema operacional. Todos os sistemas operacionais baseados em Windows devem ter exatamente uma, e somente uma, interface primária de shell. Por exemplo, no sistema operacional Windows XP padrão, normalmente o Navegador do Shell é o shell primário, embora você também possa ter vários shells de aplicativo, como o shell de comando cmd.exe, o shell Windows PowerShell® e assim por diante. Uma vez que o Windows XP Embedded é uma versão personalizável e dividida em componentes do Windows XP, você não está limitado a utilizar o Navegador do Shell como o shell primário, mas pode escolher qualquer um dos diversos shells ou até mesmo criar um shell primário personalizado.

Na barra lateral, "A automação de teste como um shell personalizado", demonstraremos como portar a automação de teste de interface do usuário para um sistema Windows XP Embedded quando um shell primário não personalizado estiver sendo usado. Na próxima seção, apresentaremos uma abordagem mais sofisticada para a portagem da automação de teste durante a utilização de um shell primário personalizado.

Na terminologia da documentação, "dispositivo" pode se referir a um conjunto de equipamentos no qual o Windows XP Embedded será executado (como em um quiosque de informações) ou a um item de hardware individual (como um mouse, teclado ou adaptador de vídeo). Neste exemplo, o dispositivo de destino será um laptop. Em um cenário mais realista, o dispositivo de destino provavelmente seria algo menos parecido com um PC £ talvez algo com um teclado reduzido ou com um monitor de vídeo pequeno. Mas os princípios de desenvolvimento do Windows XP Embedded são exatamente os mesmos.

O dispositivo de destino do nosso exemplo possui um único disco rígido físico com uma partição C: e outra D:. A nossa intenção é copiar o sistema Windows XP Embedded para a partição D:. Para criar um sistema Windows XP Embedded, você pode usar o Windows Embedded Studio, que oferece uma coleção de ferramentas e um banco de dados back-end com componentes de software. Normalmente, você não instala as ferramentas do Windows XP Embedded no dispositivo de destino, mas em uma máquina de desenvolvimento (com o Windows XP, em geral), cria uma imagem do sistema para o seu sistema operacional Windows XP Embedded e seu aplicativo personalizados em sua máquina de desenvolvimento e implanta a imagem no dispositivo de destino.

A criação de uma imagem do Windows XP Embedded pode parecer confusa a princípio, mas após algumas iterações o processo se torna fácil de entender. Antes de orientarmos você pelos detalhes de um exemplo específico, mostraremos uma visão geral de alto nível do processo. A primeira etapa do processo de criação da imagem é analisar os dispositivos de hardware individuais em seu dispositivo de destino usando um utilitário chamado Target Analyzer Probe, contido no conjunto de ferramentas do Windows XP Embedded. A próxima etapa é criar componentes do Windows XP Embedded usando uma ferramenta chamada Component Designer (falaremos mais sobre esse assunto em um minuto). A etapa três serve para combinar componentes e, com eles, criar uma imagem do Windows XP Embedded. A quarta etapa é implantar a imagem no dispositivo de destino. Como você pode ver, em princípio, o processo é simples.

Agora mostraremos a você os detalhes da criação da automação de teste de interface de usuário do Windows XP Embedded. Você começa pela análise dos itens de hardware no dispositivo de destino usando o Target Analyzer Probe (tap.exe; existem também um programa ta.exe, que pode ser usado em sistemas de 16 bits). Basta copiar o utilitário tap.exe para o dispositivo de destino. A forma como essa cópia será executada dependerá dos dispositivos de E/S disponíveis no dispositivo de destino (observe que o utilitário tap.exe requer o Windows 2000 ou o Windows XP; como alternativa, você pode usar o disco de instalação do Windows XP Embedded, inicializável para essa finalidade).

Depois que o tap.exe for copiado para o dispositivo de destino, bastará executá-lo. O resultado será um arquivo chamado devices.pmq (com freqüência chamado de "arquivo pumpkin" pelas pessoas da área), armazenado no mesmo diretório de tap.exe. Esse arquivo contém uma lista de todos os itens de hardware encontrados no dispositivo de destino. E já que o arquivo está em formato XML, será possível lê-lo em qualquer editor de texto para verificar se todos os dispositivos de hardware foram encontrados. Além disso, você pode criar o arquivo manualmente ou editá-lo, se necessário. Depois da criação de devices.pmq, copie-o do dispositivo de destino para a máquina de desenvolvimento.

Criando componentes

As próximas etapas envolvem a criação de Componentes usando uma ferramenta baseada em GUI chamada Component Designer. Você pode pensar em um componente do Windows XP Embedded como um pacote de software relacionado. Observe que, neste artigo, usaremos "Componente" (com C maiúsculo) quando nos referirmos a um pacote do Windows XP Embedded e "componente" (em minúsculas) quando fizermos uma referência geral a uma coleção de itens.

As ferramentas do Windows Embedded funcionam com Componentes em vez de arquivos individuais porque o grande número de arquivos envolvido em uma imagem do Windows XP Embedded torna o gerenciamento de arquivos individuais bem complicado. Você precisa criar dois Componentes personalizados do Windows XP Embedded £ um que encapsula o nosso aplicativo baseado em formulário e sua automação de teste baseada em console e outro que encapsula os drivers de dispositivos de hardware apropriados.

Prepare-se criando um diretório C:\Automation na máquina de desenvolvimento e coloque ali TheAppToTest.exe (o aplicativo) e Demo.exe (a automação). Essa abordagem não é estritamente necessária, mas como mostraremos a você em breve, é conveniente na criação de um Componente de automação de teste.

Primeiro, orientaremos você na criação do componente de automação de teste e, em seguida, examinaremos a criação do componente de driver de dispositivo. Comece pela execução da ferramenta Component Designer a partir do menu Iniciar do desktop e selecione File | New (Arquivo | Novo) no menu principal. Você pode salvar a sua configuração (usando File | Save) como AutoTestAutomationComponent.sld (normalmente esse arquivo é chamado de arquivo slide) em qualquer diretório conveniente. No entanto, se você salvar os seus arquivos de configuração .sld no mesmo diretório dos arquivos de recurso do Componente, todo ele será portável, já que todos os arquivos estarão no mesmo diretório.

No painel de navegação esquerdo do Component Designer, expanda o item Windows XP Embedded Client (x86). Agora crie o objeto Repository para o Component da sua automação de teste. Você pode considerar um Repository como a representação de um armazenamento físico para os arquivos que compõem um Componente. Basta clicar com o botão direito do mouse no item Repositories e selecionar Add Repository. No painel de dados do lado direito, nomeie o seu Repository como AutoTestAutomationRepository e mude a propriedade Source Path para C:\Automation para indicar o local dos arquivos de automação em sua máquina de desenvolvimento (em oposição à localização final no dispositivo de destino). Existem alguns outros atributos de Repository que você pode definir nesse ponto, mas para simplificar não falaremos sobre eles no momento.

Agora volte ao painel de navegação do lado esquerdo do Component Designer, clique com o botão direito em Componentes e selecione Add Component. Nesse ponto, o painel esquerdo do Component Designer deve ser parecido com o da tela mostrada na Figura 3 e o painel direito deve ser igual à tela mostrada na Figura 4. Na área Component Properties do painel direito, você verá que o atributo Name do componente foi definido como AutoTestAutomationComponent.

Figura 3 O painel esquerdo do Component Designer
Figura 3 O painel esquerdo do Component Designer
Figura 4 O painel direito do Component Designer
Figura 4 O painel direito do Component Designer(Clique na imagem para diminuir a exibição)
Figura 4 O painel direito do Component Designer
Figura 4 O painel direito do Component Designer (Clique na imagem para aumentar a exibição)

Em seguida, para associar o Repository recém-criado ao Component, clique no botão "Repositories ..." e selecione a entrada AutoTestAutomationRepository na caixa de diálogo Select Repository e clique em OK. Você verá que a maioria dos objetos do Windows XP Embedded, incluindo Repositories, possui um número de revisão automaticamente associado ao nome do objeto £ isso ajuda muito na organização dos objetos. Agora, no painel esquerdo, adicione referências aos arquivos físicos ao clicar com o botão direito no item Files e selecionar Add | Multiple Files (Adicionar | Vários Arquivos). Na caixa de diálogo Add Component File Resources, navegue até os arquivos do seu aplicativo e de automação que estão na máquina de desenvolvimento (nesse caso, C:\Automation), selecione TheAppToTest.exe e Demo.exe files usando a tecla <Ctrl> e clique em Open.

Agora você adicionará uma Component Dependency (Dependência de Componente). Como o aplicativo baseado em formulário e a automação de teste baseada em console deste exemplo foram criadas sobre o .NET Framework 2.0, a imagem final do Windows XP Embedded deverá incluir um Componente do .NET Framework 2.0. Ao especificar explicitamente essa dependência, você pode instruir, de forma implícita, a ferramenta Target Designer (que, como você se lembra, combina todos os Componentes em uma imagem final e de produção do Windows XP Embedded) para incluir o .NET Framework 2.0 Component. Clique com o botão direito no item Component ou Group Dependency (Dependência de Grupo) e selecione Add | Component Dependency (explicaremos o que é uma Group Dependency na próxima seção desta coluna). Nesse ponto, o Component Designer se conectará a um banco de dados back-end de Componentes, instalado como parte das ferramentas do Windows XP Embedded Studio.

Na caixa de diálogo Add Component Dependency, navegue até Software | System | System Services | Other (Software | Sistema | Serviços do Sistema | Outros), selecione o .NET Framework 2.0 Component (como mostrado na Figura 5) e clique em OK. Dependendo da versão do conjunto de ferramentas do Windows XP Embedded utilizado, talvez seja preciso instalar o Componente do .NET Framework 2.0 separadamente. A lista de Componentes é dinâmica e está crescendo £ o Windows XP Embedded possui uma grande comunidade de suporte que inclui engenheiros de terceiros e outras empresas, além da própria Microsoft.

Figura 5 Adicione a Component Dependency
Figura 5 Adicione a Component Dependency(Clique na imagem para diminuir a exibição)
Figura 5 Adicione a Component Dependency
Figura 5 Adicione a Component Dependency (Clique na imagem para aumentar a exibição)

Nesse ponto, o Componente de automação de teste está completo e, portanto, você deve salvar a sua configuração .sld e fechar a ferramenta Component Designer. Agora você precisa transferir o Component personalizado para o banco de dados back-end por meio da ferramenta Component Database Manager, que pode ser executada do menu Iniciar do seu desktop. Depois que a ferramenta se conectar ao banco de dados back-end, clique no botão Import (Importar) da guia Database (Banco de dados) principal. Na caixa de diálogo Import SLD (Importar SLD), navegue até o arquivo .sld (nesse caso, AutoTestAutomationComponent.sld) e clique em Import. O processo é muito rápido e, após a revisão das mensagens de log, clique em Close (Fechar) e em Close novamente. O Component de automação de teste estará disponível para a ferramenta Target Designer.

Agora você precisa criar o segundo Componente, que encapsulará todos os seus drivers de dispositivos e outro software necessários ao suporte a itens individuais de hardware do dispositivo de destino. Esse processo é muito fácil graças ao arquivo devices.pmq, que lista todos os itens de hardware do dispositivo de destino. Execute a ferramenta Component Designer novamente, mas desta vez clique em File | Import (Arquivo | Importar). Na caixa de diálogo Choose File for Import (Escolher Arquivo a Ser Importado), navegue até o arquivo devices.pmq e clique em Open (Abrir). Isso faz com que uma caixa de diálogo Import File (Importar Arquivo) seja exibida £ basta clicar no botão Start (Iniciar). O processo de importação leva vários minutos porque o Component Designer não está só importando os dados XML de devices.pmq; na verdade, ele está analisando o arquivo, determinando que arquivos de software serão necessários ao suporte dos dispositivos de hardware e criando um Componente a partir dessas informações. Em outras palavras, a maior parte do trabalho que você teria de fazer manualmente na criação de um Componente único de automação de teste é feita automaticamente para o componente de hardware. Você deve obter uma mensagem "Import session completed successfully" (Sessão de importação concluída com êxito) para poder fechar a caixa de diálogo Import File.

Se você expandir o item Components e selecionar o item devices, poderá renomear o Componente para AutoHardwareComponent. Agora salve a sua configuração como AutoHardwareComponent.sld e feche a ferramenta Component Designer. Depois disso, é possível usar a ferramenta Component Database Manager para importar o Componente do hardware para o banco de dados back-end, da mesma forma como foi feito para o Componente da automação de teste.

Componentes de sistema operacional

Agora que a automação de teste e os Componentes de hardware foram criados, você está pronto para combiná-los com os Componentes do sistema operacional e criar uma imagem do Windows XP Embedded. É aqui que você utiliza a ferramenta Target Designer.

Execute o Target Designer do menu Iniciar e clique em File | New. Na caixa de diálogo New Configuration (Nova Configuração) resultante, crie um nome para a configuração, AutoTestAutomation neste exemplo, e clique em OK. A interface do usuário do Target Designer possui três painéis principais. O painel esquerdo lista os Componentes que podem ser adicionados à sua imagem do Windows XP Embedded. O painel central lista os Componentes que estão em sua imagem no momento. O painel direito exibe informações sobre qualquer item selecionado no painel esquerdo ou no central.

O painel esquerdo lerá automaticamente o banco de dados back-end e listará os Componentes criados £ nesse caso, AutoHardwareComponent e AutoTestAutomationComponent. Adicione o Component de hardware ao clicar com o botão direito do mouse em AutoHardwareComponent e selecionar Add; em seguida, adicione AutoTestAutomationComponent da mesma forma (a ordem em que isso será feito não faz diferença).

Depois de adicionar dois Componentes personalizados, será preciso adicionar dois Componentes de sistema operacional centrais. Embora você possa examinar os milhares de Componentes disponíveis (normalmente há mais de 10.000), uma abordagem muito mais fácil seria usar um meta-Component pré-fabricado. No painel esquerdo do Target Designer, expanda o item Design Templates (Modelos de Design) para revelar várias opções. Neste exemplo, selecione o modelo Terminal Professional baseado em Windows, clique com o botão direito do mouse e selecione Add. Isso adiciona um grande número de Componentes individuais de software comumente usados em dispositivos semelhantes a PCs. Existem diversos modelos disponíveis, incluindo Retail Point of Sale Terminal (Ponto de Revenda do Terminal de Vendas), Home Gateway (Gateway Doméstico), Network Attached Storage (Armazenamento Conectado à Rede) e assim por diante. A desvantagem de usar um modelo de Componente é que é muito provável que você obtenha Componentes de que não precisa. Uma boa abordagem é usar um modelo simplesmente como um ponto de partida e então remover os Componentes desnecessários.

No painel central, selecione Settings (Configurações) e no painel direito clique em Target Device Settings | Show (Configurações do Dispositivo de Destino | Mostrar). Essas entradas se referem ao dispositivo de destino, e não à máquina de desenvolvimento. Assim, nesse caso, você precisa deixar a unidade de inicialização C: mas mudar de C: para D: na pasta Windows, na pasta Arquivos de Programas e nas entradas da pasta Documents and Settings. Isso acontece porque, como mencionamos anteriormente, o plano é copiar a imagem do Windows XP Embedded para a partição D: do único disco rígido do dispositivo.

Edite a entrada Boot ARC de multi(0)disk(0)rdisk(0)partition(1) para multi(0)disk(0)rdisk(0)partition(2). Os itens rdisk (unidades físicas) são baseados em 0 mas os itens de partição são baseados em 1. Altere também o tamanho da partição de inicialização para 5000 (MB). Esse é um valor mágico para qualquer partição de inicialização que tenha 5 GB ou mais. Se você especificar um valor de, digamos, 700 MB, potencialmente poderá criar um CD inicializável. Neste exemplo, basta deixar o tamanho de cluster da partição em 4096 bytes. Este é um bom momento para salvar a configuração como AutoTestAutomation.slx.

Crie a sua imagem
Automação de teste como um shell personalizado

Em alguns casos, os desenvolvedores que utilizam o Windows XP Embedded vão querer implementar um shell primário personalizado. Por exemplo, imagine o aplicativo baseado em formulário mostrado na Figura 1 agindo como uma interface do usuário completa. Essa abordagem força que a atividade do usuário seja feita estritamente no aplicativo e geralmente impede a interação com programas como o Windows Explorer e o Internet Explorer®. Em um cenário de shell personalizado, as técnicas normais de automação de teste geralmente não funcionam porque você não executa um shell de sistema operacional como o faria com um aplicativo.

Uma solução de automação de teste em um cenário de shell personalizado do Windows XP Embedded é criar um Componente de automação de teste usando as ferramentas do Windows Embedded Studio e configurando esse Componente como o shell primário. A seguir, uma rápida visão geral do processo (para obter uma descrição detalhada da criação de shells personalizados do Windows XP Embedded, dê uma olhada no artigo de Mike Hall, "Criando um shell do Windows XP Embedded", em msdn2.microsoft.com/ms838335).

Para criar um shell primário de automação de teste personalizado, primeiro você cria um Componente usando a ferramenta Component Designer exatamente como foi descrito nesta coluna. Depois disso, quando estiver no Component Designer, será preciso fazer quatro configurações para transformar o Componente em um Componente do shell primário especial.

  1. Defina a propriedade Component Prototype como Software | System | User Interface | Shells | Windows Shells | Shell Prototype Component (Software | System | Interface do Usuário | Shells | Shells do Windows | Componente de Protótipo de Shell).
  2. Adicione uma Advanced Property (Propriedade Avançada) com estas configurações: Name = cmiShellPath (diferencia maiúsculas de minúsculas), Format = String e Value = \Automation\Demo.exe (por exemplo, o caminho para a automação de teste).
  3. Defina uma associação de grupo para Group Memberships | Add Group Membership | Dependencies | Shell (Associações de Grupo | Adicionar Associação de Grupo | Dependências | Shell.
  4. Defina uma segunda associação de grupo para Group Memberships | Add Group Membership | Software | System | User Interface | Shells.

Depois de definir essas quatro configurações para criar o seu Componente de shell primário especial, ele será listado na ferramenta Target Designer como um shell de interface do usuário junto com o Navegador do Shell, o shell de comandos e assim por diante. Em seguida, se você adicionar o Componente do shell de automação personalizado à imagem do Windows XP Embedded, verá que o resultado será um sistema operacional inicializado imediatamente na automação de teste, executando o aplicativo baseado em formulário associado.

Você já está quase pronto para combinar seus Componentes em uma imagem. Mas antes de experimentar uma compilação, você deve verificar as dependências do Componente para determinar se acidentalmente deixou de fora da configuração qualquer Componente necessário. Essa é uma etapa importante. Um sistema operacional é um software complexo e, sem uma ferramenta como o Target Designer, a determinação manual de todas as dependências de componentes seria muito difícil.

Na barra de menu principal, clique em Configuration | Check Dependencies (Configuração | Verificar Dependências). Uma caixa de diálogo será aberta e exibirá mensagens de andamento. A verificação de dependência importará automaticamente qualquer Componente necessário para a configuração. Você poderá fechar a caixa de diálogo após a conclusão da verificação de dependência. Na maioria dos casos, haverá alguns erros exibidos em uma lista de tarefas no painel inferior do Target Designer. Se você clicar com o botão direito do mouse em um erro, obterá uma opção Action com recomendações de correção. Por exemplo, um dos erros deste exemplo diz "Regional and Language Options requires at least one additional enabled component not in the configuration" (As Opções regionais e de idioma exigem pelo menos um componente habilitado adicional que não esteja na configuração). Se você clicar com o botão direito do mouse e selecionar Action, obterá uma caixa de diálogo Resolve Dependency (Resolver Dependência) onde poderá selecionar o idioma e depois clicar em Add. Neste exemplo, você deve adicionar o idioma inglês, um componente do NT Loader, um componente do formato NTFS e um componente do navegador do shell para resolver os outros erros.

Agora você pode clicar em Configuration | Build Target Image (Configuração | Criar Imagem de Destino). Quando isso for feito, você obterá uma caixa de diálogo de compilação com o nome da sua configuração (AutoTestAutomation.slx). Em seguida, clique no botão Build e o Target Designer avisará que você fez alterações na configuração £ já que quatro novos Componentes foram adicionados para a resolução dos erros encontrados pela verificação de dependência £ e perguntará se deseja executar novamente a verificação de dependência. Clique em Yes (Sim) e uma segunda verificação de dependência será executada, embora de forma bem mais rápida. Após a conclusão da segunda verificação de dependência, presumivelmente sem erros, você poderá fechar a caixa de diálogo Dependency Check para iniciar o processo de compilação.

No Target Designer, selecionar Tools | Options | Build (Ferramentas | Opções | Compilar) permitirá que você configure o local padrão onde as imagens do Windows XP Embedded serão armazenadas em seu sistema. Também é possível especificar um local para uma determinada compilação na caixa de diálogo Build. É importante observar que se já houver uma imagem do Windows XP Embedded, uma mensagem de aviso alertará você que essa imagem será substituída £ normalmente isso poderá ser feito.

Após a conclusão da compilação, não se surpreenda se houver alguns avisos. Você poderá revisá-los para garantir que são somente problemas secundários (como um aviso dizendo que você está usando software de avaliação) que podem ser ignorados com segurança.

Implante a sua imagem

Agora você está pronto para implantar a sua imagem do Windows XP Embedded no dispositivo de destino. A nova imagem consiste em quatro arquivos e um diretório com quatro subdiretórios. Três dos arquivos £ boot.ini, ntdetect.com e ntldr £ são os arquivos fundamentais para a inicialização de um sistema operacional baseado em Windows XP. O quarto arquivo £ weruntime.ini £ é, essencialmente, uma chave especial do Windows XP Embedded que o habilita.

Copie esses quatro arquivos da máquina de desenvolvimento para a unidade de inicialização do dispositivo de destino. A forma exata como esses arquivos serão copiados dependerá dos tipos de dispositivos de E/S que o dispositivo de destino possui. Neste caso, como o dispositivo de destino é um laptop com uma instalação normal do Windows XP na unidade C:, você pode simplesmente inicializar o dispositivo de destino e copiar os arquivos usando a unidade de CD/DVD. A imagem principal do Windows XP Embedded está localizada em um diretório chamado DriveD (porque a unidade do dispositivo de destino é a D:). Esse diretório contém outros três diretórios: Documents and Settings, Arquivos de Programas e Windows. Além disso, também há um diretório Automation, que armazena um aplicativo baseado em formulário (TheAppToTest.exe) e automação baseada em console (Demo.exe). Copie esses quatro diretórios para a partição D: do dispositivo de destino.

Antes de tentar inicializar o dispositivo de destino, faça uma pequena edição para que o dispositivo tenha inicialização múltipla. Basta editar o arquivo boot.ini da imagem do Windows XP Embedded, adicionando a linha a seguir à seção [operating systems]:

multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="Microsoft Windows XP Professional" /fastdetect

Isso permite que você inicialize com facilidade o seu dispositivo de destino em uma sessão normal do Windows XP na unidade C: caso algo dê errado com a imagem do Windows XP Embedded na partição D:. Uma abordagem alternativa, se o seu dispositivo de destino tiver uma unidade de disquete, é criar um disquete inicializável para recuperação. Ou você pode simplesmente manter o CD da sua instalação original do Windows XP e usá-lo para inicializar em modo de recuperação, se necessário.

Agora vem o momento da verdade. Ligue o seu dispositivo de destino e, se a sua imagem do Windows XP Embedded estiver correta, o dispositivo de destino será inicializado no sistema personalizado. Um conjunto especial de programas, coletivamente conhecidos como FBA (First Boot Agent), inicializará o sistema na primeira inicialização (o FBA não será executado nas inicializações subseqüentes).

Neste ponto, você poderá executar a automação de teste. Como este exemplo utiliza um componente do Navegador do Shell, o sistema possui uma interface GUI. Você pode usar a combinação de teclado Windows-E para executar o Windows Explorer, navegar até D:\Automation e clicar duas vezes no arquivo Demo.exe para iniciar a automação de teste. O resultado será semelhante à captura de tela da Figura 1. Outra abordagem seria incluir o CMD - componente do processador de comandos do Windows em sua imagem do Windows XP Embedded. Isso lhe dará um shell de comando cmd.exe que poderá ser usado na execução da automação de teste.

Uma recapitulação rápida

Vamos recapitular o que vimos até aqui e explicar as vantagens desta técnica. Como o Windows XP Embedded é, essencialmente, uma versão do sistema operacional Windows XP, você pode criar uma automação de teste de interface do usuário poderosa para ele usando técnicas de programação padrão.

Uma abordagem para a criação da automação da interface do usuário para sistemas Windows XP Embedded é criar um Componente que inclua o aplicativo primário e sua automação de teste associada contida em uma imagem do Windows XP Embedded. Outra abordagem é criar uma imagem normal, sem o Componente de automação de teste, e portar a automação para o dispositivo de destino de forma separada.

A abordagem do Componente de automação de teste possui três vantagens significativas. Primeiro, a criação de um Componente de automação de teste modulariza o esforço do seu teste, tornando-o mais fácil de modificar e de gerenciar. Segundo como o banco de dados back-end do Componente do Windows XP Embedded pode estar localizado em um servidor remoto, um Componente de automação de teste pode ser facilmente compartilhado por uma equipe de desenvolvedores e de testadores. Terceiro, a criação de um Componente de automação de teste permite que as possíveis dependências do software complexo do Componente sejam automaticamente verificadas pela ferramenta Target Designer.

Envie suas perguntas e comentários para testrun@microsoft.com.

Dr. James McCaffrey trabalha para a Volt Information Sciences Inc., onde gerencia o treinamento técnico para engenheiros de software que trabalham no campus da Microsoft em Redmond, Washington. Ele trabalhou em vários produtos da Microsoft, incluindo o Internet Explorer e o MSN Busca e é o autor de .NET Test Automation Recipes (Receitas de automação de teste de .NET, Apress, 2006). O endereço de James é jmccaffrey@volt.com or v-jammc@microsoft.com.

Mike Hall é gerente de produto sênior do grupo Mobile and Embedded Devices (Dispositivos móveis e incorporados) da Microsoft. Mike está na Microsoft há mais de 10 anos e atualmente trabalha com o Windows CE, Windows XP Embedded e o Windows Embedded for Point of Service. Ele se apresenta em diversos eventos da Microsoft e de terceiros e escreve um blog que discute as tecnologias incorporadas.


Fundamentos: O texto 3D no WPF

Fundamentos
O texto 3D no WPF
Charles Petzold

Download do código disponível em: Foundations2007_10.exe (171KB)
Conteúdo
FormattedText e BuildGeometry
Contornos e malhas
A hierarquia Text3D
RibbonText e SliverText
A solução SolidText
Preenchimento

A
s tecnologias de fonte geométrica, como TrueType, servem principalmente, para fornecer flexibilidade e precisão tipográfica; no entanto, são também elementos gráficos que permitem o uso da criatividade. Os programadores podem ter acesso aos contornos que definem cada caractere de texto e tratá-los como objetos gráficos vetoriais. Esses contornos podem ser tracejados, preenchidos, usados em recortes ou submetidos a transformações. Um recurso popular do Microsoft® Word, conhecido como WordArt, se baseia nesse conceito.

É importante reconhecer a natureza e as limitações desses contornos de caracteres: eles são rigorosamente geométricos e não contam com as "indicações" que o sistema operacional geralmente usa para processar fontes na tela. Essas indicações permitem que os caracteres sejam rasterizados de forma inteligente, com base na grade de pixels disponível. Como conseqüência, os contornos de caracteres sem indicadores apresentam melhor aparência em fontes de tamanho grande ou em dispositivos de alta resolução. Em geral, não são adequados para processar texto na tela com fontes de tamanho normal (no entanto, conforme a resolução das impressoras aumenta e a suavização passa a ser mais usada em gráficos na tela, o valor das indicações diminui).

Sempre que me deparo com uma nova API do Windows®, empenho-me em localizar a configuração para obter acesso a esses contornos de caracteres. No Windows Forms, faz parte da classe GraphicsPath. Quatro sobrecargas de um método AddString permitem adicionar contornos de caracteres a um caminho. O capítulo 19 dos meus livros "Programming Microsoft Windows with C#" (Programando o Microsoft Windows com C#; Microsoft Press, 2002) e "Programming Microsoft Windows with Microsoft Visual Basic® .NET" (Programando o Microsoft Windows com Microsoft Visual Basic® .NET; Microsoft Press, 2003) mostram como isso é feito.

No WPF (Windows Presentation Foundation), as classes e os métodos que fornecem acesso aos contornos de caracteres estão mais ocultos, mas existem. As classes FormattedText e GlyphRun do namespace System.Windows.Media têm métodos denominados BuildGeometry, que retornam um objeto Geometry para uma fonte e uma cadeia de texto específicas. Neste artigo, utilizarei exclusivamente FormattedText, pois é a mais simples das duas classes. Os capítulos 28 e 30 do meu livro "Applications = Code + Markup" (Aplicativos = código + marcação; Microsoft Press, 2006) trazem alguns exemplos do uso de FormattedText e BuildGeometry com gráficos bidimensionais.

Quando comecei a trabalhar com gráficos 3D no WPF, naturalmente avaliei a possibilidade de transformar os contornos de caracteres em blocos de texto tridimensional, como se pode ver em mídia impressa e em efeitos de logotipos flutuantes na televisão (veja o exemplo na Figura 1). Eu sabia que a tarefa envolveria a conversão de um contorno bidimensional em uma malha de triângulos tridimensionais; mas, além disso, tinha certeza de uma coisa: isso envolveria algum tipo de programação fora dos padrões convencionais.

Figura 1 Texto 3D sólido
Figura 1 Texto 3D sólido

FormattedText e BuildGeometry

Suspeito de que a maioria dos programadores de WPF não teve muito contato com a classe FormattedText. Conforme indicado na documentação, essa classe é usada em "controle de baixo nível para desenho de texto".

Os construtores FormattedText exigem um objeto TypeFace, que define a família de fontes, um estilo (por exemplo, itálico), o possível uso de negrito e qualquer alongamento ou compactação associada à fonte. Além disso, esses construtores exigem um tamanho de EM (a altura da fonte), um pincel para colorir os caracteres da fonte e a cadeia de texto propriamente dita.

Depois que um objeto FormattedText é criado, você pode chamar vários métodos para definir diferentes fontes, estilos ou formatações para subconjuntos da cadeia de texto. As propriedades permitem definir o espaçamento entre as linhas e outras características do texto.

Os objetos FormattedText freqüentemente são usados com o método DrawText da classe DrawingContext. Em geral, você se depara com a classe DrawingContext quando substitui o método OnRender definido pelo UIElement. Esse é o nível mais baixo de saída de gráficos que o seu aplicativo pode realizar, sendo ainda considerado um aplicativo WPF puro. O método DrawText exige somente um objeto FormattedText e um ponto de coordenadas onde o texto deve começar.

Em todos os demais contextos de uso de FormattedText, o método BuildGeometry parece estranho. O método tem um argumento £ uma origem do tipo Point £ e retorna um objeto Geometry.

Geometry é uma classe importante no WPF, claramente relacionada ao caminho tradicional de gráficos. Um objeto Geometry é uma coleção de linhas retas e curvas especificadas como pontos de coordenadas. Algumas dessas linhas e curvas podem estar conectadas; algumas linhas e curvas conectadas podem estar fechadas para descrever áreas circunscritas. Não há nenhum conceito de processamento incluído em um objeto Geometry. Na programação de gráficos bidimensionais, é possível processar um objeto Geometry passando-o para a classe Path, que é muito útil e faz parte da biblioteca Shapes de alto nível. Uma alternativa é GeometryDrawing, que se baseia em um objeto Geometry, um pincel e uma caneta.

Em geral, ao programar gráficos bidimensionais, há poucos motivos para aprofundar-se em um objeto Geometry específico. Mas converter um objeto Geometry em texto tridimensional é uma tarefa bastante diferente. Então, o que é exatamente o objeto Geometry retornado por FormattedText.BuildGeometry?

Geometry é uma classe abstrata da qual derivam sete outras classes. Minha experiência mostra o seguinte: o objeto Geometry retornado por FormattedText.BuildGeometry é constituído, na verdade, por um ou mais objetos GeometryGroup aninhados contendo vários objetos PathGeometry, um para cada caractere na cadeia de texto. Cada objeto PathGeometry consiste em um ou mais objetos PathFigure definindo um caminho fechado. Alguns caracteres (como l, t ou x) exigem somente um PathFigure. Outros caracteres exigem dois. Um i minúsculo, por exemplo, requer um segundo PathFigure para o ponto. Um O requer um PathFigure para o contorno do círculo externo e outro para o círculo interno. Um B maiúsculo requer três objetos PathFigure. Um sinal de porcentagem requer cinco.

Cada PathFigure é uma coleção de objetos do tipo PathSegment, que é outra classe abstrata. Minha experiência mostra que os objetos PathFigure associados a contornos de texto contêm vários objetos dos tipos LineSegment, PolyLineSegment, BezierSegment e PolyBezierSegment, que contribuem para a definição de um único caminho fechado.

O WPF 3D não tem os conceitos de polilinhas ou curvas de Bézier, mas as malhas de triângulos se baseiam em coleções de pontos. Assim, a primeira etapa exige a conversão de um objeto Geometry de FormattedText.BuildGeometry em uma série de polilinhas circunscritas. Essa é a parte fácil. Mas logo se tornou claro para mim que criar um algoritmo para converter essas polilinhas em uma malha de triângulos seria um exercício matemático complexo e, provavelmente, além das minhas habilidades.

Contornos e malhas

Na edição de abril de 2007 desta revista, abordei o mecanismo de geração dos objetos MeshGeometry3D para uso com gráficos do WPF 3D (consulte msdn.microsoft.com/msdnmag/issues/07/04/Foundations). É preciso definir, pelo menos, as propriedades Positions e TriangleIndices de um objeto MeshGeometry3D. A propriedade Positions é uma coleção de pontos no espaço 3D. A coleção TriangleIndices descreve como construir triângulos a partir desses pontos. Cada três números inteiros em TriangleIndices fazem referência a três pontos na coleção Positions para formar um triângulo.

Descobri que as polilinhas que gerei a partir dos contornos de texto formariam membros da coleção Positions de MeshGeometry3D. A Figura 2 mostra um A maiúsculo em uma fonte sem serifa, que consiste em duas polilinhas circunscritas com um total de onze pontos. A segunda imagem mostra como a letra pode ser dividida em sete triângulos. Seria trivial fazer à mão um caractere simples como esse, mas a forma de descrever o processo em código não me parecia tão clara.

Figura 2 Letra definida por pontos e triângulos
Figura 2 Letra definida por pontos e triângulos

Logicamente, você quer ter certeza de que os lados de cada triângulo não ultrapassam os limites do caractere e, para isso, precisa garantir que toda a face do caractere seja representada pela coleção de triângulos. Lembre-se também de que o exemplo usado aqui é um caractere simples de uma fonte sem serifa. Imagine um S maiúsculo em uma fonte com serifa. Isso tornaria a tarefa bastante desafiadora. Existe uma técnica conhecida como Triangulação de Delaunay que pode ajudar, mas os cálculos matemáticos são bastante complexos.

Felizmente, nem tudo estava perdido. Enquanto analisava esse problema, vi em um anúncio um texto 3D com uma abordagem diferente, envolvendo somente os contornos e deixando vazio o corpo dos caracteres. Percebi que isso eu saberia fazer. Primeiro, eu precisava de um nome de classe, e criei RibbonText.

A hierarquia Text3D

O código que pode ser baixado para este artigo contém uma única solução do Visual Studio® denominada Text3D. Ela consiste em um projeto de DLL denominado Petzold.Text3D e em várias pequenas demonstrações de programas em XAML que usam a DLL. Observe que os arquivos na DLL têm o namespace Petzold.Text3D.

A hierarquia de classes na biblioteca Petzold.Text3D.dll começa com ModelVisualBase, uma classe abstrata que deriva da classe ModelVisual3D do WPF 3D, uma técnica que abordei na edição de abril de 2007 desta revista. No entanto, essa classe ModelVisualBase não é exatamente a mesma ModelVisualBase da coluna anterior, porque ela precisava de um pouco mais de flexibilidade. Mas é importante perceber que o conceito é o mesmo. ModelVisualBase armazena internamente um objeto GeometryModel3D e um objeto MeshGeometry3D e define as propriedades públicas Material e BackMaterial que transfere para o objeto GeometryModel3D.

A maioria das propriedades nas classes Petzold.Text3D tem o apoio de propriedades de dependência. A classe ModelVisualBase define dois manipuladores de eventos PropertyChanged (um estático e uma instância) que as classes descendentes podem usar ao definir propriedades de dependência. A versão de instância prepara adequadamente para alterações as diversas propriedades do objeto MeshGeometry3D interno (um processo que abordei na minha coluna de abril) e, então, chama um método abstrato denominado Triangulate. As classes descendentes podem substituir Triangulate para definir as diversas coleções de MeshGeometry3D.

Na biblioteca Petzold.Text3D, a classe abstrata Text3D deriva de ModelVisualBase. Essa classe define várias propriedades relacionadas a texto denominadas Text, FontFamily, FontStyle, FontWeight, FontStretch e FontSize, todas exigidas pelo construtor FormattedText. A classe define também uma propriedade Origin exigida pelo método BuildGeometry. Sempre que qualquer uma dessas propriedades é alterada, a classe cria um novo objeto FormattedText e define uma oitava propriedade de Text3DBase, denominada TextGeometry, a partir do objeto Geometry retornado por BuildGeometry:

FormattedText formtxt =
    new FormattedText(Text, CultureInfo.CurrentCulture,
        FlowDirection.LeftToRight,
        new Typeface(FontFamily, FontStyle,
            FontWeight, FontStretch),
            FontSize, Brushes.Transparent);

TextGeometry = formtxt.BuildGeometry(Origin);

Esse objeto TextGeometry está disponível para todas as classes descendentes.

A classe Text3DBase não pode criar um novo objeto TextGeometry sem que ocorram alocações de heap. A classe precisa alocar um novo objeto FormattedText e, sem dúvida, BuildGeometry faz várias alocações de memória. Essas alocações de heap significam que, provavelmente, as propriedades definidas pela classe Text3DBase não deveriam ser animadas. Usei a propriedade IsAnimationProhibited definida por UIPropertyMetadata para sinalizar essas propriedades que, do contrário, seriam fortes candidatas à animação.

Nas classes que escrevi para criar texto 3D, a propriedade Origin definida por Text3DBase é a única que indica onde o texto é posicionado no espaço 3D. A propriedade é do tipo Point, indicando uma localização em duas dimensões. Avaliei a possibilidade de definir um conjunto abrangente de propriedades para posicionar o texto com precisão no espaço 3D. Essas propriedades precisariam incluir não apenas uma origem de texto 3D, mas também um vetor 3D indicando a direção da linha de base e outro vetor 3D indicando a direção vertical. Em vez disso, decidi usar a propriedade Origin para posicionar o texto no plano XY do espaço 3D e, depois, realizar o restante do posicionamento com transformações. A grande vantagem dessa abordagem é simplificar a parte matemática.

A propriedade FontSize definida por Text3DBase indica o tamanho EM da fonte, que se relaciona de forma aproximada com a altura total dos caracteres da fonte. Em três dimensões, as fontes têm também profundidade. A classe DeepTextBase deriva de Text3DBase e existe somente para definir uma propriedade Depth (futuramente, você entenderá o motivo dessa extensa hierarquia de classes; durante a programação dessas classes, ocorreu uma quantidade considerável de refatorações que geraram a estrutura atual). Como o texto se posiciona no plano XY, criei a propriedade Depth descrevendo a profundidade do texto ao longo do eixo Z negativo.

Até o momento, nenhuma dessas classes realizou efetivamente qualquer função. A classe abstrata GeometryTextBase mostrada na Figura 3 deriva de DeepTextBase e substitui o método Triangulate definido por ModelVisualBase. Com o auxílio do método GetFlattenedPathGeometry, da classe Geometry, a classe GeometryTextBase converte o objeto Geometry, disponível como a propriedade TextGeometry, em várias polilinhas conectadas correspondentes às figuras fechadas que constituem os contornos de texto. Para cada figura fechada, a classe GeometryTextBase faz uma chamada para um método abstrato, da seguinte forma:

Figure 3 Classe GeometryTExtBase (Close Figure 3)
public abstract class GeometryTextBase : DeepTextBase
{
    // Field prevent re-allocations during mesh generation.
    CircularList<Point> list = new CircularList<Point>();

    protected override void Triangulate(
        DependencyPropertyChangedEventArgs args,
        Point3DCollection vertices, Vector3DCollection normals,
        Int32Collection indices, PointCollection textures)
    {
        // Clear all four collections.
        vertices.Clear();
        normals.Clear();
        indices.Clear();
        textures.Clear();

        // Convert TextGeometry to series of closed polylines.
        PathGeometry path =
            TextGeometry.GetFlattenedPathGeometry(0.001,
            ToleranceType.Relative);

        foreach (PathFigure fig in path.Figures)
        {
            list.Clear();
            list.Add(fig.StartPoint);

            foreach (PathSegment seg in fig.Segments)
            {
                if (seg is LineSegment)
                {
                    LineSegment lineseg = seg as LineSegment;
                    list.Add(lineseg.Point);
                }
                else if (seg is PolyLineSegment)
                {
                    PolyLineSegment polyline = seg as PolyLineSegment;

                    for (int i = 0; i < polyline.Points.Count; i++)
                        list.Add(polyline.Points[i]);
                }
            }

            // Figure is complete. Post-processing follows.
            if (list.Count > 0)
            {
                // Remove last point if it's the same as the first.
                if (list[0] == list[list.Count - 1])
                    list.RemoveAt(list.Count - 1);

                // Convert points to Y increasing up.
                for (int i = 0; i < list.Count; i++)
                {
                    Point pt = list[i];
                    pt.Y = 2 * Origin.Y - pt.Y;
                    list[i] = pt;
                }

                // For each figure, process the points.
                ProcessFigure(list, vertices, normals,
                    indices, textures);
            }
        }
    }

    // Abstract method to convert figure to mesh geometry.
    protected abstract void ProcessFigure(CircularList<Point> list,
        Point3DCollection vertices, Vector3DCollection normals,
        Int32Collection indices, PointCollection textures);
}

protected abstract void ProcessFigure(
    CircularList<Point> list, Point3DCollection vertices,
    Vector3DCollection normals, Int32Collection indices,
    PointCollection textures);

O primeiro argumento é uma coleção dos pontos bidimensionais na figura. CircularList é uma classe de coleção que defini para funcionar como um buffer circular. Sempre que um membro é acessado por um índice, o objeto normaliza o índice para o intervalo adequado. Em outras palavras, um índice de -1 acessa o último membro da coleção e um índice de list.Count acessa o primeiro membro da coleção. A coleção contém todos os pontos da polilinha fechada.

Os outros argumentos para ProcessFigure correspondem às coleções Positions, Normals, TriangleIndices e TextureCoordinates do objeto MeshGeometry3D. É necessária uma classe que implemente o método ProcessFigure para preencher pelo menos as coleções de vértices e índices, com base nos pontos da coleção CircularList.

RibbonText e SliverText

A primeira classe que escrevi para gerar efetivamente texto tridimensional chama-se RibbonText e é mostrada na Figura 4. Como você pode ver, a classe deriva de GeometryTextBase e consiste somente em uma implementação do método ProcessFigure. Os objetos Point3D gerados por ela se baseiam simplesmente nos pontos 2D da coleção CircularList e na propriedade Depth. Os pontos se alternam entre aqueles no plano XY (onde Z é igual a zero) e as unidades de Depth por trás do plano XY. A coleção de índices conecta esses dois conjuntos de pontos, definindo uma série de triângulos.

Figure 4 Classe RibbonText (Close Figure 4)
public class RibbonText : GeometryTextBase
{
    protected override void ProcessFigure(CircularList<Point> list,
        Point3DCollection vertices, Vector3DCollection normals,
        Int32Collection indices, PointCollection textures)
    {
        int offset = vertices.Count;

        for (int i = 0; i <= list.Count; i++)
        {
            Point pt = list[i];

            // Set vertices.
            vertices.Add(new Point3D(pt.X, pt.Y, 0));
            vertices.Add(new Point3D(pt.X, pt.Y, -Depth));

            // Set texture coordinates.
            textures.Add(new Point((double)i / list.Count, 0));
            textures.Add(new Point((double)i / list.Count, 1));

            // Set triangle indices.
            if (i < list.Count)
            {
                indices.Add(offset + i * 2 + 0);
                indices.Add(offset + i * 2 + 2);
                indices.Add(offset + i * 2 + 1);

                indices.Add(offset + i * 2 + 1);
                indices.Add(offset + i * 2 + 2);
                indices.Add(offset + i * 2 + 3);
            }
        }
    }
}

Lembre-se de que o método ProcessFigure é chamado várias vezes para uma cadeia de texto específica. O método Triangulate em GeometryTextBase é responsável pela limpeza inicial das coleções MeshGeometry3D; a classe ProcessFigure usa um número inteiro denominado deslocamento para determinar os índices dos novos pontos que adiciona à coleção de vértices.

A Figura 5 mostra um pequeno arquivo XAML que utiliza a classe RibbonText e a Figura 6 mostra sua aparência. De certa forma, parece mais sofisticado do que um bloco de texto 3D normal, talvez por ser um tanto fora do comum. Mas, sob o ponto de vista dos algoritmos, é provavelmente a forma mais simples de texto 3D já concebida.

Figure 5 RibbonTextDemo.xaml (Close Figure 5)
<!-- RibbonTextDemo.xaml by Charles Petzold, June 2007 -->
<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:src="clr-namespace:Petzold.Text3D;assembly=Petzold.Text3D"
      WindowTitle="RibbonText Demo"
      Title="RibbonText Demo">

    <Viewport3D>
        <src:RibbonText Text="Ribbon"
                FontFamily="Times New Roman" Depth="2">
            <src:RibbonText.Material>
                <DiffuseMaterial Brush="Cyan" />
            </src:RibbonText.Material>

            <src:RibbonText.BackMaterial>
                <DiffuseMaterial Brush="Pink" />
            </src:RibbonText.BackMaterial>
        </src:RibbonText>

        <!-- Lights. -->
        <ModelVisual3D>
            <ModelVisual3D.Content>
                <Model3DGroup>
                    <AmbientLight Color="#404040" />
                    <DirectionalLight Color="#C0C0C0"
                        Direction="2 -3 -1" />
                </Model3DGroup>
            </ModelVisual3D.Content>
        </ModelVisual3D>

        <!-- Camera. -->
        <Viewport3D.Camera>
            <PerspectiveCamera Position="-3 0 8" UpDirection="0 1 0"
                               LookDirection="1 0 -2" FieldOfView="45" />
        </Viewport3D.Camera>
    </Viewport3D>
</Page>
Figura 6 Exibição de RibbonTextDemo
Figura 6 Exibição de RibbonTextDemo

Na verdade, os caracteres são vazios. Se você visse o texto de frente, veria através dele. Você só pode ver que o exterior e o interior têm cores diferentes por causa do ângulo. O pincel Material (definido como ciano em RibbonTextDemo.xaml) colore o exterior dos caracteres e o pincel BackMaterial (definido como rosa) colore o interior. Contudo, em algumas fontes, essas cores poderiam ser invertidas. Isso depende da direção dos pontos que definem os contornos dos caracteres. Essa direção poderia até mesmo ser diferente para caracteres distintos da mesma fonte.

A classe RibbonText define pontos para a coleção TextureCoordinates, de forma que você não fique restrito apenas a pincéis de cores sólidas. Os pontos da coleção TextureCoordinates têm coordenadas Y de 0 para a parte da faixa em primeiro plano e de 1 para a parte em segundo plano, e coordenadas X baseadas no índice do ponto na coleção CircularList. Qualquer pincel não-sólido usado com esse texto será aplicado a cada faixa separadamente. Os resultados mais previsíveis ocorrem quando é usado um LinearGradientBrush com coordenadas iniciais e finais de (0, 0) e (0, 1). Outros pincéis podem tornar visível uma costura em lugares imprevisíveis nos caracteres de texto.

Bem mais difícil do que RibbonText foi uma classe que chamei de SliverText. Essa classe também se concentra somente nos contornos dos caracteres de texto, mas confere a eles uma largura diferente de zero, que defini como a propriedade SliverWidth. A Figura 7 mostra o programa SliverTextDemo executado com FontSize padrão de 1, Depth padrão de 1 e SliverWidth padrão de 0,05. Defini as TextureCoordinates de SliverText com base nas coordenadas X e Y dos pontos, permitindo a variação de um pincel na parte frontal do texto. O exemplo mostra um pincel em dégradé. A parte superior dos caracteres parece um pouco arredondada porque as coordenadas das extremidades são compartilhadas pelos triângulos superiores e laterais. O WPF 3D calcula normais (que controlam o reflexo da luz) com base na média.

Figura 7 Exibição de SliverTextDemo
Figura 7 Exibição de SliverTextDemo

Se você definir SliverWidth com um valor muito alto em relação a FontSize, os caracteres começarão a mesclar-se uns com os outros. Se, depois disso, o texto for animado por uma transformação, poderá surgir uma aparência indesejável, pois as superfícies competem pelo domínio do primeiro plano (o termo técnico para esse efeito é "Z fighting").

A classe SliverText envolveu alguma aplicação da geometria analítica bidimensional. Cada contorno de caractere é uma polilinha bidimensional, mas SliverText requer dois conjuntos de polilinhas paralelas. Pensei em usar o método GetWidenedPathGeometry definido por Geometry; mas, pela minha experiência, caminhos ampliados geralmente produzem artefatos que eu preferia evitar.

Em vez disso, eu mesmo ampliei o caminho. Essa tarefa requer o cálculo das linhas paralelas a cada uma das linhas individuais das polilinhas. No entanto, em geral essas linhas paralelas precisam ser reduzidas ou aumentadas para alcançar a mesma continuidade e forma do caminho subjacente. Contei com a ajuda de uma pequena estrutura que escrevi, denominada Line2D. Essa estrutura define a adição de um objeto Line2D e de um vetor como um deslocamento da linha. A multiplicação de dois objetos Line2D retorna um objeto Point que indica a interseção.

A solução SolidText

Passaram-se vários meses depois que escrevi RibbonText e SliverText, e eu realmente temia não conseguir avançar mais com o texto 3D. Retornei algumas vezes ao problema da triangulação da face dos caracteres de texto, mas não conseguia ver uma forma de fazer isso sem recorrer a cálculos matemáticos extremamente complexos.

Claro que eu sabia perfeitamente como colocar uma cadeia de texto em uma superfície plana em 3D. Basta usar um VisualBrush baseado em um elemento TextBlock. Certamente me ocorreu que eu poderia sobrepor essa superfície plana ao objeto RibbonText e criar a ilusão de um texto sólido. Mas eu conhecia por experiência própria os problemas associados à combinação de texto rasterizado (que é exibido por TextBlock) com texto construído a partir de contornos geométricos. Esses dois algoritmos diferentes não combinam visualmente.

A solução surgiu quando percebi que poderia, em vez disso, cobrir uma superfície plana no espaço 3D com um DrawingBrush baseado na mesma geometria usada em RibbonText. O conceito era claro; já a implementação, não.

Chamei a classe que gera esse DrawingBrush de PlanarText, derivada de Text3DBase. PlanarText precisa da propriedade TextGeometry gerada por Text3DBase, mas não da propriedade Depth definida por DeepTextBase porque (como o nome da classe indica) PlanarText exibe texto em um plano. Contudo, a classe PlanarText define uma propriedade denominada Z que indica onde o plano do texto se localiza, paralelamente ao plano XY.

PlanarText implementa o método Triangulate simplesmente definindo um retângulo plano com base em dois triângulos. As coordenadas dos quatro cantos desse retângulo vêm da propriedade Bounds associada ao objeto Geometry, obtido a partir da propriedade TextGeometry.

PlanarText é claramente diferente das demais classes derivadas de ModelVisualBase no tratamento das propriedades Material e BackMaterial.

O que acho mais interessante é que, quando ModelVisualBase foi criada, as propriedades Material e BackMaterial foram consideradas, mais do que qualquer outra coisa, como um inconveniente. ModelVisualBase deriva de ModelVisual3D, mas ModelVisual3D não define as propriedades Material e BackMaterial. A classe do WPF 3D que define essas propriedades é, na verdade, GeometryModel3D. ModelVisualBase armazena internamente uma instância dessa classe como sua propriedade Content.

ModelVisualBase precisa definir as propriedades Material e BackMaterial para que seja possível defini-las em classes derivadas na marcação, da seguinte forma:

<src:RibbonText Text="Ribbon">
    <src:RibbonText.Material>
        <DiffuseMaterial Brush="Cyan" />
    </src:RibbonText.Material>

    <src:RibbonText.BackMaterial>
        <DiffuseMaterial Brush="Pink" />
    </src:RibbonText.BackMaterial>
</src:RibbonText>

Está associado às propriedades Material e BackMaterial definidas por ModelVisualBase um manipulador de propriedade alterada denominado MaterialPropertyChanged, que simplesmente transfere qualquer objeto atribuído a essas propriedades para as propriedades idênticas do objeto GeometryModel3D interno.

Mas PlanarText precisa fazer algo diferente. Quando a propriedade Material de PlanarText é definida, por exemplo, como um objeto DiffuseMaterial, PlanarText precisa extrair o objeto Brush associado a DiffuseMaterial e criar um novo objeto DrawingBrush com base no pincel existente e na propriedade TextGeometry:

new DrawingBrush(new GeometryDrawing(brush, null, TextGeometry))

Esse DrawingBrush se torna a base de um novo objeto DiffuseMaterial, que PlanarText define como a propriedade Material do objeto GeometryModel3D interno.

Esse processo parece razoavelmente simples se o objeto atribuído à propriedade Material de PlanarText é um DiffuseMaterial. Mas a propriedade Material pode ser atribuída a um objeto MaterialGroup, e esse objeto MaterialGroup pode ter filhos do tipo DiffuseMaterial, SpecularMaterial, EmissiveMaterial ou até mesmo outro MaterialGroup aninhado. Em geral, a classe PlanarText precisa construir uma estrutura paralela de objetos Material para atribuir a seu objeto GeometryModel3D interno, e cada DrawingBrush associado a esses objetos Material precisa ser calculado a partir do objeto Brush dos objetos Material associados a PlanarText.

Devido simplesmente a essa lógica de transferência de Material, PlanarText.cs tornou-se o maior arquivo da biblioteca Text3D, embora o resultado não indique isso, como demonstra a Figura 8.

Figura 8 Exibição de PlanarTextDemo
Figura 8 Exibição de PlanarTextDemo

Ainda não estou totalmente satisfeito com a classe PlanarText. Por exemplo, se o tamanho do texto for alterado, PlanarText precisará recriar os pincéis. A classe evitará recriar uma série de objetos Material se a estrutura de suas propriedades Material e BackMaterial públicas for igual à estrutura das mesmas propriedades do objeto GeometryModel3D interno. Mas, se um dos pincéis associados a esses objetos Material for animado £ talvez com o uso de ColorAnimation £ então PlanarText será obrigada a recriar os objetos GeometryDrawing e DrawingBrush a cada passagem, o que talvez seja dispendioso.

PlanarText não é muito importante em si, mas é útil para tornar possível a classe SolidText. SolidText, mostrada na Figura 9, deriva de DeepTextBase. Isso significa que tem todas as propriedades relacionadas a texto definidas por Text3DBase e também pela propriedade Depth. Mas SolidText não gera malhas de triângulos sozinha. Ela substitui o método Triangulate, mas é retornada por ele.

Figure 9 Classe SolidText (Close Figure 9)
public class SolidText : DeepTextBase
{
    public SolidText()
    {
        // Create RibbonText and two PlanarText children.
        RibbonText ribbon = new RibbonText();
        ribbon.Depth = Depth;
        Children.Add(ribbon);

        PlanarText planar = new PlanarText();
        Children.Add(planar);

        planar = new PlanarText();
        planar.Z = -Depth;
        Children.Add(planar);
    }

    // SideMaterial dependency property and property.
    public static readonly DependencyProperty SideMaterialProperty =
        DependencyProperty.Register("SideMaterial",
            typeof(Material), typeof(SolidText),
            new PropertyMetadata(SideMaterialChanged));

    public Material SideMaterial
    {
        set { SetValue(SideMaterialProperty, value); }
        get { return (Material)GetValue(SideMaterialProperty); }
    }

    // SideMaterialChanged handlers.
    static void SideMaterialChanged(DependencyObject obj,
        DependencyPropertyChangedEventArgs args)
    {
        ((SolidText)obj).SideMaterialChanged(args);
    }

    void SideMaterialChanged(DependencyPropertyChangedEventArgs args)
    {
        // Transfer SideMaterial to RibbonText.
        Text3DBase txtbase = Children[0] as Text3DBase;
        txtbase.Material = args.NewValue as Material;
        txtbase.BackMaterial = args.NewValue as Material;
    }

    // MaterialChanged override.
    protected override void MaterialPropertyChanged(
        DependencyPropertyChangedEventArgs args)
    {
        // Transfer Material and BackMaterial properties to PlanarText.
        if (args.Property == MaterialProperty)
            ((PlanarText )Children[1] ).Material =
                                    args.NewValue as Material;

        else if (args.Property == BackMaterialProperty)
            ((PlanarText)Children[2]).BackMaterial =
                                   args.NewValue as Material;
    }

    // TextPropertyChanged override.
    protected override void TextPropertyChanged(
        DependencyPropertyChangedEventArgs args)
    {
        base.TextPropertyChanged(args);

        // Transfer text-related property to all three children.
        for (int i = 0; i < 3; i++)
        {
            Text3DBase txtbase = Children[i] as Text3DBase;
            txtbase.SetValue(args.Property, args.NewValue);
        }
    }

    // PropertyChanged override.
    protected override void PropertyChanged(
        DependencyPropertyChangedEventArgs args)
    {
        if (args.Property == DepthProperty)
        {
            // Set Depth property to RibbonText and PlanarText children.
            double depth = (double)args.NewValue;
            ((DeepTextBase )Children[0]).Depth = depth;
            ((PlanarText)Children[2]).Z = -depth;
        }

        base.PropertyChanged(args);
    }

    // Move on, move on. Nothing to see here.
    protected override void Triangulate(
        DependencyPropertyChangedEventArgs args,
        Point3DCollection vertices, Vector3DCollection normals,
        Int32Collection indices, PointCollection textures)
    {}
}

Em vez disso, SolidText tira proveito de uma propriedade herdada de ModelVisual3D, a propriedade Children. SolidText pode ter filhos do tipo ModelVisual3D e, portanto, dos tipos RibbonText e PlanarText. Qualquer transformação aplicada a SolidText será aplicada a todos os seus filhos. Além disso, SolidText define uma propriedade SideMaterial a ser aplicada ao filho RibbonText. A função principal de SolidText é distribuir as propriedades definidas nela a todos os seus filhos.

O programa SolidTextDemo gira um objeto SolidText, que você viu na Figura 1. Para testar a lógica de transferência de Material em PlanarText, usei na figura um pincel em dégradé e um material refletidor que captura a luz durante a rotação.

Preenchimento

Depois de resolver o problema básico de combinar os valores RibbonText e PlanarText em um bloco de texto unificado, decidi tentar arredondar um pouco as superfícies. A classe EllipticalText é semelhante a SliverText, exceto pelo fato de que cada ponto no contorno do caractere de texto transforma-se em uma elipse, e não em um retângulo. A classe EllipticalText ficou um pouco menor do que SliverText.

RoundedText deriva de SolidText e usa seu construtor para substituir o filho RibbonText criado por SolidText com um objeto EllipticalText. Essa combinação confere ao texto uma aparência um tanto arredondada nas extremidades do objeto PlanarText e também dá volume à parte intermediária do bloco, como você pode ver claramente na Figura 10.

Figura 10 Texto 3D sólido
Figura 10 Texto 3D sólido

O grau de arredondamento é controlado por uma propriedade de EllipticalText e RoundedText denominada EllipseWidth, que tem um valor padrão de 0,05. Definir essa propriedade como uma fração muito grande da propriedade TextSize fará com que os caracteres mesclem-se uns com os outros, resultando no efeito "Z fighting" durante a animação.

Espero, algum dia, identificar um algoritmo que permita triangular os contornos de fontes com mais flexibilidade. Enquanto isso, terei de me contentar com essas classes. Sei que o mundo sofre com a falta de logotipos flutuantes com efeitos 3D incorporados, então espero sinceramente que a minha modesta contribuição aqui ajude a aliviar essa escassez.

Envie suas dúvidas e seus comentários para Charles em cp@charlespetzold.com.

Charles Petzold é editor colaborador da MSDN Magazine. Seu livro mais recente é "3D Programming for Windows" (Programação em 3D para Windows; Microsoft Press, 2007).


Windows com C++: Agendador de Tarefas 2.0

Windows com C++
Agendador de Tarefas 2.0
Kenny Kerr

Download do código disponível em: WindowsWithC++2007_10.exe (156KB)
Conteúdo
Serviço de tarefa e armazenamento
Usando o serviço de tarefa
Definições de tarefa
Ações de tarefa
Disparadores de tarefa
O que vem a seguir?

O
Agendador de Tarefas recebeu uma revisão completa no Windows Vista®. Embora haja algumas semelhanças, o novo Agendador de Tarefas (apelidado de Agendador de Tarefas 2.0) é muito mais eficiente do que o original, presente desde o Windows® 98. Não se trata mais apenas de uma simples ferramenta para usuários finais, e sim uma plataforma eficiente para a criação e o gerenciamento de operações complexas em segundo plano – tanto que, em muitos casos, ele é capaz de remover a necessidade do desenvolvimento de serviços do Windows.

Imagine que o projeto precise verificar se há atualizações automaticamente. Você talvez pense em escrever um serviço do Windows que seja executado em segundo plano e verifique se há atualizações em intervalos de alguns dias. Em lugar de um serviço que precisa ser executado o tempo todo, é possível projetar uma tarefa agendada que seja executada apenas em alguns dias, verifique se há atualizações e pare. O que é ainda melhor, é possível garantir que ela só seja executada quando um usuário fizer logon para que os recursos não sejam consumidos desnecessariamente quando não houver ninguém para realizar uma atualização.

É evidente que os desenvolvedores da Microsoft consideram o novo Agendador de Tarefas pronto para o necessário pelo simples fato de ser usado por muitas partes do Windows Vista para gerenciar as tarefas em segundo plano. Isso tem o benefício adicional de reduzir o número de novos serviços que acabariam sendo criados inevitavelmente para tratar todas essas tarefas. Isso também simplifica a administração e os diagnósticos do Windows, por ser muito mais simples interrogar uma tarefa para ver o que ela está fazendo do que adivinhar o que virá de um serviço caixa-preta do Windows. Para tarefas que fazem parte do próprio Windows, o Agendador de Tarefas também funciona como um host de tarefas, consolidando muitos processos específicos de tarefa em um só, o que reduz ainda mais os recursos necessários à hospedagem dessas operações.

Nesta coluna, eu irei explorar os principais conceitos e os blocos de construção que formam o Agendador de Tarefas para que você possa compreendê-lo o mais rápido possível e comece a aproveitar imediatamente esse novo serviço excelente.

Serviço de tarefa e armazenamento

A história começa com o serviço Agendador de Tarefas, o serviço responsável pelo agendamento real das tarefas. Como o Agendador de Tarefas usa o sistema de arquivos para armazenar informações sobre a tarefa, é possível enumerar e preparar as tarefas sem a ajuda do serviço, mas você não irá muito longe sem ele. Na verdade, como as versões futuras do Agendador de Tarefas podem trocar tranqüilamente de formato e de local, confiar nisso não é recomendável. De fato, é essencial que você não confie no acesso direto ao armazenamento, uma vez que o Agendador de Tarefas talvez não reconheça as tarefas gravadas diretamente no sistema de arquivos e deverá se recusar a executar tarefas que foram violadas. Como o Agendador de Tarefas é parte integrante do Windows Vista, os administradores não podem mais simplesmente desabilitá-lo. Trata-se de uma boa notícia para os desenvolvedores porque é possível confiar tranqüilamente nele para a realização das operações em segundo plano e a abstração do armazenamento da tarefa.

Por convenção, as tarefas são agrupadas por empresa, produto e nome do componente. Por exemplo, o Desfragmentador de Disco do Windows armazena as tarefas na subpasta \Microsoft\Windows\Defrag. Cada uma delas é armazenada como sendo um documento XML definido pelo esquema do Agendador de Tarefas. Dessa forma, é possível criar tarefas diretamente com um analisador XML como o XmlLite (consulte o meu artigo anterior publicado na MSDN® Magazine em msdn.microsoft.com/msdnmag/issues/07/04/Xml/) ou use as interfaces COM extensivas fornecidas pela API do Agendador de Tarefas. As interfaces COM também permitem que você forneça e consulte diretamente as definições XML das tarefas. Dessa forma, você tem o benefício de usar XML para criar e consultar tarefas ao mesmo tempo em que fica livre do armazenamento real. O Agendador de Tarefas também pode ser acessado remotamente.

Usando o serviço de tarefa

A interface ITaskService é o gateway na API do Agendador de Tarefas. Todas as definições de que você precisará estão localizadas no arquivo de cabeçalho taskschd.h. É claro que você precisará do SDK do Windows que inclui o suporte ao Windows Vista. Comece criando uma instância local em processo da seguinte forma:

CComPtr<ITaskService> service;
HR(service.CoCreateInstance(__uuidof(TaskScheduler)));

(Eu uso a macro HR nesses exemplos para identificar claramente onde os métodos retornam HRESULT que precisa ser verificado; é possível substituir isso pelo tratamento de erros apropriado – independentemente de estar lançando uma exceção ou retornando HRESULT para você mesmo.)

Em seguida, chame o método Connect para estabelecer uma conexão com o Agendador de Tarefas do computador escolhido por você. O método Connect é definido da seguinte forma:

HRESULT Connect(
    VARIANT computer, VARIANT user,
    VARIANT domain, VARIANT password);

O uso do tipo VARIANT está equivocado mas é fácil o bastante para lidar com o uso da prática classe derivada ATL (Active Template Library) CcomVariant. Para se conectar ao computador local, basta omitir o nome do computador. Para se conectar usando a identidade do chamador ou o token efetivo, não especifique um nome de usuário. Para usar o domínio atual do computador local, não especifique um nome de domínio. Por fim, só especifique uma senha caso você também tenha especificado um nome de usuário. Dessa forma, em sua forma mais simples, você pode chamar o método Connect da seguinte forma para se conectar ao serviço do Agendador de Tarefas do computador:

HR(service->Connect(
    CComVariant(),   // local computer
    CComVariant(),   // current user
    CComVariant(),   // current domain
    CComVariant())); // no password

Agora é possível realizar várias operações como, por exemplo, criar tarefas e enumerar pastas e tarefas preexistentes. A primeira etapa é conhecer a interface ItaskFolder, uma vez que as tarefas não podem ser usadas até que sejam armazenadas em uma pasta. O método GetFolder de ItaskService retorna um ponteiro de interface ITaskFolder para uma determinada pasta. Uma barra invertida representa a pasta da tarefa raiz, conforme ilustração a seguir:

CComPtr<ITaskFolder> folder;
HR(service->GetFolder(CComBSTR(L"\\"), &folder));

Assim, é possível criar uma pasta para o aplicativo da seguinte forma:

CComPtr<ITaskFolder> newFolder;
HR(folder->CreateFolder(
    CComBSTR(L"Company\\Product"),
    CComVariant(), &newFolder));

O segundo parâmetro opcional para CreateFolder especifica o descritor de segurança para a pasta do sistema de arquivos recém-criada. Se ele for omitido, a pasta apenas herdará o descritor de segurança do pai. É possível substituir o controle de acesso padrão usando a SDDL (Security Descriptor Definition Language). Por exemplo, para permitir aos administradores, bem como ao sistema local, o controle total, você deve usar a seguinte SDDL:

HR(folder->CreateFolder(
    CComBSTR(L"Company\\Product"),
    CComVariant(L"D:(A;;FA;;;BA)(A;;FA;;;SY)"),
    &newFolder));

Rapidamente, D: indica que as próximas ACEs (entradas de controle de acesso) formam parte da DACL (lista de controle de acesso discricionário). A primeira entrada indica que o acesso está sendo permitido (A) com todo o arquivo (FA) sendo concedido a administradores internos (BA). A segunda entrada indica que o mesmo acesso está sendo concedido ao sistema local (SY). Não se esqueça de que a definição do descritor de segurança só é usada na criação da pasta Produto e que a pasta pai Empresa continuará sendo criada com um descritor de segurança herdado. Além disso, lembre-se de que você não precisa acessar a conta Sistema Local porque ela é a identidade usada pelo serviço do Agendador de Tarefas e deverá haver um comportamento inesperado quando o agendador não conseguir acessar as tarefas que deveria agendar!

Dada uma pasta preexistente, é possível usar o método GetFolder de ItaskService para abrir uma pasta referente à pasta da tarefa raiz. Observe que ITaskFolder oferece o mesmo método para abrir pastas referentes à pasta representada pela instância da interface:

CComPtr<ITaskFolder> productFolder;
HR(service->GetFolder(
    CComBSTR(L"Company\\Product"),
    &productFolder));

CComPtr<ITaskFolder> componentFolder;
HR(productFolder->GetFolder(
    CComBSTR(L"Component"),
    &componentFolder));

A Figura 1 ilustra o aplicativo de exemplo Task Scheduler Explorer incluído no download desta coluna. Ele oferece uma exibição prática das tarefas e é muito útil quando você começa a usar a API do Agendador de Tarefas.

Figura 1 Task Scheduler Explorer
Figura 1 Task Scheduler Explorer(Clique na imagem para diminuir a exibição)
Figura 1 Task Scheduler Explorer
Figura 1 Task Scheduler Explorer (Clique na imagem para aumentar a exibição)

Definições de tarefa

Antes de ser criada, uma tarefa precisa ser modelada por uma definição. A Figura 2 descreve os componentes que formam uma definição de tarefa. Por conta da complexidade de uma definição e de sua necessidade na criação de tarefas, eu não recomendo passar diretamente à criação de tarefas. Ajuda muito compreender primeiro os blocos de construção fundamentais. Antes que eu me aprofunde nos aspectos específicos da definição de ações e disparadores, vejamos rapidamente como uma definição de tarefa é composta e criada.

Figure 2 Componentes de definição da tarefa (Close Figure 2)

Tarefa | Definição
Actions | Uma ou mais ações definem o trabalho a ser realizado pela tarefa.
Triggers | Um ou mais disparadores indicam quando a tarefa deve começar.
Principal | A autorização e a auditoria baseiam-se no contexto de segurança.
Settings | Essas configurações controlam o comportamento do runtime e os limites da tarefa.
Data | Uma cadeia de caracteres disponível a ações.
RegistrationInfo | Informações contábeis administrativas.

Comece pedindo para que o serviço do Agendador de Tarefas crie uma definição de tarefa vazia que seja possível preencher:

CComPtr<ITaskDefinition> definition;
HR(service->NewTask(0, // reserved
                    &definition));

Antes de registrar a definição de tarefa em uma pasta específica, você precisa preenchê-la com pelo menos uma ação e, opcionalmente, com disparadores e outras informações. Assim que isso estiver pronto, será possível registrá-la em uma pasta usando o método RegisterTaskDefinition da seguinte forma:

CComPtr<IRegisteredTask> registeredTask;
HR(folder->RegisterTaskDefinition(
    CComBSTR(L"Task"),
    definition,
    TASK_CREATE_OR_UPDATE,
    CComVariant(), // user name
    CComVariant(), // password
    TASK_LOGON_INTERACTIVE_TOKEN,
    CComVariant(), // sddl
    &registeredTask));

Um ponteiro de interface IRegisteredTask é retornado representando a tarefa recém-registrada. Trata-se da interface usada para iniciar ou parar tarefas, bem como recuperar informações de runtime sobre uma tarefa.

O primeiro parâmetro de RegisterTaskDefinition especifica o nome da nova tarefa. Como esse valor também é usado para nomear o arquivo salvo na pasta, ele deve estar em conformidade com as convenções de nomenclatura do sistema de arquivos. É possível definir esse parâmetro como NULL, e o método irá gerar um GUID a ser usado como nome, mas isso não é recomendável por não ser muito significativo ou muito útil para administradores do computador. O segundo parâmetro especifica a definição de tarefa descrevendo a tarefa a ser criada. Examinarei isso mais detalhadamente nas seções a seguir. O terceiro parâmetro especifica um valor da enumeração TASK_CREATION. Os valores mais comuns são TASK_ CREATE e TASK_UPDATE, enquanto TASK_CREATE_OR_UPDATE é apenas OR bit a bit de ambos.

Os próximos três parâmetros especificam as credenciais de registro. RegisterTaskDefinition garantirá que o usuário especificado tenha pelo menos acesso de leitura à tarefa registrada, mesmo que a DACL especificada por você no parâmetro seguinte não o conceda especificamente. O Agendador de Tarefas dá suporte a várias técnicas de logon diferentes. O exemplo anterior usa TASK_LOGON_INTERACTIVE_TOKEN, o que indica que a tarefa só será executada em uma determinada sessão de logon interativa do usuário especificado. Se um nome de usuário e senha não forem fornecidos, ele presumirá a identidade do chamador. Uma senha não é armazenada desde que uma sessão de logon interativa seja obrigatória.

TASK_LOGON_PASSWORD é uma alternativa que instrui o Agendador de Tarefas a criar uma sessão de logon em lotes. Nesse caso, tanto um nome de usuário quanto uma senha devem ser fornecidos, e a conta deve receber o privilégio "Fazer logon como um trabalho em lotes".

TASK_LOGON_S4U é outra opção que proporciona uma alternativa mais segura. Ela tira proveito de um serviço de logon do usuário (S4U) para executar a tarefa em nome do usuário especificado, mas sem precisar armazenar a senha. Como o Agendador de Tarefas é executado na conta do sistema local, ele pode criar uma sessão de logon S4U e receber um token que pode não apenas ser usado para a identificação, mas também para a representação no computador local. Normalmente, um token S4U só é bom para a identificação.

Além das credenciais fornecidas no momento em que você registra uma tarefa, a interface Iprincipal oferece maior controle sobre o contexto de segurança a ser apresentado para a tarefa. Em especial, IPrincipal oferece uma forma prática de controlar o nível de execução do processo que hospeda o mecanismo do Agendador de Tarefas da tarefa. Comece consultando a definição de tarefa do objeto principal associado e defina a propriedade RunLevel conforme o necessário. Isso precisa ser feito antes do registro da definição de tarefa. Veja um exemplo:

CComPtr<IPrincipal> principal;
HR(definition->get_Principal(&principal));
HR(principal->put_RunLevel(TASK_RUNLEVEL_HIGHEST));

TASK_RUNLEVEL_HIGHEST indica que a tarefa deve ser executada em grande parte do contexto de segurança sem restrições do usuário em especial. Presumindo que o usuário seja um administrador, ele fornecerá ao processo um token não restringido pelo UAC (Controle de Conta de Usuário). A alternativa é TASK_RUNLEVEL_LUA, que oferece um token restrito, sem execução privilegiada.

Ações de tarefa
Agendador de Tarefas .NET

Embora não haja um wrapper Microsoft® .NET Framework para o Agendador de Tarefas, não é difícil de usá-lo graças a um recurso do CLR (Common Language Runtime) conhecido como RCW (Runtime Callable Wrapper).

Como o Agendador de Tarefas oferece interfaces COM amigáveis em relação ao script, é possível usar a API do código gerenciado com pouco esforço. Basta referenciar a biblioteca de tipos usando a caixa de diálogo Adicionar Referência do Visual Studio®. Isso é um pouco enganador, já que a biblioteca de tipos do Agendador de Tarefas 2.0 se chama TaskScheduler1.1 Type Library. No entanto, o Visual Studio criará um assembly de interoperabilidade que oferece todas as definições gerenciadas para as interfaces COM de forma que seja possível criar e referenciar os vários objetos COM diretamente em código gerenciado usando o RCW.

Aqui está um exemplo ilustrando o equivalente em C# dos exemplos de código da primeira seção desta coluna, conectando-se ao serviço do Agendador de Tarefas e criando uma pasta para as tarefas do produto:

ITaskService service = new TaskSchedulerClass();
service.Connect(null, // local computer
                null, // current user
                null, // current domain
                null); // no password

ITaskFolder folder = service.GetFolder("\\");
ITaskFolder newFolder = folder.CreateFolder(
    "Company\\Product", "D:(A;;FA;;;BA)(A;;FA;;;SY)");

Daqui é possível criar definições de tarefa, ações, disparadores e muito mais, tudo com a comodidade da sua linguagem favorita destinada ao .NET Framework.

Com o fundamento já abordado, vejamos a variedade dos tipos de ações fornecidas pelo Agendador de Tarefas. As ações são criadas e adicionadas a uma definição de tarefa usando o ponteiro de interface IActionCollection retornado pela propriedade Actions de definição da tarefa:

CComPtr<ITaskDefinition> definition;
HR(service->NewTask(0, // reserved
                    &definition));

CComPtr<IActionCollection> actions;
HR(definition->get_Actions(&actions));

IActionCollection fornece um método Create que cria um novo objeto de ação e o adiciona à coleção. As ações propriamente ditas são expostas por meio de interfaces que derivam de IAction. Vejamos um exemplo disso:

CComPtr<IAction> action;
HR(actions->Create(TASK_ACTION_EXEC, &action));

TASK_ACTION_EXEC é, de longe, o tipo de ação mais comum por descrever uma operação de linha de comando e poder ser usado para iniciar todos os tipos de programa. Ele pode até mesmo abrir documentos e outros tipos de arquivo registrados usando o shell do Windows. Para definir as propriedades específicas da ação, você deve primeiro consultar a interface específica do tipo, e é isso o que CComQIPtr faz de forma elegante:

CComQIPtr<IExecAction> execAction(action);

Dado um ponteiro de interface IExecAction, agora é possível configurar a ação para executar o comando escolhido por você. A seguir, um exemplo que ativa a ferramenta Contig da Sysinternals para desfragmentar as imagens do meu disco rígido virtual:

HR(execAction->put_Path(CComBSTR(L"g:\\Tools\\contig.exe")));
HR(execAction->put_Arguments(CComBSTR(L"-s g:\\VirtualMachines")));

Embora a execução de um comando seja prática para os administradores, ela nem sempre é a solução mais apropriada para os desenvolvedores, porque o comando não tem nenhuma percepção da ação que representa na tarefa ou de que é parte de uma tarefa agendada. Felizmente, o Agendador de Tarefas oferece outro tipo de ação projetado especificamente para corrigir esse problema. O tipo de ação TASK_ACTION_COM_HANDLER cria um servidor COM e o consulta a respeito da interface ITaskHandler que você deve implementar. Isso estabelece um canal de comunicação entre a ação e o mecanismo do Agendador de Tarefas, o que possibilita uma integração mais transparente entre as operações específicas do aplicativo e o Agendador de Tarefas.

Veja como criar uma ação COM:

CComPtr<IAction> action;
HR(actions->Create(TASK_ACTION_COM_HANDLER, &action));

CComQIPtr<IComHandlerAction> comAction(action);
HR(comAction->put_ClassId(CComBSTR(
    L"{25C6DB11-4ADC-4e89-BA47-04576C7AA46A}")));

É claro que você precisará de um servidor COM que seja registrado com o CLSID (identificador de classe) e que implemente a interface ITaskHandler. A Figura 3 fornece um exemplo de uma classe COM que implementa ITaskHandler com a ajuda da ATL.

Figure 3 Classe de ação COM (Close Figure 3)
class DECLSPEC_UUID("25C6DB11-4ADC-4e89-BA47-04576C7AA46A")
DECLSPEC_NOVTABLE CoSampleTask :
    public CComObjectRootEx<CComMultiThreadModel>,
    public CComCoClass<CoSampleTask, &__uuidof(CoSampleTask)>,
    public ITaskHandler
{
public:
    DECLARE_REGISTRY_RESOURCEID(IDR_SAMPLETASK)
    BEGIN_COM_MAP(CoSampleTask)
        COM_INTERFACE_ENTRY(ITaskHandler)
    END_COM_MAP()

private:
    STDMETHODIMP Start(IUnknown* taskScheduler, BSTR data);
    STDMETHODIMP Stop(HRESULT* actionResult);
    STDMETHODIMP Pause();
    STDMETHODIMP Resume();
};

Há uma etapa adicional a ser realizada que, na época em que escrevi este artigo, não estava documentada em lugar algum, mas sem a qual haverá uma falha no carregamento da ação COM. O mecanismo do Agendador de Tarefas espera carregar o servidor COM em um processo separado usando o contexto CLSCTX_LOCAL_SERVER ao chamar a função CoCreateInstance com o CLSID (a menos que a ação faça parte do próprio Windows, quando ela pode ser carregada em processo). Para que isso dê certo, o servidor COM deve estar configurado para permitir a ativação em um processo substituto. Tecnicamente, você poderia usar um componente fora do processo tradicional, embora isso seja menos comum e não muito prático na maioria dos casos. Isso é facilmente resolvido com a atualização do código de registro. Atualize o registro ATL para incluir o valor DllSurrogate. Veja um exemplo:

HKCR
{
    NoRemove AppID
    {
        '%APPID%' = s 'SampleTask'
        {
            val DllSurrogate = s ''
        }
        'SampleTask.DLL'
        {
            val AppID = s '%APPID%'
        }
    }
}

As ações COM são superiores aos demais tipos de ação em vários aspectos. Especialmente, uma ação COM é o único tipo que permite que uma ação seja parada de maneira amigável e previsível. Depois que o mecanismo do Agendador de Tarefas cria uma instância da classe COM, ele chama o método Start, fornecendo um ponteiro de interface que você pode usar para se comunicar com o Agendador de Tarefas. Em seguida, o método Start pode consultar a interface ITaskHandlerStatus, fornecendo um mecanismo simples para que a tarefa notifique o Agendador de Tarefas a respeito do progresso e de sua eventual conclusão. A ação pode ser encerrada de duas formas. Um usuário pode optar por encerrar a tarefa usando vários métodos, todos os quais resultando na chamada do método Stop da classe COM. Como alternativa, a classe COM pode informar que concluiu suas tarefas chamando o método TaskCompleted de ITaskHandlerStatus.

É fornecido um outro tipo de ação útil que permite a uma tarefa enviar mensagens de email. Diferentemente dos tipos de ação anteriores, este é menos útil para a realização de operações ou tarefas administrativas, embora seja ideal para ser usado como um mecanismo de notificação. A próxima seção lida com disparadores, mas, por enquanto, basta dizer que as tarefas podem ser iniciadas por eventos que não sejam os tradicionais com base no calendário ou no relógio. Veja como criar uma ação de email:

CComPtr<IAction> action;
HR(actions->Create(TASK_ACTION_SEND_EMAIL, &action));

CComQIPtr<IEmailAction> emailAction(action);
HR(emailAction->put_From(CComBSTR(L"kenny@example.com")));
HR(emailAction->put_To(CComBSTR(L"karin@example.com")));
HR(emailAction->put_Subject(CComBSTR(L"subject")));
HR(emailAction->put_Body(CComBSTR(L"body")));
HR(emailAction->put_Server(CComBSTR(L"mail.example.com")));

Disparadores de tarefa

O Agendador de Tarefas fornece vários disparadores que lhe permitem iniciar as tarefas sem a intervenção do usuário. Há os disparadores habituais com base no relógio e no calendário, bem como vários disparadores úteis com base em eventos.

As tarefas são criadas e adicionadas a uma definição usando o ponteiro de interface ITriggerCollection retornado pela propriedade Triggers de definição da tarefa:

CComPtr<ITriggerCollection> triggers;
HR(definition->get_Triggers(&triggers));

ITriggerCollection fornece um método Create que cria um novo objeto de disparador e o adiciona à coleção. Os disparadores propriamente ditos são expostos por meio de interfaces que derivam de ITrigger. A seguir, um exemplo de um disparador que pode ser apropriado ao exemplo da seção anterior para iniciar uma operação de desfragmentação:

CComPtr<ITrigger> trigger;
HR(triggers->Create(TASK_TRIGGER_WEEKLY, &trigger));

CComQIPtr<IWeeklyTrigger> weeklyTrigger(trigger);
HR(weeklyTrigger->put_StartBoundary(CComBSTR(
    L"2007-01-01T02:00:00-08:00")));
HR(weeklyTrigger->put_DaysOfWeek(0x01)) // Sunday

TASK_TRIGGER_WEEKLY é um dos 11 tipos de disparadores disponíveis na versão inicial do novo Agendador de Tarefas. A Figura 4 fornece a você um resumo dos tipos de disparador disponíveis, bem como a interface COM associada a cada um. A propriedade StartBoundary indica a data mais antiga em que a tarefa será disparada, bem como a hora do dia em que ela será iniciada. A propriedade DaysOfWeek especifica os determinados dias da semana em que a tarefa é executada. Ela usa bitmask para que seja possível combinar os dias desejados. O disparador no exemplo acima fará com que a tarefa seja executada em todos os domingos, às 02h00. PST começando no primeiro dia de 2007.

Figure 4 Tipos de disparadores (Close Figure 4)

Tipo | Interface | Uso e opções
TASK_TRIGGER_EVENT | IEventTrigger | Eventos definidos pelo log de eventos do Windows.
TASK_TRIGGER_TIME | ITimeTrigger | Hora do dia; atraso aleatório opcional.
TASK_TRIGGER_DAILY | IDailyTrigger | A cada n dias; atraso aleatório opcional.
TASK_TRIGGER_WEEKLY | IWeeklyTrigger | Dias da semana especificados; a cada n semanas; atraso aleatório opcional.
TASK_TRIGGER_MONTHLY | IMonthlyTrigger | Dias do mês especificados; meses especificados; último dia opcional do mês; atraso aleatório opcional.
TASK_TRIGGER_MONTHLYDOW | IMonthlyDOWTrigger | Dias da semana especificados; semanas do mês especificadas; meses do ano especificados; última semana opcional do mês; atraso aleatório opcional.
TASK_TRIGGER_IDLE | IIdleTrigger | Quando o computador está ocioso.
TASK_TRIGGER_REGISTRATION | IRegistrationTrigger | Quando a tarefa é criada ou atualizada; atraso opcional.
TASK_TRIGGER_BOOT | IBootTrigger | Quando um computador é inicializado; atraso opcional.
TASK_TRIGGER_LOGON | ILogonTrigger | Quando um usuário faz logon; atraso opcional.
TASK_TRIGGER_SESSION_STATE_CHANGE | ISessionStateChangeTrigger | Vários eventos de sessão.

O que vem a seguir?

Há muitas razões convincentes para usar o novo Agendador de Tarefas no Windows Vista e além dele. Se confiou no Agendador de Tarefas nas versões anteriores do Windows, você certamente ficará feliz por todos os novos recursos. Na verdade, ele pode substituir, em breve, muitos agendadores internos personalizados escritos para os aplicativos atuais.

Não cheguei nem perto de abordar todos os recursos do Agendador de Tarefas nesta coluna. Dedique alguns momentos para explorar a documentação sobre o SDK do Windows (msdn2.microsoft.com/en-us/library/aa383614.aspx), e você descobrirá alguns recursos adicionais, inclusive a possibilidade de criar tarefas diretamente em XML, maior controle sobre o contexto de segurança no qual as tarefas são executadas, a chance de enumerar e gerenciar tarefas em execução e muito mais!

Envie suas perguntas a Kenny para mmwincpp.

Kenny Kerr é um profissional de fabricação de software especializado no desenvolvimento para o Windows. Ele adora escrever e ensinar programação e design de softwares aos desenvolvedores. Entre em contato com Kenny em weblogs.asp.net/kennykerr.


Netting C++: Descrevendo o sistema EEK!

Netting C++
Descrevendo o sistema EEK!
Stanley B. Lippman

Conteúdo
Perigos e recompensas no ambiente
O layout do mundo
Resumo

N
a edição anterior desta coluna, comecei a criar o meu rato e a simulação do seu ambiente, chamado EEK! Discuti os dois principais métodos de análise do arquivo XML do ambiente: o firehose e o DOM (Document Object Model). Neste mês, começarei a criar o documento XML e mostrarei outra forma de incorporar um arquivo XML ao programa: usando um DataSet, encontrado no namespace System::Data da biblioteca MFC do .NET. O DataSet utiliza o documento como um conjunto de tabelas relacionais que você pode percorrer usando valores de chave exclusivos.

A tarefa prioritária será gerar uma descrição do ambiente simulado para permitir que o programa reconstrua e manipule aquele mundo durante a execução de seu aplicativo. Para fazer isso, eu preciso determinar o melhor design para o documento XML que represente o terreno que o rato irá habitar na simulação.

O ambiente que criei (se você ainda não o conhece, leia a coluna de junho de 2007 em msdn.microsoft.com/msdnmag/issues/07/06/NettingC) possui uma restrição exclusiva. Preciso ser capaz de representá-lo em dimensões macro e micro e poder deslizar por elas para examinar o estado de cada uma simultaneamente £ na marcação do simulador. Por exemplo, em nível micro, quero poder rastrear o movimento das moléculas químicas do ambiente (o cheiro de entidades diferentes, como a comida ou um gato). Em nível macro, preciso controlar o andamento e as ações resultantes do rato. Grande parte do comportamento de um rato é orientada por moléculas químicas que disparam funções de programa associadas porque, ao contrário dos humanos, ele depende do faro, e não da visão, para explorar seu ambiente. Eu chamo isso de Scalable Environment Map (Mapa de Ambiente Escalável) ou SEM.

Perigos e recompensas no ambiente

O ambiente simulado em si é somente um mapeamento, embora contenha perigos e recompensas. Ele representa uma área contígua pela qual o rato se movimenta, encontrando entidades importantes, como locais para a criação de ninhos, comida, barulhos, choques elétricos, odores e assim por diante (consulte a Figura 1). Esses encontros entre o rato e o ambiente colorem literalmente o ambiente.

Figura 1 Atributos do quadrado do mundo neutro
Figura 1 Atributos do quadrado do mundo neutro(Clique na imagem para diminuir a exibição)
Figura 1 Atributos do quadrado do mundo neutro
Figura 1 Atributos do quadrado do mundo neutro (Clique na imagem para aumentar a exibição)

Na simulação, a experiência do rato explorando o ambiente é "lembrada" pela conservação de um mapa pessoal. Nesse mapa, que no início está em branco, uma cor representa como o rato experimenta aquela parte do ambiente (suas interações com as entidades encontradas) e permite que ele identifique e avalie áreas subseqüentes do ambiente ao reconhecer padrões similares. Existem cinco cores associadas a quatro avaliações.

Verde e azul escuro indicam uma experiência de recompensa £ comida e bebida, um local para fazer o ninho e assim por diante. O azul brilhante denota medo £ algum encontro aterrador (nesse caso, representado por um choque elétrico simulado). Vermelho significa raiva ou frustração £ alguma obstrução no caminho do rato (na forma do que eu chamo de barulhos). E o laranja representa uma experiência neutra.

Um painel da exibição da simulação mostra as cores em tempo real do ambiente à medida que ele é revelado. Como o ambiente é proposital, os mapas resultantes e a capacidade do rato de alcançar seus objetivos devem ser previsíveis com base em sua natureza configurada.

Em termos práticos, isso significa que há somente um mapa da "verdade" e um mapa subjetivo para cada movimento (consciente) de entidades na simulação. O mapa da verdade é criado a partir do documento XML e representa a parte física do ambiente simulado. Cada mapa subjetivo é criado pela exploração e colorido pela experiência pessoal. Ele sempre será um subconjunto do mapa da "verdade". A primeira aventura do rato será marcar um território pessoal por meio de seu mapa subjetivo, que contém comida e água e um local para o ninho e que ele poderá defender. Até eu preencher a simulação com várias entidades sensíveis competindo por território, os barulhos e choques elétricos do ambiente representarão perigos e restrições. A Figura 1 dá uma idéia de como um quadrado do mundo do ambiente é preenchido.

O ambiente é separado em quadrados, conectados por meio de uma a quatro "passagens", que representam entradas e saídas disponíveis para o rato. A simulação começa em uma câmara inicial que possui três passagens representando caminhos alternativos. Essa câmara é "desagradável" para um rato: não tem vegetação e é totalmente exposta a uma luz branca intensa. Por causa do desempenho, a simulação só mantém o quadrado em que o rato está explorando no momento, além de todos os quadrados conectados a ele por uma passagem. Esse se torna o conjunto de trabalho (consulte a Figura 2).

Figura 2 Quadrados do mundo em memória
Figura 2 Quadrados do mundo em memória

O layout do mundo

Os elementos que precisam ser representados no documento XML consistem no conjunto de quadrados, as passagens que os conectam e as entidades que acontecem neles:

<element name="tile" ...
<element name="doorto" ...
<element name="burr" ...
<element name="e-shock" ...

A primeira questão é determinar como relacionar os elementos uns aos outros. Cada elemento de seu tipo possui atributos exclusivos, embora repita aspectos de instâncias anteriores que podem ter sido encontradas pelo rato (sempre que ele encontrar um elemento que faça o rato "lembrar" de uma experiência anterior, a cor da experiência anterior será transferida para o novo elemento. Com o tempo, isso fará com que as reações do rato em relação ao ambiente sejam mais rápidas).

Uma forma de organizar o XML é aninhando a descrição de cada elemento no quadrado em que ele ocorre. Na Figura 1, isso poderia significar que os dois barulhos, os dois odores, o choque elétrico, o local do ninho e a comida e bebida seriam incluídos na descrição do quadrado. Dessa forma, ao analisar a definição do quadrado, você também terá uma descrição completa de todas as entidades que ele contém. Isso também complica a descrição e o próprio documento se torna difícil de compreender para um leitor.

Em vez disso, o documento agrupa cada entidade de forma separada £ primeiro todos os quadrados, depois as passagens, os barulhos e assim por diante. Cada entidade do quadrado é representada como um identificador exclusivo, mais o seu local no quadrado. Ou seja, os elementos são apresentados no documento como um conjunto de tabelas associadas por chaves.

Uma segunda motivação para não definir elementos no quadrado em que eles ocorrem é a dupla natureza, por assim dizer, das passagens. Uma passagem conecta dois (e somente dois) quadrados. Na verdade, ela não pertence a nenhum dos lados. Isso é similar a um problema de design que em parte motivou o mecanismo de amizade no C++. Eu crio uma classe Matrix e uma classe Vector. Agora eu crio um operador de multiplicação para multiplicar uma Matrix por um Vector. Onde vou colocá-lo? Ele também não pertence a nenhum dos dois; no entanto, por causa do desempenho, digo que ele é amigo de ambos. Essa é a minha forma de pensar em passagens; na verdade, elas não pertencem a nenhum dos quadrados que conectam.

Assim como acontece na maioria das soluções de design, essa apresenta um problema: como conectar uma passagem a um quadrado, ou a um barulho, ou a um odor ou a qualquer outra entidade. Faço isso combinando uniqueIDs entre os dois conjuntos de entradas. Isso imita a representação de um banco de dados relacional e eu posso usar o suporte ao banco de dados do namespace System::Data das bibliotecas MFC do Microsoft® .NET Framework, como indicado anteriormente.

Um DataSet representa uma coleção de uma ou mais instâncias de DataTable. Nesse caso, o conjunto de quadrados representa a tabela principal. Cada entidade de ambiente, como barulho ou passagem, será armazenada como uma DataTable adicional. Para cada atributo do quadrado, recupero seu uniqueID e faço uma referência cruzada para a tabela associada adequada.

A conversão da representação XML em DataSEt é muito simples. Um documento XML e um banco de dados relacional compartilham um relacionamento especial, em um sentido similar ao do relacionamento entre água e gelo. Você pode considerá-los como formas alternativas dos mesmos dados, cada uma adequada a atividades diferentes.

Um DataSet possui um método ReadXml que obtém um nome de caminho de documento XML e preenche o objeto de DataSet. Por exemplo:

DataSet^ ds = gcnew DataSet;
ds->ReadXml( eek_world_xml_path_string );

Embora o objeto de DataSet armazene agora o documento XML como um conjunto de DataTables, ele ainda não reconhece o relacionamento entre as tabelas. É preciso que isso seja esclarecido. Esse esclarecimento é feito pela adição de uma propriedade DataRelation ao DataSet. DataRelation define um relacionamento entre duas tabelas com base em uma coluna compartilhada, que representa o identificador exclusivo do elemento, ou a chave.

A propriedade Tables da classe DataSet retorna um objeto de DataTableCollection que contém o conjunto de objetos de DataTable associados ao DataSet. Para acessar uma instância específica, você pode indexá-la por posição

enum class tableIndex {
    tile, doorto, burr, e-shock, ...
};

DataTable^ world_tile = ds->Tables[tile];

ou através de associações, usando uma representação de cadeia de caracteres da tabela que deseja acessar:

DataTable^ world_tile = ds->Tables[tile];
world_tile = ds->Tables[ worldTile_table_string ];

Existem algumas propriedades associadas a DataTable, como TableName, Rows, Columns e PrimaryKey:

// returns the name of the table ...
String^ tabName = world_tiles->TableName;
DataRowCollection^    drows = world_tiles->Rows;
DataColumnCollection^ ccols = world_tiles->Columns;
int countRows = drows->Count;
int countCols = ccols->Count;
array< DataColumn^ >^ keys = world_tile->PrimaryKey;

A chave primária é uma coluna que certamente contém um valor exclusivo para cada entrada de registro e que também serve como identidade do registro na tabela (uma chave primária também pode ser um conjunto de colunas. É por isso que a propriedade PrimaryKey retorna uma matriz de objetos de DataColumn em vez de somente um objeto).

Na tabela tile, por exemplo, o atributo doorto contém o uniqueID da passagem. Essa chave é a chave primária usada na organização das entradas de passagens na tabela doorto.

Eu identifico esse relacionamento ao definir um objeto da classe DataRelation vinculando as colunas das duas tabelas que armazenam o valor do identificador da chave:

DataRelation^ doorto = gcnew DataRelation("UniqueID",
    ds->Tables["worldTile"]->Columns[ doortoID_string ],
    ds->Tables["doorto"]->Columns[ doortoID_key ]);

A primeira coluna é conhecida como pai. Seu valor é usado na recuperação de ocorrências de entradas na segunda coluna, conhecidas como filhos. Adicionamos o relacionamento ao DataSet:

ds->Relations->Add(doorto);

Dessa forma, em vez de pesquisar explicitamente a tabela doorto procurando pelo identificador explícito, eu utilizo o objeto de DataRelation para fazer a recuperação.

Resumo

Um dos aspectos mais produtivos do .NET é o seu valioso conjunto de ferramentas. Se houver uma restrição de espaço quando o nosso ambiente aumentar, serei mais bem servido pelo suporte do XML ao firehose. Caso contrário, o DOM oferecerá a representação XML mais rica em dados. Nesse caso, eu vou explorar o aspecto relacional do layout do documento ao fazer sua leitura em um DataSet. E essas soluções funcionam em todas as linguagens compatíveis com .NET. Como já disse várias vezes, a questão não é mais sobre as linguagens, mas sobre as bibliotecas. Na próxima coluna, começarei a implementar o rato.

Envie suas dúvidas e seus comentários para Stanley em purecpp@microsoft.com.

Stanley B. Lippman é mais conhecido por seu trabalho realizado com o criador do C++, Bjarne Stroustrup, na Bell Laboratories nos anos 80 e 90, bem como pelo seu primeiro livro, C++ Primer (Addison-Wesley, 2005), agora na quarta edição. Stan também trabalhou como arquiteto na equipe de Visual C++ da Microsoft que criou o C++/CLI, que é o assunto desta coluna.


Assuntos .NET: Monitor de deadlocks

Assuntos .NET
Monitor de deadlocks
Stephen Toub

Download do código disponível em: NETMatters2007_10.exe (156KB)

P Estou usando bloqueios em meu aplicativo para sincronizar o trabalho em vários segmentos. Infelizmente, estou fazendo algo errado e meus segmentos, algumas vezes, simplesmente param de funcionar. Acho que venho me deparando com deadlocks, mas não sei ao certo como encontrá-los. Existe alguma forma de fazer isso por meio de programação? Gostaria que uma exceção fosse emitida quando um deadlock fosse encontrado.

R Em primeiro lugar, é importante entender o que significa um deadlock entre segmentos e que condições levam à sua ocorrência. Os segmentos apresentam um deadlock quando cada um espera que o outro libere alguns recursos; no entanto, ao realizar essa espera de bloqueio, eles não liberam os recursos necessários aos demais segmentos para o desbloqueio. Os segmentos não podem realizar qualquer progresso até que os recursos sejam liberados, mas, como não fazem progressos, os recursos jamais são liberados e os segmentos permanecem bloqueados. Por isso ocorre o "deadlock". Muitos manuais de sistemas operacionais mencionam as quatro condições necessárias para a ocorrência de um deadlock:

  • A existência de um número limitado de um recurso específico. No caso de um monitor em C# (o que você usa quando emprega a palavra-chave lock), esse número limitado é um, pois um monitor é um bloqueio de exclusão mútua (ou seja, somente um segmento pode ser proprietário de um monitor por vez).
  • A capacidade de reter um recurso e solicitar outro. Em C#, isso se assemelha a bloquear um objeto e, então, bloquear outro antes de liberar o primeiro bloqueio. Por exemplo:

lock(a)
{
    lock(b)
    {
        ...
    }
}

  • Incapacidade de preempção. Em C#, isso significa que um segmento não pode forçar outro a liberar um bloqueio.
  • Uma condição de espera circular. Isso significa que há um ciclo de segmentos, cada qual aguardando que o próximo libere um recurso para poder continuar.

Se qualquer uma dessas condições deixar de ocorrer, o deadlock não será possível. A primeira condição é inerente ao próprio monitor. Portanto, se você usa monitores, esse ponto é inalterável. A segunda condição pode ser evitada garantindo-se que somente um objeto seja bloqueado por vez. Contudo, muitas vezes esse requisito é inviável em grandes projetos de software. A terceira condição poderia ser evitada no Microsoft® .NET Framework, anulando ou interrompendo o segmento que retém o recurso necessário ao seu segmento. Contudo, a) isso exigiria saber qual o segmento proprietário do recurso e b) trata-se de uma operação perigosa por natureza (para saber os vários motivos disso, consulte msdn.microsoft.com/msdnmag/issues/05/10/Reliability). Assim, a forma de evitar deadlocks é evitando (ou impedindo) a quarta condição.

Em seu artigo na edição de abril de 2006 (disponível em msdn.microsoft.com/msdnmag/issues/06/04/Deadlocks), Joe Duffy apresenta diversas técnicas para evitar e detectar deadlocks, inclusive uma conhecida como nivelamento de bloqueios. No nivelamento de bloqueios, são atribuídos valores numéricos aos bloqueios e os segmentos precisam apenas adquirir bloqueios com números superiores aos dos bloqueios já adquiridos. Isso impede a possibilidade de um ciclo. Em geral, também é difícil que isso funcione bem em um aplicativo de software típico da atualidade. A falha em seguir o nivelamento de bloqueios em todas as aquisições de bloqueios potencializa o deadlock.

Em vez de impedir totalmente a ocorrência de deadlocks, muitos sistemas se propõem a detectá-los e eliminá-los quando encontrados. O SQL Server®, por exemplo, é capaz de detectar quando ocorrem deadlocks e anular uma das tarefas envolvidas no ciclo, removendo assim o deadlock. Em seu artigo, Joe cria um host CLR (Common Language Runtime) capaz de realizar esse tipo de detecção de deadlocks em aplicativos .NET que usam monitores. É um recurso muito interessante. Infelizmente, nem sempre é prático usar um host CLR personalizado em aplicativos .NET, inclusive naqueles que já têm um host personalizado, como os ASP.NET. Assim, seria vantajoso poder utilizar recursos semelhantes de detecção de deadlocks, mas sem a necessidade de um host CLR personalizado, de forma que esses tipos de deadlocks pudessem ser detectados em runtime. Isso representaria uma vantagem durante as fases de desenvolvimento e teste, para identificar erros de código onde os bloqueios estivessem sendo usados de forma incorreta. O recurso poderia também ser usado na produção, para detectar e eliminar deadlocks conforme ocorressem (os segmentos seriam impedidos de causar deadlocks bloqueando-se a tentativa de espera crítica que completaria um ciclo). No entanto, os algoritmos típicos para detecção de deadlocks são caros e nem sempre adequados para sistemas de produção, por motivos de desempenho (farei alguns comentários sobre desempenho no final da coluna).

Para atender a essa necessidade, criei um exemplo de invólucro para a classe .NET System.Threading.Monitor que inclui recursos de detecção de deadlocks. Assim como Monitor, a minha classe DdMonitor fornece os métodos Enter e Exit e, nos bastidores, faz delegação aos métodos equivalentes em Monitor. Contudo, também controla o uso do monitor e emite exceções quando a tentativa de aquisição de um bloqueio está prestes a completar um ciclo, gerando um deadlock. No resto da coluna, abordarei em detalhes a implementação dessa classe de monitoramento de deadlocks e fornecerei informações adicionais sobre seu uso, e também sobre as vantagens e desvantagens da sua implantação.

A estrutura da classe DdMonitor é mostrada na Figura 1. Logo de início, observe que DdMonitor não imita completamente a interface pública de System.Threading.Monitor. Ela fornece os mesmos métodos estáticos públicos TryEnter, Enter e Exit, mas não os métodos estáticos públicos Wait, Pulse e PulseAll da classe equivalente.

Figure 1 Classe DdMonitor (Close Figure 1)
class DdMonitor
{
    public static IDisposable Lock(object monitor) { ... }

    public static void Enter(object monitor) { ... }

    public static bool TryEnter(object monitor) { ... }
    public static bool TryEnter(
        object monitor, TimeSpan timeout) { ... }
    public static bool TryEnter(
        object monitor, int millisecondsTimeout) { ... }

    public static void Exit(object monitor) { ... }
}

DdMonitor fornece também um método estático público Lock. Como a palavra-chave C# lock representa uma boa abstração em relação a Monitor, é vantajosa a existência de algo semelhante em DdMonitor. Sem a capacidade de modificar a linguagem C#, é impossível obter uma sintaxe idêntica, mas podemos chegar perto disso:

// with Monitor
lock(obj) { ... }

// with DdMonitor
using(DdMonitor.Lock(obj)) { ... }

Esse feito sintático é obtido com a implementação de Lock, conforme mostrado na Figura 2. O método Lock faz delegação para DdMonitor.Enter a fim de adquirir o bloqueio. No entanto, também instancia um objeto DdMonitorCookie, que implementa IDisposable. O método Dispose desse objeto chamará DdMonitor.Exit para liberar o bloqueio; isso permitirá ao desenvolvedor encapsular DdMonitor.Lock em uma instrução C# using (Using em Visual Basic® ou alocação de pilhas em C++/CLI), conforme mostrado anteriormente.

Figure 2 Implementando o método Lock (Close Figure 2)
public static IDisposable Lock(object monitor)
{
    if (monitor == null) throw new ArgumentNullException("monitor");
    IDisposable cookie = new DdMonitorCookie(monitor);
    Enter(monitor);
    return cookie;
}

private class DdMonitorCookie : IDisposable
{
    private object _monitor;

    public DdMonitorCookie(object obj) { _monitor = obj; }

    public void Dispose()
    {
        if (_monitor != null)
        {
            DdMonitor.Exit(_monitor);
            _monitor = null;
        }
    }
}

O método Enter e dois dos três métodos TryEnter são invólucros simples para o terceiro método TryEnter, conforme mostrado na Figura 3. Essas implementações devem seguir a mesma especificação implementada pelos métodos Enter e TryEnter de Monitor. As chamadas para Enter serão bloqueadas até que o bloqueio seja adquirido (o design do nosso método Enter difere ligeiramente, pois uma exceção é emitida se um deadlock é detectado, em vez de um bloqueio infinito), delegando para TryEnter com um tempo limite Timeout.Infinite. Observe que Timeout.Infinite equivale a -1, um valor especial que sinaliza para que TryEnter seja bloqueado até a aquisição do bloqueio, em vez de falhar após um período específico. Por outro lado, a sobrecarga de TryEnter que não aceita um valor de tempo adota como padrão o uso de um tempo limite de 0. Isso significa que retornará false se o bloqueio não puder ser adquirido imediatamente (mais uma vez, nossa implementação também emitirá uma exceção se a aquisição do bloqueio puder causar um deadlock; no entanto, essa decisão é um ponto controverso em TryEnter, então você pode optar por modificar a implementação como for adequado). Observe que TryEnter retornará true se o bloqueio tiver sido adquirido; caso contrário, retornará false. A segunda sobrecarga de TryEnter simplesmente converte o valor de tempo limite TimeSpan fornecido em um número de milissegundos, valida o argumento e faz delegação à sobrecarga final de TryEnter, onde todo o trabalho realmente acontece.

Figure 3 Implementando Enter (Close Figure 3)
public static void Enter(object monitor)
{
    TryEnter(monitor, Timeout.Infinite);
}

public static bool TryEnter(object monitor)
{
    return TryEnter(monitor, 0);
}

public static bool TryEnter(object monitor, TimeSpan timeout)
{
    long totalMilliseconds = (long)timeout.TotalMilliseconds;
    if (totalMilliseconds < -1 ||
        totalMilliseconds > Int32.MaxValue)
            throw new ArgumentOutOfRangeException("timeout");
    return TryEnter(monitor, (int)totalMilliseconds);
}

Nosso design é relativamente simples, embora envolva alguns detalhes interessantes de implementação. A implementação começa validando os parâmetros fornecidos, garantindo que foi fornecido um objeto onde será feito o bloqueio e um tempo limite válido (ou seja, um número -1 ou um número de milissegundos diferente de zero).

Neste ponto, DdMonitor precisa acessar algum estado compartilhado. Como não há suporte a nenhuma forma para que DdMonitor interrogue o CLR e obtenha informações sobre um monitor (inclusive qual segmento pode possuí-lo e quais segmentos podem estar esperando por ele), DdMonitor precisa controlar essas informações manualmente. Isso é feito com o armazenamento de uma tabela estática de estruturas de dados contendo todas as informações relevantes sobre os monitores adquiridos no sistema. Além disso, sempre que uma operação Enter ou Exit é realizada em um monitor, ela atualiza esse estado compartilhado.

Observe que isso tem algumas implicações. Em primeiro lugar, embora DdMonitor acabe fazendo a delegação para Monitor com o mesmo objeto de monitor fornecido pelo usuário, DdMonitor desconhece as chamadas diretas para os métodos de Monitor. Por isso, não é capaz de atualizar suas informações de estado interno com base em chamadas desse tipo. Isso significa que a detecção de deadlocks não funcionará corretamente se você combinar o uso de DdMonitor e Monitor (ou da palavra-chave lock) no seu aplicativo, causando falsos negativos e possivelmente deixando de impedir alguns deadlocks.

Outra implicação dessa implementação é que a tabela estática é específica de AppDomain. Isso não deve representar um problema, uma vez que os objetos nos quais for feito o bloqueio devem residir somente em um AppDomain. Contudo, há vários tipos especiais de objetos (como Type e String) capazes de cruzar os limites de AppDomain e, se você usar DdMonitor para fazer, a partir de vários domínios, o bloqueio em um desses objetos que transitam entre domínios, a tabela estática mantida por DdMonitor em cada domínio conterá somente um subconjunto das informações relevantes. Isso, mais uma vez, poderá gerar falsos negativos.

Um terceiro problema dessa abordagem está relacionado à confiabilidade. Uma instrução lock em C# se expande para uma chamada para Monitor.Enter, seguida imediatamente por um bloco try, que tem o corpo equivalente ao da instrução lock e um bloco finally que libera o monitor. O CLR, usando alguns recursos em JIT, garante que o bloco try seja inserido se a chamada para Monitor.Enter for bem-sucedida; isso garante que o bloco finally também seja executado se Monitor.Enter tiver sucesso. Isso é importante diante das exceções assíncronas que, de outro modo, poderiam ocorrer entre a chamada para Monitor.Enter e a inserção do bloco try (para obter mais informações, consulte msdn.microsoft.com/msdnmag/issues/05/10/Reliability e www.bluebytesoftware.com/blog/2007/01/30/MonitorEnterThreadAbortsAndOrphanedLocks.aspx). No entanto, como você verá na nossa implementação de DdMonitor, pode haver código depois da chamada subjacente real para Monitor.Enter e antes do bloco try que vem depois da chamada para DdMonitor.Enter; assim, essas garantias de confiabilidade se perdem na nossa implementação.

Tenha em mente todos esses aspectos caso algum deles seja perigoso nos seus cenários.

Voltemos à nossa implementação de TryEnter, mostrada na Figura 4; ela continua, obtendo o bloqueio interno para seu estado compartilhado. Depois de adquirido, ele acessa o dicionário para localizar dados anteriores inseridos no monitor. Se esses dados não existirem, isso significa que, no momento, nenhum segmento é o proprietário ou está aguardando o monitor. Nesse caso, TryEnter inicializa um novo objeto MonitorState para controlar esse monitor e o coloca de volta na tabela.

Figure 4 Implementando TryEnter (Close Figure 4)
private static Dictionary<object, MonitorState> _monitorStates =
    new Dictionary<object, MonitorState>();

public static bool TryEnter(object monitor, int millisecondsTimeout)
{
    if (monitor == null) throw new ArgumentNullException("monitor");
    if (millisecondsTimeout < 0 &&
        millisecondsTimeout != Timeout.Infinite)
            throw new ArgumentOutOfRangeException("millisecondsTimeout");

    bool thisThreadOwnsMonitor = false;
    MonitorState ms = null;
    try
    {
        lock (_globalLock)
        {
            if (!_monitorStates.TryGetValue(monitor, out ms))
            {
                _monitorStates[monitor] = ms = new MonitorState(monitor);
            }

            if (ms.OwningThread != Thread.CurrentThread)
            {
                ms.WaitingThreads.Add(Thread.CurrentThread);
                ThrowIfDeadlockDetected(ms);
            }
        }

        thisThreadOwnsMonitor = Monitor.TryEnter(
            monitor, millisecondsTimeout);
    }
    finally
    {
        lock (_globalLock)
        {
            if (ms != null)
            {
                ms.WaitingThreads.Remove(Thread.CurrentThread);

                if (thisThreadOwnsMonitor)
                {
                    if (ms.OwningThread != Thread.CurrentThread)
                        ms.OwningThread = Thread.CurrentThread;
                    else ms.ReentranceCount++;
                }
            }
        }
    }

    return thisThreadOwnsMonitor;
}

private class MonitorState
{
    public MonitorState(object monitor) { MonitorObject = monitor; }
    public object MonitorObject;
    public Thread OwningThread;
    public int ReentranceCount;
    public List<Thread> WaitingThreads = new List<Thread>();
}

MonitorState armazena diversas informações sobre um monitor. Em primeiro lugar, armazena o monitor em si; o sistema o utiliza somente para fornecer informações de diagnóstico caso um deadlock seja detectado (pois é útil para a depuração saber quais monitores estiveram envolvidos em um ciclo de deadlock). MonitorState armazena também o objeto Thread do segmento que for proprietário do monitor naquele momento (esse valor será nulo se o monitor não tiver sido adquirido), uma ReentranceCount que indica quantas vezes o monitor foi inserido (pois o segmento proprietário do monitor poderá reinseri-lo com segurança) e uma lista de segmentos em espera, indicando os segmentos que, no momento, estão tentando adquirir o monitor.

Agora, com um objeto de controle MonitorState à disposição para esse monitor, TryEnter registra no objeto a intenção desse segmento de esperar, adicionando o Thread atual à lista WaitingThreads de MonitorState. Quando a intenção tiver sido registrada, TryEnter chamará o método particular ThrowIfDeadlockDetected, que faz exatamente o que o nome indica (em breve, analisaremos a implementação desse método). Se ThrowIfDeadlockDetected detectar que a aquisição do bloqueio pode causar um ciclo de deadlock, emite uma exceção, impedindo a aquisição do bloqueio. Observe também que, ao fazer isso, o bloco finally no qual a chamada está encapsulada remove o segmento atual da lista de segmentos em espera.

Se nenhum deadlock for detectado, esse segmento poderá tentar adquirir o monitor com segurança e TryEnter simplesmente fará a delegação para que Monitor.TryEnter insira o monitor real. Independentemente de o bloqueio ser adquirido ou não, o bloqueio de estado compartilhado é então adquirido. Observe que eu tenho meu próprio esquema de nivelamento de bloqueios para impedir DdMonitor de causar deadlocks (o que não seria nada bom para uma ferramenta destinada a impedi-los). Não há problema em _globalLock ser adquirido enquanto retém um bloqueio em um monitor fornecido pelo usuário, mas há problema se ocorrer o inverso. É por isso que posso pegar o bloqueio de _globalLock no bloco finally, onde já tenho a possibilidade de ser proprietário do monitor fornecido pelo usuário, mas não posso tentar adquirir um bloqueio no monitor fornecido pelo usuário retendo _globalLock. Preciso primeiro sair do bloqueio em _globalLock para depois chamar Monitor.TryEnter.

Neste ponto, independentemente de o monitor fornecido pelo usuário ter sido ou não adquirido com sucesso, removo o segmento atual da lista de espera do monitor (ou o segmento é proprietário do monitor ou a espera falhou; em qualquer um dos casos, não há mais espera). Então, se o monitor tiver sido adquirido, os campos relevantes serão atualizados em MonitorState.

É mais simples sair do bloqueio, conforme mostrado na Figura 5. Com exceção da validação de argumento, todo o corpo é encapsulado em um bloqueio no monitor _globalLock. O MonitorState para o monitor fornecido pelo usuário é recuperado (se não existir, é porque esse monitor não foi bloqueado e uma exceção foi emitida) e verificado para saber qual o segmento proprietário. Se não houver proprietário ou se o proprietário não for o segmento atual, a chamada para Exit foi errônea e uma exceção será emitida. Caso contrário, se o segmento atual tiver inserido esse monitor várias vezes, a ReentranceCount será simplesmente reduzida; caso isso não tenha ocorrido, o OwningThread será definido como nulo, o que significará a liberação do bloqueio desse segmento no monitor. Além disso, se não houver segmentos aguardando esse monitor, o MonitorState será removido da tabela, de forma que possa ser coletado pelo lixo (se deixássemos de fazer isso, a tabela poderia ter um aumento desmedido ao longo do tempo). Por fim, o método faz delegação para Monitor.Exit para liberar o monitor real.

Figure 5 Implementando Exit (Close Figure 5)
public static void Exit(object monitor)
{
    if (monitor == null) throw new ArgumentNullException("monitor");

    lock (_globalLock)
    {
        MonitorState ms;
        if (!_monitorStates.TryGetValue(monitor, out ms))
            throw new SynchronizationLockException();

        if (ms.OwningThread != Thread.CurrentThread)
        {
            throw new SynchronizationLockException();
        }
        else if (ms.ReentranceCount > 0)
        {
            ms.ReentranceCount--;
        }
        else
        {
            ms.OwningThread = null;
            if (ms.WaitingThreads.Count == 0)
                monitorStates.Remove(monitor);
        }

        Monitor.Exit(monitor);
    }
}

Passemos agora à melhor parte. Já vimos toda a implementação, exceto do algoritmo de detecção de deadlocks propriamente dito. O núcleo dele é implementado em ThrowIfDeadlockDetected (veja a Figura 6), chamado por DdMonitor.Enter, como vimos anteriormente.

Figure 6 Detectando ciclos em cadeias de espera (Close Figure 6)
private static void ThrowIfDeadlockDetected(MonitorState targetMs)
{
    if (targetMs.OwningThread == null) return;

    Dictionary<Thread, List<MonitorState>> locksHeldByThreads;
    Dictionary<MonitorState, List<Thread>> threadsWaitingOnLocks;
    CreateThreadAndLockTables(
        out locksHeldByThreads, out threadsWaitingOnLocks);

    Queue<CycleComponentNode> threadsToFollow =
        new Queue<CycleComponentNode>(locksHeldByThreads.Count);

    threadsToFollow.Enqueue(new CycleComponentNode(
        Thread.CurrentThread, targetMs, null));

    while (threadsToFollow.Count > 0)
    {
        CycleComponentNode currentChain = threadsToFollow.Dequeue();
        Thread currentThread = currentChain.Thread;

        List<MonitorState> locksHeldByThread;
        if (!locksHeldByThreads.TryGetValue(
            currentThread, out locksHeldByThread)) continue;

        foreach (MonitorState ms in locksHeldByThread)
        {
            List<Thread> nextThreads;
            if (!threadsWaitingOnLocks.TryGetValue(
                ms, out nextThreads)) continue;

            foreach (Thread nextThread in nextThreads)
            {
                if (currentChain.ContainsThread(nextThread))
                    throw new SynchronizationLockException(
                        CreateDeadlockDescription(
                            currentChain, locksHeldByThreads));

                threadsToFollow.Enqueue(new CycleComponentNode(
                    nextThread, ms, currentChain));
            }
        }
    }
}

O método ThrowIfDeadlockDetected aceita como parâmetro a instância MonitorState, que representa o monitor prestes a ser aguardado. Para que haja um deadlock, esse monitor (que o segmento atual está prestes a aguardar, com essa intenção já registrada) precisa ser retido por um segmento que retenha direta ou indiretamente esse monitor e que esteja aguardando um monitor retido pelo segmento atual. Com "retenha direta ou indiretamente", quero dizer que adquiriu um bloqueio no monitor ou está aguardando um monitor retido por um segmento que retém direta ou indiretamente esse monitor (sinto muito, mas gosto de remissões). Em outras palavras, estamos procurando um ciclo entre esse monitor, o segmento proprietário dele, um monitor que esse segmento está aguardando, o segmento proprietário do monitor e assim por diante, voltando ao monitor original e ao segmento proprietário dele. Se encontrarmos um ciclo assim, emitiremos uma exceção sinalizando um deadlock.

A implementação segue essa abordagem. Em primeiro lugar, verificamos se o MonitorState de destino tem realmente um segmento como proprietário; se não tiver, não haverá problema em aguardá-lo. Quando geramos tabelas de pesquisa com base em todas as informações que possuímos no momento sobre quais os bloqueios proprietários e aguardando quais monitores (já temos todas essas informações na tabela _monitorState, então simplesmente as refatoramos em duas estruturas de dados Dictionary, que tornarão o restante do algoritmo mais fácil de implementar).

Neste ponto, é preciso começar a examinar os segmentos. Começamos com o segmento atual que, como sabemos, está aguardando o MonitorState fornecido. Para capturar esse emparelhamento, crio um simples contêiner auxiliar CycleComponentNode, que, além de armazenar dados, mantém uma referência ao próximo nó do ciclo identificado. Quando iniciamos o algoritmo, o único nó do qual temos conhecimento é o do segmento inicial e do MonitorState, então o valor Next é nulo.

Até que não haja mais segmentos para serem examinados, o próximo segmento a ser examinado (ou então o CycleComponentNode que armazena esse segmento) será recuperado da fila. Usando as tabelas que criamos anteriormente, recuperamos todos os bloqueios retidos por esse segmento; se ele não retiver nenhum bloqueio, não poderá fazer parte de um deadlock (lembre-se das condições necessárias para um deadlock, especificamente da relacionada a reter um recurso e, ao mesmo tempo, solicitar outro). Cada um desses bloqueios é examinado e, novamente usando as tabelas já criadas, pesquisamos quais segmentos estão aguardando esses bloqueios. Se qualquer um desses segmentos existir na cadeia de espera atual que criamos (representada em forma de lista por CycleComponentNode), isso significa que encontramos nosso deadlock! O método CreateDeadlockDescription enumera todas as instâncias de CycleComponentNode na cadeia e gera uma descrição como esta (que é então emitida em uma exceção):

Thread 24 waiting on lockP (39C82F0)
    while holding lockH (36BEF47), lockK (3B3D5E3)
Thread 30 waiting on lockH (36BEF47)
    while holding lockB (2CF7029), lockN (349C40A
Thread 29 waiting on lockB (2CF7029)
    while holding lockP (39C82F0)

Se o segmento não existir na cadeia de espera atual, ainda assim será preciso examiná-lo. Então, criamos um CycleComponentNode em torno dele e do MonitorState que está aguardando, e seu valor Next é definido como a cadeia de espera atual. Dessa forma, quando viermos a examinar esse segmento e todos os segmentos que aguardam os monitores retidos por ele, poderemos pesquisar adequadamente a cadeia de espera em busca desses segmentos.

Com isso, concluímos a solução. Caso ela não atenda a todas as suas necessidades, logicamente você poderá modificá-la conforme necessário. Por exemplo, ao fazer um bloqueio, é possível armazenar em MonitorState o rastreamento de pilha do segmento atual e, quando for encontrado um deadlock, incluir os rastreamentos de pilha de todos os segmentos relevantes na mensagem da exceção emitida. Ou então, em vez disso, incrementar a solução para dar suporte a primitivos de sincronização adicionais como System.Threading.Mutex.

Observe também que, se você está escrevendo código nativo e fazendo o bloqueio em primitivos de sincronização do Windows, poderá aproveitar as novas APIs Wait Chain Traversal para realizar análises semelhantes e mais avançadas. Para obter mais informações sobre isso, consulte a coluna Exterminador de Bugs, de John Robbins, na edição de julho de 2007 da MSDN® Magazine, disponível em msdn.microsoft.com/msdnmag/issues/07/07/Bugslayer.

Para encerrar, alguns comentários sobre o desempenho dessa implementação. O código de detecção de deadlocks como implementei aqui é dispendioso. Assim, é provável que você queira usá-lo em compilações de depuração, mas não em compilações para venda. Há algumas formas de realizar isso, mas uma das mais simples consiste em incrementar a implementação de DdMonitor com uma propriedade estática:

private static bool _useDeadlockDetection = false;

public static bool UseDeadlockDetection
{
    get { return _useDeadlockDetection; }
    set { _useDeadlockDetection = value; }
}

Assim, é possível incrementar a implementação de Enter, TryEnter e Exit para verificar essa propriedade e usá-la para determinar se você irá implementar o algoritmo completo de detecção de deadlocks ou simplesmente fazer a delegação para os métodos correspondentes em Monitor:

public static void TryEnter(object monitor, int millisecondsTimeout)
{
    if (UseDeadlockDetection)
    {
         ... // existing logic here
    }
    else TryEnter(monitor, millisecondsTimeout);
}

Para definir UseDeadlockDetection no seu código, você pode ler um valor em um arquivo de configuração ou, se quiser definir o valor com base na configuração da compilação, usar um código como este:

public static void Main(string [] args)
{
    DdMonitor.UseDeadlockDetection =
#if DEBUG
        true;
#else
        false;
#endif

    ... // the rest of your app here
}

Definir UseDeadlockDetection como false e usar DdMonitor em toda a base do código não fornecerá comportamento e desempenho idênticos aos obtidos quando se usa apenas Monitor, mas devem ficar bem próximos. Apenas certifique-se de não alterar o valor de UseDeadlockDetection, a não ser no início do programa; se esse valor for alterado em outro lugar, pode ser que você use a detecção de deadlocks para algumas aquisições de monitor e não para outras. Já abordamos os perigos dessa situação. Uma abordagem alternativa sugerida por Joe Duffy consiste em alterar a implementação de TryEnter para, primeiro, evitar o código de detecção de deadlocks e, então, fazer a delegação para Monitor.TryEnter com um tempo limite configurável, digamos, de 500 ms (ou o tempo limite fornecido pelo usuário, se for menor). Se essa chamada for bem-sucedida, não será preciso realizar a detecção de deadlocks, pois o monitor foi adquirido com sucesso. Se a chamada se encerrar e o tempo limite escolhido for menor do que o tempo limite fornecido pelo usuário, então poderá ser executado o algoritmo completo de detecção de deadlocks. Assim, ainda seria possível capturar deadlocks em programas implantados, mas você não precisaria arcar com o custo do algoritmo de detecção até que a primeira aquisição de Monitor fosse encerrada. E, se você escolher um tempo limite razoável, provavelmente esse encerramento ocorrerá mesmo devido a um deadlock.

Envie suas dúvidas e seus comentários para Stephen em netqa@microsoft.com.

Stephen Toub é gerente sênior de programas da equipe da plataforma de computação paralela da Microsoft. É também editor colaborador da MSDN Magazine.


{ End Bracket }: Mashup de casamento

{ End Bracket }
Mashup de casamento
Michael Richter

No meu trabalho como gerente de site da MSDN® Magazine e da TechNet Magazine, costumo ter que optar por um trabalho manual bobo que pode demorar dias ou tentar escrever um aplicativo que faça tudo para mim. É claro que, como escrever esse aplicativo pode demorar dias, a relação tempo£benefício da automação nem sempre é notada. Ainda assim, criar o aplicativo é algo que me mantém feliz e comprometido com o meu trabalho, enquanto tarefas manuais repetitivas me fazem odiá-lo.

Vou me casar em outubro, e decidi aplicar minhas habilidades de desenvolvedor na automação de algumas das tarefas para planejar o casamento – que pode ser igualmente chata (não diga à minha noiva que eu lhe disse isso). Felizmente para mim, ela está cuidando da maioria dos planos, mas ela me pediu para fazer duas coisas: criar o site do casamento e gerenciar a lista de convidados.

(Clique na imagem para diminuir a exibição)

(Clique na imagem para aumentar a exibição)

Em vez de digitar as direções e os links para mapas estáticos, eu criei uma página dinâmica com a API do Virtual Earth™ (consulte msdn.microsoft.com/msdnmag/issues/06/09/EarthlyDelights). Preenchi duas listas suspensas com endereços de um arquivo XML e usei uma lista como ponto de rota inicial e outra como ponto de rota final. Cada controle tem um evento onchange que detecta se ambos pontos de rota estão definidos e envia os endereços para o método GetRoute do meu objeto VEMap. Isso atualiza o mapa e exibe as direções entre os pontos.

Isso funcionou bem quando tracei uma rota entre dois endereços, mas eu também quis contar com a possibilidade de traçar rotas entre coordenadas de longitude e latitude específicas e entre coordenadas e endereços. Bem, aí a minha página simples de automação dos mapas e das direções se tornou complexa rapidamente.

Eu tentei enviar as coordenadas como cadeias de caracteres para GetRoute porque é dessa forma que os endereços são enviados, mas descobri que as coordenadas devem ser enviadas como objetos VELatLong. Acabei colocando um sinalizador no valor das coordenadas na lista para encontrar o JavaScript da criação de um novo VELatLong para o ponto de rota:

<option
   value="821 Stewart Avenue, Garden City, NY">Wingate Hotel</option>
<option value="POSITION,40.729877,-74.038788">Holland Tunnel</option>
<option value="POSITION,40.60294,-74.066305">Verazzano Bridge</option>

Meu código JavaScript é capaz de detectar se o ponto de rota é um conjunto de coordenadas por meio do sinalizador POSITION e de enviar um objeto VELatLong para GetRoute, e não uma cadeia.

Eu também queria ver os endereços de todos os convidados em um mapa. Ajudaria muito ver quais convidados viriam de muito longe e que, por isso, precisariam de um quarto de hotel. Também seria ótimo ver quais deles poderiam formar um comboio. E, claro, seria muito legal ver o mapa em nosso site.

Optei por usar pinos do Virtual Earth, mas eu não tinha as coordenadas de nenhum dos endereços. O objeto VEMap tem um método Find na versão 5 do controle do Virtual Earth para o qual é possível fornecer um endereço, e ele retornará as coordenadas de uma função de chamada de retorno (o nome da chamada de retorno é passado para o método Find como um parâmetro). Isso é muito bom quando você precisa localizar um endereço, mas e se precisa localizar 150? Demorei para perceber que o problema de chamar o método Find 150 vezes é que cada chamada para o método de retorno de chamada substitui a chamada anterior. Dessa forma, não é possível usar o método Find para obter as coordenadas de muitos endereços diferentes em um mapa, pelo menos não em uma seqüência rápida.

Então eu usei o Web service de geocodificação. A Microsoft fornece o Web service MapPoint®, que oferece muitos recursos, inclusive um que faz exatamente aquilo que eu quero (msdn2.microsoft.com/bb545004).

Em vez de criar esse mapa em uma página da Web, optei por colocá-lo no Microsoft® Excel. Inspirado no artigo Geopegging de Josh Trupin (consulte msdn.microsoft.com/msdnmag/issues/07/04/EndBracket), usei as Visual Studio® Tools for Office Second Edition para hospedar um controle do navegador da Web em um painel personalizado do Excel 2007. Quando eu seleciono um grupo de células, o meu suplemento detecta se há algum endereço presente e, caso haja algum, mostra um mapa dos endereços no controle do navegador da Web. Dessa forma, eu posso adicionar endereços diferentes no Excel ou realçar vários endereços e vê-los em um mapa instantaneamente. É possível baixar a solução final no site da MSDN Magazine.

Eu acho que a maioria dos desenvolvedores concordará que é divertido automatizar um trabalho, mesmo que realizá-lo manualmente possa ser mais rápido. Dá mais satisfação ver todo o trabalho sendo feito com um simples botão no aplicativo personalizado. No caso do meu casamento, peguei algumas tarefas possivelmente chatas e me diverti com elas. Isso me deixou feliz, mas também a minha noiva, e é isso o que realmente interessa.

Michael Richter é gerente de site da MSDN Magazine e da TechNet Magazine. Ele mora em Nova York.