Escolar Documentos
Profissional Documentos
Cultura Documentos
Capítulo 4
É mais fácil trabalhar com valores de matiz, saturação e brilho do que com valores de vermelho,
verde e azul para determinar uma estrela antiga.
A função IsOldStar recebe como parâmetro uma instância de Color e retorna o resultado da
aplicação das regras acima para seu matiz, saturação e brilho, conforme as linhas de código
a seguir:
Essa função é chamada para cada pixel no retrato de bitmap infravermelho e retorna um valor
bool .
Neste exemplo, usaremos o método IsAlive para verificar o thread e, em seguida, pausar o thread
principal com o método Sleep por um período de tempo antes de verificar novamente.
Este é um padrão de design comum para coordenar o trabalho entre threads e monitorar
threads para saber quando eles são concluídos.
[ 113 ]
www.it-ebooks.info
Machine Translated by Google
[ 114 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 4
° Um botão mostrando uma estrela e sua propriedade Text definida como Find old
star (butFindOldStars). Este botão iniciará vários tópicos antigos do localizador
de estrelas.
3. Adicione as seguintes linhas na definição de classe do formulário para declarar dois novos
variáveis privadas:
// A lista de threads private
List<Thread> prloThreadList;
// O enorme retrato de bitmap infravermelho original
Bitmap proOriginalBitmap;
// Um sinalizador bool
bool lbContinue = true; int
liDeadThreads = 0; int liThreadNumber;
while (lbContinuar) {
quebrar;
} outro
{
// Aumenta a contagem de threads mortos liDeadThreads+
+;
}
} if (liDeadThreads == priProcessorCount) {
quebrar;
}
[ 115 ]
www.it-ebooks.info
Machine Translated by Google
Thread.Sleep(100);
liDeadThreads = 0;
}
}
int liThreadNumber;
// Cada parte do bitmap
Bitmap loBitmap;
// A linha inicial em cada iteração int liStartRow = 0;
proOriginalBitmap.Height);
Graphics g = Graphics.FromImage((Image)loBitmap); g.InterpolationMode
= System.Drawing.Drawing2D.
InterpolationMode.
Bicúbico de alta qualidade;
}
// Mostra o bitmap na PictureBox picStarsBitmap picStarsBitmap.Image =
loBitmap;
g.Dispose();
}
[ 116 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 4
// Número do
encadeamento int liThreadNumber;
// Cria a lista de threads; a lista longa e a lista de bitmap prloThreadList = new
List<Thread>(priProcessorCount); prliOldStarsCount = new List<long>(priProcessorCount);
prloBitmapList = new List<Bitmap>(priProcessorCount);
int liStartRow = 0;
(ThreadOldStarsFinder)));
[ 117 ]
www.it-ebooks.info
Machine Translated by Google
prloThreadList[liThreadNumber].Start(liThreadNumber);
}
WaitForThreadsToDie();
ShowBitmapWithOldStars();
[ 118 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 4
Como funciona
Quando o usuário clica no botão Old Star Finder , o processo é executado da seguinte maneira:
1. A imagem original é dividida em muitos bitmaps independentes. Cada parte será atribuída a um
segmento diferente.
2. Muitos threads são criados e iniciados (executados de forma assíncrona) com um parâmetro para
que saibam qual bitmap pertence a eles.
3. O thread principal espera até que todos os threads do localizador de estrelas terminem seu
trabalho, dormindo 100 milissegundos em cada consulta do estado dos threads.
4. Assim que todos os threads concluírem seu trabalho, o thread principal (o único capaz de tocar na
interface do usuário) reconstrói o bitmap dividido e o mostra no controle da caixa de imagem.
Cada thread trabalha em seu bloco independente, sem atrapalhar ou interferir nas outras threads.
O código usado para dividir o bitmap original dinamicamente em vários bitmaps menores é um pouco
complexo. Esse é o preço que temos que pagar pelo aprimoramento de desempenho e escalabilidade de
nosso aplicativo.
Esta linha obtém uma instância de Bitmap da caixa de imagem picStarsBitmap (começamos a desacoplar a
interface do usuário, pois não podemos tocá-la de threads independentes):
Essas linhas criam a lista de threads, a lista de números longos e a lista de bitmaps para deixá-los crescer
dinamicamente em tempo de execução, dependendo do número de núcleos disponíveis no computador onde
o aplicativo é executado:
Devemos criar bitmaps correspondentes ao número de núcleos disponíveis. Usamos as linhas para
selecionar um número semelhante de linhas para cada parte do bitmap.
Definimos uma variável liStartRow do tipo int como a linha inicial de onde começaremos a recortar o bitmap
original:
int liStartRow = 0;
[ 119 ]
www.it-ebooks.info
Machine Translated by Google
liEachBitmapHeight = liHeightToAdd;
Se a altura calculada para cada bitmap for maior que a altura a ser adicionada (isso
pode acontecer na última porção de bitmap a ser cortada), reduzimos esse número da
altura a ser adicionada, que é o resultado dessa linha em cada iteração :
liHeightToAdd -= liEachBitmapHeight;
Além disso, a cada iteração, a linha inicial aumenta a altura calculada para cada
bitmap.
A captura de tela a seguir mostra os resultados da aplicação desse algoritmo aos retratos
infravermelhos obtidos pelo Telescópio Espacial Spitzer da NASA com quatro threads:
[ 120 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 4
Usando nossas habilidades clássicas de programação em C#, podemos gerar algoritmos inteligentes
para dividir o trabalho em muitos blocos independentes de código concorrente seguro. Novamente,
dominar as listas é realmente necessário na programação paralela.
Usamos threads com parâmetros, como aprendemos nos capítulos anteriores, e os iniciamos com
uma execução assíncrona usando o seguinte loop:
prloThreadList[liThreadNumber].Start(liThreadNumber);
}
No entanto, devemos esperar até que os threads do localizador de estrelas simultâneos terminem seu
trabalho para mostrar o bitmap modificado final na interface do usuário. Não queremos usar o
componente BackgroundWorker .
Por este motivo, criamos o procedimento WaitForThreadsToDie , que é chamado de forma síncrona
pela thread principal da aplicação. Quando esse método retorna, podemos mostrar com segurança o
bitmap resultante na interface do usuário porque todos os threads concluíram seu trabalho. Claro, para
atingir o mesmo objetivo, também podemos usar o componente BackgroundWorker combinado com as
threads criadas como instâncias da classe Thread , conforme aprendemos nos capítulos anteriores.
Uma vez no loop while (lbContinue), devemos verificar se cada thread termina seu trabalho.
Usamos a conhecida propriedade IsAlive :
[ 121 ]
www.it-ebooks.info
Machine Translated by Google
{
quebrar;
} outro
{
liDeadThreads++;
}
}
Se uma thread estiver ativa, sairemos do loop for . Se todos os encadeamentos não estiverem
ativos, liDeadThreads será igual ao número total de encadeamentos criados. Portanto, sairemos
do loop externo:
if (liDeadThreads == priProcessorCount) {
quebrar;
}
A pausa; a instrução pode ser substituída por lbContinue = false; e teríamos o mesmo
resultado.
Thread.Sleep(100);
liDeadThreads = 0;
Utilizando métodos como estes, e flags, podemos ter total controle sobre threads independentes, sem
causar os clássicos problemas relacionados à concorrência e perda de controle sobre as threads
independentes.
Este método reproduz o trabalho feito ao dividir o bitmap original em várias partes independentes,
mas na ordem inversa.
Repetimos o processo de cálculo da altura explicado anteriormente. Então, devemos criar um novo bitmap
com toda a largura e altura. Este bitmap deve manter as diferentes partes alinhadas conforme foram
extraídas do bitmap original:
[ 122 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 4
Portanto, usamos o construtor Bitmap passando a largura e a altura do bitmap original como
parâmetros para definir seu tamanho. Em seguida, criamos uma instância Graphics do typecast Bitmap
para uma Image (a classe Bitmap é descendente da classe Image ).
Uma vez que temos a instância Graphics , devemos desenhar cada imagem bitmap processada por
cada thread em sua linha correspondente (liStartingRow), que é calculada da mesma forma que
fizemos ao separar as partes do bitmap:
Além disso, a cada iteração, a linha inicial aumenta a altura calculada para cada bitmap. Então,
estamos prontos para mostrar o bitmap reconstruído na caixa de imagem, picStarsBitmap:
picStarsBitmap.Image = loBitmap;
A captura de tela a seguir mostra os resultados da aplicação desse algoritmo aos retratos infravermelhos
obtidos pelo Telescópio Espacial Spitzer da NASA processados por quatro threads:
A captura de tela mostra uma clara independência alcançada por cada thread.
[ 123 ]
www.it-ebooks.info
Machine Translated by Google
// Apenas espere que as threads sinalizem que cada // item de trabalho terminou
WaitHandle.WaitAll(praoAutoResetEventArray);
[ 124 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 4
Como funciona
Quando o usuário clica no botão Old Star Finder :
1. A imagem original é dividida em muitos bitmaps independentes. Cada parte será atribuída
a um segmento diferente.
2. Muitos threads e seus manipuladores de eventos de reinicialização automática são criados para permitir
comunicação entre os fios.
3. Os threads são iniciados (executados de forma assíncrona) com um parâmetro para que
eles sabem qual bitmap lhes pertence.
4. Assim que cada thread termina seu trabalho, ele sinaliza que o trabalho está concluído, definindo o evento
de reinicialização automática.
5. O thread principal espera até que todos os threads do localizador de estrelas terminem seu
trabalho, aguardando todos os sinais necessários dos vários eventos de reinicialização automática.
6. Assim que todos os threads concluírem seu trabalho, o thread principal (o único capaz de
tocar na interface do usuário) reconstrói o bitmap dividido e o mostra no controle da
caixa de imagem.
O código usado para esperar que todas as threads terminem seu trabalho é mais
fácil e elegante.
Usando a classe AutoResetEvent para manipular sinais entre threads Uma instância
[ 125 ]
www.it-ebooks.info
Machine Translated by Google
Primeiro, temos que criar manipuladores de espera de evento correspondentes ao número de threads.
Fazemos isso na seguinte linha, no método FindOldStarsAndShowResult :
Usamos um array porque o método WaitAll recebe um array de handles de espera como
parâmetro.
Assim, cada thread independente pode acessar sua própria instância de AutoResetEvent . Depois
que o thread termina seu trabalho, ele sinaliza que o trabalho foi concluído chamando o método Set ,
conforme mostrado na linha a seguir, no final do procedimento ThreadOldStarsFinder :
praoAutoResetEventArray[liThreadNumber].Set();
Isso acontece quando todos os threads terminaram seu trabalho e chamaram o método Set para sua
instância AutoResetEvent correspondente .
Podemos verificar isso usando uma única linha de código no método WaitForThreadsToDie :
WaitHandle.WaitAll(praoAutoResetEventArray);
O método WaitAll irá monitorar todos os manipuladores de eventos, esperando que seus sinais
mudem (conclusão dos threads). Ele recebe uma matriz de manipuladores de eventos como parâmetro.
[ 126 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 4
Juntando tópicos
Já examinamos a junção de um thread no capítulo anterior. Quando você ingressa em um thread, você
diz ao thread atual para aguardar a conclusão do thread no qual você está ingressando. Isso permite
que você coordene o trabalho entre dois segmentos.
Isso foi muito útil no exemplo em que queríamos saber quando uma peça está completa antes de
iniciarmos a próxima.
Para nosso aplicativo, digamos que queremos examinar como o desempenho muda se todos os threads
de processamento de bitmap forem executados sequencialmente em vez de simultaneamente.
que todos os nossos threads de processamento de bitmap sejam executados sequencialmente, logo
após iniciarmos um thread, iremos ingressar no thread. Isso interromperá a execução do thread principal
até que o thread de processamento de bitmap que acabamos de iniciar seja concluído. Para fazer isso,
altere o loop for inferior no manipulador de eventos butFindOldStars_Click e adicione a seguinte linha:
prloThreadList[liThreadNumber].Join();
[ 127 ]
www.it-ebooks.info
Machine Translated by Google
Como funciona
Agora, execute o aplicativo e clique no botão Old Star Finder . Comente a linha e execute novamente o
aplicativo. O que você vê?
Você notará que o tempo para processar a imagem e concluir o processo é muito mais lento com a linha
extra de código do que sem a linha extra de código. Como mencionamos anteriormente, isso ocorre porque
estamos pausando a thread principal cada vez que executamos uma nova thread para processar parte da
imagem bitmap. Portanto, não estamos processando a imagem simultaneamente, mas sequencialmente.
Para nosso aplicativo, isso não é muito prático, mas para muitos problemas há uma razão pela qual você
pode querer fazer isso. A principal lição aqui é que a classe Thread tem várias maneiras de controlar o
processamento, para que você, o desenvolvedor, possa ter controle total sobre como os threads são
criados, executados e coordenados. É também por isso que isso é chamado de simultaneidade pesada ,
porque exige esforço extra e trabalho do desenvolvedor para realizar o comportamento exato que você
deseja.
Bloqueando recursos para garantir dados thread-safe Até agora optamos por projetar nosso aplicativo
de maneira que não haja necessidade de bloquear recursos para protegê-los de serem "pisados" por outros
bloqueio (objVariável) {
Outros threads que tentam executar uma operação em um objeto que está bloqueado aguardarão até que
o bloqueio seja liberado antes de continuarem sua operação no objeto. Isso é importante observar porque
pode criar problemas de "bloqueio" em que um encadeamento está aguardando um recurso bloqueado por
outro encadeamento. Isso pode ser autodestrutivo ao tentar obter melhorias de desempenho com código
multithreading se os vários threads estiverem constantemente esperando um pelo outro para liberar um
recurso. Esta é uma das razões pelas quais projetamos o código para usar uma lista separada de valores
longos para contar estrelas antigas
com um item na lista para cada segmento em vez de uma variável total que todos os
atualização de tópicos.
[ 128 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 4
Em uma nota separada, ao usar bloqueios, você precisa ter cuidado para não criar uma situação de impasse.
Isso ocorre quando um thread está aguardando um objeto bloqueado por outro thread e esse thread está
aguardando um objeto bloqueado pelo primeiro thread. Portanto, cada thread está esperando na outra
thread e nenhuma delas pode prosseguir.
Também vale a pena observar que os bloqueios são tratados de maneira diferente entre C# 3.0 e C#
4.0. No 3.0, o seguinte é como um bloqueio é convertido em código:
Monitor.Enter(temp);
tentar
// corpo
} finalmente
{
Monitor.Exit(temp);
}
} finalmente
{
if (bloqueado) {
Monitor.Exit(temp);
}
}
Como você pode ver, no C# 4.0, se o bloqueio não for feito em um objeto, nada de diferente
acontecerá com o objeto.
[ 129 ]
www.it-ebooks.info
Machine Translated by Google
2. Além disso, adicione um controle de rótulo e defina a propriedade Text como Old Stars Count e
adicione um controle de caixa de texto e defina a propriedade Name como tbCount.
Essa variável manterá nossa antiga contagem de estrelas e todos os encadeamentos atualizarão essa
variável. Você notará que estamos usando uma string em vez de um inteiro. Isso ocorre porque um número
inteiro é um tipo base da linguagem e não um objeto. Por causa disso, é thread-safe por padrão e não precisa
ser bloqueado. Então, para demonstrar nosso ponto, usaremos um objeto String como contador.
Agora, no método ThreadOldStarsFinder , adicione o seguinte código ao final (e dentro) do loop for
aninhado:
bloqueio (prsOldStarsCount)
{
int i = Convert.ToInt32(prsOldStarsCount); i= i + 1;
prsOldStarsCount = i.ToString();
}
pode ver neste exemplo, cada vez que um dos threads de processamento atualiza a contagem, ele bloqueia
o recurso, atualiza prsOldStarsCount e o desbloqueia.
Isso protege sua integridade, mas retardará o processamento geral devido à troca de contexto extra que o
bloqueio causará.
[ 130 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 4
tentar
[ 131 ]
www.it-ebooks.info
Machine Translated by Google
produtorThread.Start();
consumidorThread.Start();
} catch (ThreadStateException e) {
} catch (ThreadInterruptedException e) {
Environment.ExitCode = resultado;
}
}
Célula celular;
quantidade int = 1;
célula = caixa;
quantidade = pedido;
[ 132 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 4
{
Célula celular;
quantidade int = 1;
célula = caixa;
quantidade = pedido;
{
Monitor.Wait(this); // Aguarda o
Monitorar.Pulso em Gravação
} catch (SynchronizationLockException e) {
System.Diagnostics.Debug.WriteLine(e);
} catch (ThreadInterruptedException e) {
System.Diagnostics.Debug.WriteLine(e);
}
}
System.Diagnostics.Debug.WriteLine(String.
Format("Item de célula consumido {0}", cellContents));
[ 133 ]
www.it-ebooks.info
Machine Translated by Google
if (State) { //
Espera atéque Cell.Read termine o consumo.
tentar
{
Monitor.Wait(this); // Espera pelo Monitor.
Pulso em Leitura.
} catch (SynchronizationLockException e) {
System.Diagnostics.Debug.WriteLine(e);
} catch (ThreadInterruptedException e) {
System.Diagnostics.Debug.WriteLine(e);
}
} CellContents = n;
System.Diagnostics.Debug.WriteLine(String.
Format("Item de célula produzido {0}", cellContents));
estado = verdadeiro; // Define o estado para indicar a produção
é feito.
}
}
}
[ 134 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 4
Neste exemplo, criamos um objeto Cell que possui dois métodos, Read e Write.
Dois outros objetos são criados a partir das classes Produtor e Consumidor . Esses
objetos possuem um método ThreadRun cujo trabalho é chamar Cell.Read e Cell.Write.
A sincronização é feita aguardando "pulsos" do objeto Monitor . Esses pulsos virão na
ordem em que forem recebidos.
[ 135 ]
www.it-ebooks.info
Machine Translated by Google
Então, primeiro um item é produzido (o consumidor neste ponto está esperando por um pulso),
então ocorre um pulso e então o consumidor "consome" o que foi produzido. Enquanto isso, o
produtor está esperando por um pulso. Isso é repetido até atingirmos o limite que definimos
quando criamos os objetos Produtor e Consumidor .
Resumo
Neste capítulo, mergulhamos mais fundo no trabalho com a classe Thread . Através de nossos
exemplos, aprendemos como coordenar threads de uma forma mais avançada. Aprendemos
como usar o bloco try..catch , o objeto Monitor , locks, Join, IsAlive e métodos Sleep para
coordenar atividades entre threads.
Também introduzimos dois padrões de projeto comuns que exploraremos com muito mais
detalhes nos capítulos posteriores — o pipeline e os padrões produtor-consumidor.
Estes são comumente usados para resolver problemas em um aplicativo paralelo. Eles são
boas maneiras de separar o trabalho que pode ser feito em paralelo do trabalho que não pode
ser feito em paralelo.
[ 136 ]
www.it-ebooks.info
Machine Translated by Google
Simultaneidade leve –
Biblioteca Paralela de Tarefas (TPL)
No .NET 4.0, a Microsoft forneceu o que é chamado de Task Parallel Library (TPL) e respondeu às
preocupações dos usuários desenvolvendo aplicativos multithread. O TPL permite que os
desenvolvedores se concentrem na funcionalidade que estão tentando implementar e não fiquem
sobrecarregados com o gerenciamento de vários encadeamentos, o conjunto de encadeamentos e o
número de núcleos de processamento disponíveis para eles.
Até agora, cobrimos o componente BackgroundWorker e a classe Thread para mostrar maneiras
de realizar a funcionalidade multithread em um aplicativo C#/.NET.
Essas duas maneiras de executar a funcionalidade multithread existem desde os primeiros estágios
do .NET. A classe Thread foi introduzida na versão 1.1 do .NET e o BackgroundWorker na versão 2.0
do .NET. Classificamos esses métodos como simultaneidade pesada porque eles exigem bastante
trabalho do desenvolvedor e aumentam a complexidade do design do código. O desenvolvedor deve
gerenciar os diferentes threads e, para obter o desempenho máximo, determinar o número de núcleos
de processamento em uma máquina.
• Ter um entendimento completo da Biblioteca Paralela de Tarefas e das diferentes classes que a
compõem • Entender como criar e usar a classe Tarefa
www.it-ebooks.info
Machine Translated by Google
A ideia era criar uma biblioteca de simultaneidade gerenciada para levar os recursos multithread
do .NET para o próximo nível. TPL consiste em um conjunto de APIs e tipos públicos localizados nos
namespaces System.Threading e System.Threading.Tasks .
Uma das vantagens de usar TPL sobre threads é que o .NET pode dimensionar dinamicamente
um aplicativo para usar com mais eficácia os núcleos de processamento do hardware que está
executando. O .NET é inteligente o suficiente para determinar o número de núcleos de processamento
em uma máquina e gerenciar o ThreadPool adequadamente. Ao programar diretamente com
threads, o desenvolvedor precisa lidar com esse trabalho. O desenvolvedor não precisa mais
determinar o número de núcleos e threads correspondentes criados para atingir o desempenho máximo.
Se você se lembra de nossos exemplos anteriores com threads e componentes BackgroundWorker ,
tivemos que fazer isso no código.
TPL também gerencia o ThreadPool para nós. Ele lida com agendamento de threads, cancelamento de
threads e gerenciamento de estado. Esse ThreadPool gerenciado permite que o .NET tenha um grau
mais alto de inteligência no gerenciamento de tarefas versus threads. A classe Task.Factory pode ser
informada se uma tarefa é de execução longa e não exige muito da CPU em comparação com uma tarefa
que exige muito da CPU. Com essas informações, ele pode ser gerenciado pelo ThreadPool para criar
um único encadeamento por núcleo (tarefas com uso intensivo de CPU) ou vários encadeamentos por
núcleo (tarefas de execução longa que aguardam outros recursos). Essa é a lógica que antes precisava
ser tratada pelo desenvolvedor. Agora o .NET faz isso por você.
[ 138 ]
www.it-ebooks.info
Machine Translated by Google
capítulo 5
Explorando tarefas
A classe Task representa algum trabalho que pode ser feito atomicamente de maneira assíncrona. É um
item de trabalho executado e gerenciado no ThreadPool pelo TPL.
É muito semelhante a um thread, mas com um nível mais alto de abstração e funcionalidade construída
em torno dele. É o controle central da Biblioteca Paralela de Tarefas.
Vamos começar criando e executando uma tarefa. Primeiro, criaremos alguns métodos que representarão
o trabalho a ser feito. Em seguida, executaremos este trabalho usando tarefas.
Há duas maneiras principais de fazer isso: Parallel.Invoke e Task.Factory.
Comece novo. Vamos dar uma olhada em cada um. Começaremos com tarefas que não retornam
um valor. A próxima seção examinará maneiras de executar tarefas que retornam valores.
1. Primeiro, adicionaremos duas instruções using ao arquivo Program.cs para nos permitir
trabalhar com as classes TPL.
2. Em seguida, vamos definir três métodos estáticos diferentes que representarão o trabalho realizado
por três tarefas: WriteNumbers, WriteWords e WriteColors. Um fará um loop pelos primeiros 20
números e gravará cada um no console.
O outro percorrerá uma frase e escreverá cada palavra no console.
O último fará um loop através de uma matriz de cores e gravará cada cor no console. Agora,
adicione os três métodos a seguir ao seu arquivo Program.cs :
static void WriteNumbers()
{
//Configura o nome do thread.
Thread.CurrentThread.Name = "Tópico 1";
[ 139 ]
www.it-ebooks.info
Machine Translated by Google
}
}
CurrentThread.Name, s);
Thread.Sleep(2000);
}
String[] localColors = {"vermelho", "laranja", "azul", "verde", "amarelo", "branco", "preto"}; foreach
(String s em localColors) {
3. Por fim, adicionaremos o código para executar cada método como uma tarefa:
//Criar as 3 Tarefas.
Tarefa t1 = new Tarefa(() => WriteNumbers()); Tarefa t2 = new
Tarefa(() => WriteWords()); Tarefa t3 = new Tarefa(() => WriteColors());
//Execute as 3 Tarefas.
t1.Start(); t2.Start(); t3.Start();
Console.ReadLine();
[ 140 ]
www.it-ebooks.info
Machine Translated by Google
capítulo 5
4. Agora, compile e execute o aplicativo e você deverá ver uma janela de console parecida com
esta:
Este é um exemplo simples, mas permite que você veja as três tarefas executadas em threads
separados, cada uma executando um método diferente. Você pode ver o nome do
encadeamento de cada tarefa e notará que os encadeamentos nem sempre são executados
simultaneamente em sequência.
//Execute as 3 Tarefas. //
t1.Start(); //t2.Start(); //
t3.Start();
[ 141 ]
www.it-ebooks.info
Machine Translated by Google
Parallel.Invoke (
);
Console.ReadLine();
A razão pela qual os resultados podem ser diferentes é porque os três métodos
estão sendo executados em três threads separados em três núcleos separados.
Portanto, dependendo do desempenho e de outros itens executados em seu computador,
as três tarefas podem ser executadas em tempos diferentes, o que pode fazer com que
a saída do console esteja em uma ordem diferente.
Como funciona No
exercício anterior, você aprendeu duas maneiras de usar a classe Task para implementar
a funcionalidade em um thread separado. Esses exemplos usam métodos que não retornam
um valor e instanciar uma classe Task para executar os métodos.
Utilizamos uma expressão lambda para definir o Action delegate, que encapsula um
método a ser executado. Mais adiante neste capítulo, definiremos delegados e lambdas com
mais detalhes.
Primeiro, usamos o método Task.Start() para executar a tarefa. Isso coloca a tarefa no
ThreadPool e permite que o .NET gerencie sua execução. Para instanciar a classe Task ,
usamos uma expressão lambda no construtor.
[ 142 ]
www.it-ebooks.info
Machine Translated by Google
capítulo 5
programa de classe
{
static void Main(string[] args) {
//Criar as 3 Tarefas.
Task<String> t1 = new Task<String>(() => WriteNumbers()); Task<String> t2 = new
Task<String>(() => WriteWords()); Task<String> t3 = new Task<String>(() => WriteColors());
//Execute as 3 Tarefas.
t1.Start(); t2.Start();
t3.Iniciar();
Console.WriteLine(t1.Result);
Console.WriteLine(t2.Result);
Console.WriteLine(t3.Result);
Console.ReadLine();
[ 143 ]
www.it-ebooks.info
Machine Translated by Google
[ 144 ]
www.it-ebooks.info
Machine Translated by Google
capítulo 5
Por fim, vamos construir e executar nosso aplicativo. A janela do seu console deve
ficar assim:
Como funciona
Neste exemplo, usamos a versão Task<TResult> da classe Task , que nos permite
especificar um objeto de retorno da tarefa quando ela tiver concluído a execução. O valor
de retorno será colocado na propriedade Task.Result e será do tipo que você definir na
declaração. Então, na linha de código a seguir, dizemos ao .NET para criar um objeto do
tipo Task que executará o método WriteNumbers() e retornará um valor String :
Isso é muito útil porque nos permite retornar qualquer tipo de objeto.
[ 145 ]
www.it-ebooks.info
Machine Translated by Google
Além disso, em cada método, definimos o nome do thread atual, portanto, em nossa saída,
podemos ver que cada uma das três tarefas opera em um thread diferente.
Em seguida, examinaremos a API em TPL que nos permite usar coleções simultâneas.
Ter um conjunto de coleções thread-safe para serem usadas por muitos threads facilita
o design multithread. Observe como temos menos coisas para contabilizar do que nossos exemplos
anteriores. Antes de dividirmos o trabalho, tínhamos que saber o número de núcleos para os quais
criar um thread. Isso é tratado por Task e ThreadPool agora.
[ 146 ]
www.it-ebooks.info
Machine Translated by Google
capítulo 5
Tivemos que dividir nosso conjunto de dados em partes e dar a cada thread uma parte
conhecida dos dados (ou seja, no exemplo de processamento de imagem, cada thread obteve
uma seção distinta da imagem). Além disso, tivemos que voltar no final e remontar os resultados
de cada thread. Não precisamos mais nos preocupar com essas três preocupações.
Neste projeto, pegaremos uma lista de números de 0 a 5000 (ou seja, 0 + 1 + 2 + 3 + 4 e assim
por diante) e os somaremos em três segmentos diferentes. Não daremos a cada thread um
intervalo para somar e depois adicionar os resultados dos três, como antes. Usaremos apenas
uma coleção ConcurrentQueue com os 5.000 números na fila e faremos com que cada um dos
três encadeamentos remova itens, some-os e adicione a soma ao total geral.
classe ConcurrentCollection
{
static void Main()
{
ConcurrentQueue<int> fila = new ConcurrentQueue<int>();
SingleThreadSum += i;
fila.Enfileirar(i);
}
[ 147 ]
www.it-ebooks.info
Machine Translated by Google
int MultiThreadSum = 0;
Console.ReadLine();
}
}
2. Agora, vamos construir e executar o aplicativo. O que você acha que serão os
resultados? Eles devem se parecer com a seguinte saída:
Como funciona
Neste programa, declaramos um objeto ConcurrentQueue usando a seguinte instrução:
[ 148 ]
www.it-ebooks.info
Machine Translated by Google
capítulo 5
Em seguida, iniciamos três tarefas paralelas que desenfileiram itens da fila sem precisar bloquear a fila
para proteger a segurança do encadeamento porque é uma fila simultânea.
Eles usam o seguinte comando para tirar todos os itens da fila:
Ao usar esse objeto ConcurrentQueue , o .NET lida com todos os problemas de segurança do thread e
permite que todas as três tarefas se concentrem apenas no trabalho a ser executado. todos eles então
adicione suas somas locais ao valor MultiThreadSum . Mas observe que esse valor precisa
para ser bloqueado porque não é thread-safe por padrão, já que três tarefas separadas estão tentando
adicionar a ele em paralelo.
Então, no final, não importa como as três tarefas independentes sejam executadas, o MultiThreadSum
sempre será o mesmo porque cada número entre 1 e 5000 é retirado da fila apenas uma vez e adicionado
à soma total.
A maior parte do nosso trabalho com TaskFactory será abordada no Capítulo 6, Paralelismo
baseado em tarefas, e no Capítulo 7, Paralelismo de dados, e novamente no Capítulo 11, O
modelo de programação assíncrona. Mas neste capítulo, vamos realizar um exemplo simples para
demonstrar como eles são usados.
[ 149 ]
www.it-ebooks.info
Machine Translated by Google
usando Sistema;
usando System.Collections.Generic; usando
System.Linq; usando System.Text; usando
System.Threading; usando System.Threading.Tasks;
namespace TaskFactoryExample {
class TaskFactoryExample {
[ 150 ]
www.it-ebooks.info
Machine Translated by Google
capítulo 5
Console.ReadLine();
}
Agora, vamos compilar e executar este aplicativo. Você deve ver os resultados conforme mostrado
na captura de tela a seguir:
agora examinar o que acabamos de realizar e por que funcionou. Você pode ver na saída que
executamos cinco tarefas em threads diferentes e, em seguida, esperamos que elas fossem concluídas.
Primeiro, criamos uma classe TaskFactory estática para usar:
Existem várias sobrecargas para o construtor TaskFactory . O que usamos apenas pega um objeto
TaskScheduler e escolhemos o padrão. Na próxima seção deste capítulo, examinaremos a classe
TaskScheduler com mais detalhes.
[ 151 ]
www.it-ebooks.info
Machine Translated by Google
Vamos dar uma olhada nos diferentes parâmetros passados para o método StartNew() . Passamos a
ele um token de cancelamento, uma opção de criação de tarefa e um agendador.
Isso permite que muito do gerenciamento de encadeamentos da tarefa seja tratado sem a necessidade
de fazê-lo manualmente.
O token de cancelamento nos permite dizer ao .NET se as tarefas podem ser canceladas ou não.
Também nos permite definir um identificador de espera que é sinalizado se a tarefa for cancelada.
As opções de criação de tarefas permitem as seguintes configurações, que nos dão muito mais
controle sobre a tarefa do que tínhamos com o thread (referenciado em http://msdn.microsoft.com/
en-us/library/vstudio/system.threading .tasks. taskcreationoptions):
Também passamos ao construtor TaskFactory uma expressão lambda para o objeto Action , que
informa o que a tarefa deve executar:
Por fim, executamos um Task.WaitAll na lista de tarefas, portanto, tivemos que aguardar a conclusão de
todas as tarefas. Veremos no próximo capítulo como podemos fazer isso diretamente com o objeto
TaskFactory :
Task.WaitAll(tasklist.ToArray());
[ 152 ]
www.it-ebooks.info
Machine Translated by Google
capítulo 5
Este é um exemplo básico de uso do TaskFactory , mas você pode ver os muitos benefícios que
ele oferece e quanto trabalho é reduzido para os desenvolvedores em comparação com o uso de
encadeamentos diretos.
Um dos melhores recursos do TaskScheduler é que ele é uma classe abstrata da qual você pode
derivar suas próprias classes. O TaskScheduler permite agendar tarefas no ThreadPool exatamente
como você precisa se o TaskScheduler padrão não atender às suas necessidades. Isso lhe dá o
máximo em flexibilidade e controle.
Vamos falar um pouco sobre o ThreadPool. O ThreadPool consiste em uma fila (FIFO) de itens de
trabalho para threads em um domínio de aplicativo. As tarefas são colocadas nessa fila até que um
thread esteja disponível para processá-las. No .NET 4.0, o ThreadPool foi aprimorado para melhorar
o desempenho essencialmente tornando a fila de trabalho um objeto de coleção ConcurrentQueue ,
o que elimina a necessidade da lógica de bloqueio para tornar a fila thread-safe.
Outro ponto a ser observado é que as tarefas que não são filhas de outras tarefas são colocadas em
uma fila global , enquanto as tarefas que são filhas de outras tarefas são colocadas em filas locais
da tarefa pai. Portanto, quando um thread termina de processar um item de trabalho, ele primeiro
procura na fila local da tarefa por mais trabalho antes de ir para a fila global. Essa é outra maneira pela
qual o .NET 4.0 melhorou o desempenho do ThreadPool.
[ 153 ]
www.it-ebooks.info
Machine Translated by Google
[ 154 ]
www.it-ebooks.info
Machine Translated by Google
capítulo 5
Vimos isso brevemente no início deste capítulo, quando usamos o método Parallel.Invoke
para executar um grupo de tarefas em paralelo. Mas seu uso principal é para paralelismo de dados.
// Método nomeado.
Parallel.For(0, 5, Método);
// Método anônimo.
Parallel.For(0, 5, delegate(int i) {
//Expressão lambda.
Parallel.For(0, 5, i => {
Na versão do método nomeado, você precisaria escrever um método chamado Method que
recebesse um parâmetro inteiro e não retornasse nada. O uso de Parallel.For faz com que o .NET
execute cada iteração do loop simultaneamente. Se ele faz isso ou não depende do número de
núcleos de processamento e outros trabalhos acontecendo ao mesmo tempo.
O método Parallel.ForEach pega uma fonte de dados IEnumerable e um delegado Action e itera
por meio da fonte de dados e chama o delegado Action em cada item. Também retorna um objeto
ParallelLoopResult com os resultados do processamento, se houver. A sintaxe básica para isso é a
seguinte:
[ 155 ]
www.it-ebooks.info
Machine Translated by Google
classe SimpleForEach {
);
Console.ReadLine();
}
}
2. Agora, vamos compilar e executar o aplicativo. Seus resultados devem ficar assim:
[ 156 ]
www.it-ebooks.info
Machine Translated by Google
capítulo 5
Como funciona
Primeiro, criamos uma coleção IEnumerable dividindo uma sentença em uma matriz de strings
para cada palavra na sentença. Em seguida, executamos um loop Parallel.ForEach em cada palavra e
simplesmente imprimimos a palavra e o ID do encadeamento atual no console.
Mas o que deve ser observado aqui é que não tivemos que administrar nada disso. Usando
threads diretamente, teríamos que interrogar o hardware e perceber que existem quatro núcleos de
processamento. Em seguida, divida a matriz em quatro matrizes menores e entregue cada uma das
matrizes menores a um único thread para obter desempenho máximo.
Em nosso exemplo, o construtor Task usa um delegado Action para definir a ação a ser executada pela
tarefa. Em nosso exemplo, definimos o método para o delegado na definição do construtor, mas não
precisamos. Podemos escrever assim:
Tarefa de ação1Método;
task1Method = new Action(WriteNumbers);
Tarefa t1 = new Tarefa(task1Method);
[ 157 ]
www.it-ebooks.info
Machine Translated by Google
Tudo o que precisamos saber em tempo de design é que queremos executar um método
sem parâmetros e sem tipo de retorno nesta tarefa. Isso nos dá muito poder e flexibilidade.
Uma expressão lambda é uma função anônima que pode ser usada para criar delegados.
Em uma expressão lambda, existe o operador lambda, =>, e os lados esquerdo e direito desse
operador. O lado esquerdo contém quaisquer parâmetros de entrada e o lado direito contém a
expressão do bloco de código. Parênteses vazios representam parâmetros zero. Vejamos a
seguinte afirmação:
Nesta instrução, estamos usando uma expressão lambda para representar o delegado que o
O construtor da tarefa recebe como entrada: () => WriteNumbers()
Essa expressão lambda está nos dizendo que o delegado não tem parâmetros de entrada
(()) e que o bloco de código para o método do delegado é WriteNumbers().
Portanto, podemos ver que nos exemplos anteriores, o construtor Task usa um tipo de
referência delegado e usamos uma expressão lambda para definir esse delegado. Ao fazer isso,
temos flexibilidade para alterar em tempo de execução qual método uma tarefa executará
quando for executada. A única restrição em tempo de compilação são os parâmetros passados
para o delegado e o tipo de retorno do delegado.
Resumo
Neste capítulo, iniciamos nossa jornada na Task Parallel Library e esse será o foco do
restante do livro. Você aprendeu sobre as classes Task, Action, TaskFactory, Parallel e
TaskScheduler .
Os exemplos de código neste capítulo foram muito simples, mas projetados para fazer
você começar a pensar a partir de uma mentalidade TPL e fora da mentalidade Thread e
BackgroundWorker . No restante deste livro, exploraremos muitos recursos mais detalhados
das classes TPL e vários padrões de projeto paralelo comuns e como eles são implementados
usando TPL.
[ 158 ]
www.it-ebooks.info
Machine Translated by Google
capítulo 5
Você já deve ser capaz de ver o quão poderoso é o TPL e quanta complexidade para fazer um
aplicativo projetado para simultaneidade ele lida. Ao criar um aplicativo multithread, geralmente há
quatro considerações que precisam ser tratadas pelo desenvolvedor que não fazem parte de um
aplicativo single-threaded; eles estão listados a seguir:
• Como garanto segurança de thread nos dados e valores que se sobrepõem entre
tópicos? Ou dividir os dados e valores para não se sobrepor? •
Embora tenhamos apenas começado a discutir o TPL, você já pode ver como os três últimos são
tratados para você, ao contrário da programação simultânea com threads diretamente.
A primeira é uma decisão de projeto e, à medida que discutimos padrões de projeto paralelos
comuns, você verá que o TPL também nos ajuda nisso.
Agora, vamos passar para o Capítulo 6, Paralelismo baseado em tarefas, e começar a realmente
se sentir confortável desenvolvendo software usando o TPL.
[ 159 ]
www.it-ebooks.info
Machine Translated by Google
www.it-ebooks.info
Machine Translated by Google
No Capítulo 5, Simultaneidade Leve – Biblioteca Paralela de Tarefas (TPL), começamos nossa exploração da
simultaneidade leve e da nova maneira preferida do .NET de realizar programação multithread - a TPL. Neste
capítulo, exploraremos mais o paralelismo baseado em tarefas e mostraremos como aguardar tarefas ou várias
tarefas, agendamento personalizado de uma tarefa, tratamento de erros e cancelamento de uma tarefa.
O TPL nos oferece uma maneira fácil de agendar, executar e coordenar tarefas em um nível de abstração mais alto do
que trabalhar diretamente com threads. Como mencionado anteriormente, uma tarefa é um conjunto de instruções para
realizar um objetivo, uma unidade de trabalho se desejar. Podemos definir uma tarefa em um método chamado por um
delegado ou em uma expressão lambda definida diretamente em
o comando de criação de tarefa.
Ao executar mais de uma tarefa simultaneamente, haverá momentos em que precisaremos aguardar a conclusão
de uma ou mais tarefas antes de executar uma função, ou cancelar uma ou mais tarefas se surgir uma determinada
condição, ou coordenar o tratamento de exceções se houver ou mais tarefas gera um erro ao executar em paralelo.
Discutiremos todos esses cenários neste capítulo e trabalharemos com exemplos. No final deste capítulo, você
estará muito confortável com todos os aspectos do uso de tarefas em paralelo em seus aplicativos.
• Aguardar uma tarefa específica quando várias tarefas estiverem sendo executadas simultaneamente •
www.it-ebooks.info
Machine Translated by Google
Faremos isso usando uma tarefa que não retorna um resultado e outra que retorna, e
veremos a diferença entre Task e Task<TResult>. Quando uma tarefa retorna um valor de
resultado, a espera é implícita e, quando não, precisamos esperar explicitamente por ela.
usando Sistema;
usando System.Threading; usando
System.Threading.Tasks;
{
// Espera em uma única tarefa sem tempo limite.
Tarefa taskA = Task.Factory.StartNew(() => Worker(10000));
tarefaA.Wait();
Console.WriteLine("Tarefa A concluída.");
if (tarefaB.IsCompleted)
Console.WriteLine("Tarefa B Concluída.");
outro
Console.WriteLine("Tempo esgotado sem terminar a Tarefa B.");
Console.ReadLine(); }
[ 162 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 6
Como você pode ver neste código, estamos criando uma tarefa para ser executada por 10 segundos e
vamos esperar a tarefa terminar. Esta tarefa chama um método chamado Worker e é
instanciado usando uma Task.Factory e uma expressão lambda. A tarefa não retorna um valor.
Agora, vamos compilar e executar nosso novo aplicativo. Veja a seguir um exemplo da saída que você deve
ver:
Como você pode ver, primeiro iniciamos uma tarefa com o método Worker e esperamos que ela seja
concluída e, em seguida, iniciamos uma tarefa com o método Worker e realizamos uma espera com
um valor de tempo limite. Você notou alguma diferença entre as duas execuções?
Na primeira vez que executarmos a tarefa, esperaremos os 10 segundos completos para que a
tarefa seja concluída. Na segunda vez que executamos a tarefa, definimos um tempo limite e continuamos
antes que a tarefa seja concluída. Como a tarefa é executada por mais de 2 segundos, o comando de espera
expira e o programa continua, enquanto a tarefa ainda está em execução.
Em ambos os exemplos, temos uma tarefa que não retorna um valor. Agora, vamos mudar nossa
tarefa de trabalho para retornar um valor e ver o que acontece. Vamos adicionar um novo método ao
nosso programa chamado Worker1. Adicione o seguinte código ao método Worker1 :
[ 163 ]
www.it-ebooks.info
Machine Translated by Google
Agora, no método Main do arquivo Program.cs , adicione as duas linhas de código a seguir no final,
antes do comando Console.ReadLine :
Agora, vamos compilar e executar nosso programa novamente. A seguir está a saída que você deve
ver ao executar o programa:
Você notou que o programa foi executado e esperou que o TaskC fosse concluído? Em breve,
chegaremos ao método SpinWait da classe Thread , mas por enquanto, por que o programa esperou
que TaskC terminasse de executar quando não colocamos um comando de espera no código para
isso?
anterior, vimos três maneiras de esperar a conclusão de uma tarefa. Os dois primeiros exemplos
usaram o método Task.Wait para dizer ao nosso programa para aguardar a tarefa.
Como a tarefa está sendo executada em um thread diferente, temos que esperar que ela seja
concluída antes de continuarmos a execução do programa em nosso thread principal. Nesses dois
exemplos, nossa tarefa não retorna um valor. Nesse caso, precisamos esperar explicitamente que
a tarefa seja concluída se não quisermos que nossa thread principal continue depois de iniciar a tarefa
(ou pelo menos agendá-la no threadpool). Fazemos isso usando o método Task.Wait na classe Task .
Na primeira vez, dissemos ao nosso programa para aguardar a conclusão da tarefa e depois seguir
em frente. Na segunda vez, definimos um valor de tempo limite no método Wait . Isso diz ao nosso
thread principal para aguardar a quantidade de tempo no parâmetro de tempo limite e, em seguida,
seguir em frente. Em seguida, verificamos a propriedade Task.IsComplete para ver se nossa tarefa foi
concluída ou não.
[ 164 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 6
Como a tarefa foi definida para ser executada por mais tempo do que o valor de tempo limite, a
tarefa ainda estava em execução quando nosso thread principal continuou a execução. É por isso que
a propriedade IsComplete é falsa e a parte else de nossa instrução if é executada:
Você pode brincar com esse código e fazer com que a tarefa seja executada por menos tempo do
que o valor do tempo limite e ver o que acontece.
Em seguida, executamos uma tarefa usando o método Worker1 que retorna um valor duplo . O que vimos
aqui? Não colocamos um comando Task.Wait , mas nosso programa principal ainda esperou a conclusão
da tarefa antes de continuar com sua execução. Por que é que?
Sabemos que ele esperou até que a tarefa fosse concluída porque, quando escrevemos o arquivo Task.
Valor do resultado para o console, ele tinha um valor e não era nulo. Nesse caso, não precisamos
usar o método Task.Wait explicitamente porque o thread principal aguardará a conclusão da tarefa; ele
precisa esperar até que a propriedade Task.Result seja definida. Ao definir uma tarefa com um valor de
retorno, estamos dizendo ao nosso thread principal implicitamente para aguardar a conclusão da tarefa.
Então, como vimos, se não retornarmos um valor em nossa tarefa, precisamos dizer
explicitamente ao nosso programa para esperar. No entanto, se retornarmos um valor, estamos dizendo
implicitamente ao nosso programa para aguardar a conclusão da tarefa.
A seguir, veremos como controlar a execução de nossa thread principal quando iniciamos várias
tarefas simultâneas de uma só vez. Nesse caso, se nossas tarefas retornarem valores, talvez não
desejemos implicitamente aguardar a conclusão de cada tarefa. Talvez só queiramos esperar que uma
tarefa seja concluída.
Em nosso programa atual, usamos o método Thread.SpinWait . Este método é diferente do método
Thread.Sleep . O método Thread.Sleep espera por um tempo especificado enquanto o método
SpinWait inicia um loop para as iterações especificadas passadas para o método. Portanto, nesse caso, a
velocidade do processador ditará a duração porque a velocidade do clock determinará a rapidez com que
ele pode concluir o número de iterações. Ao usar isso em vez de um método Sleep , ele também permite
que o escalonador execute a troca de contexto se as prioridades do thread exigirem.
[ 165 ]
www.it-ebooks.info
Machine Translated by Google
Às vezes, podemos querer esperar que uma tarefa de um grupo seja concluída. Às vezes, podemos
esperar que todas as tarefas sejam concluídas e, outras vezes, podemos esperar que uma determinada
condição de tarefas seja concluída antes de prosseguir.
A captura de tela a seguir mostra todos os métodos Wait e When fornecidos com a classe Task . Os
métodos Wait são como mostrados:
[ 166 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 6
Demonstraremos como executar diferentes técnicas de Espera e Quando neste capítulo, mas isso
é para lhe dar mais ideias sobre o que é possível.
Vamos começar criando um programa simples para demonstrar como o método WaitAll funciona,
preenchendo três círculos com números aleatórios após a conclusão de três tarefas.
precisamos criar um aplicativo WPF usando o Visual Studio. Abra o Visual Studio e crie um novo projeto
WPF chamado RandomCircles. Depois de criarmos este projeto, prossiga executando as seguintes
etapas:
° Em seguida, adicione três caixas de texto e coloque-as dentro dos círculos. Nome
eles text1, text2 e text3. Defina a propriedade Text em todos os três como uma
string vazia e torne sua propriedade Background da mesma cor que a propriedade
Fill nos controles Ellipse.
° Em seguida, adicione um controle de botão e nomeie-o como btnRandomAll.
Defina a propriedade Content neste controle como Random All.
[ 167 ]
www.it-ebooks.info
Machine Translated by Google
Agora, seu arquivo MainWindow.xaml deve se parecer com a captura de tela a seguir:
2. Em seguida, vamos para o código MainWindow.xaml.cs por trás desse arquivo. Precisamos adicionar uma
instrução using para o namespace Threading . usando System.Threading;
3. Agora, vamos adicionar um método de trabalho para fornecer trabalho para as tarefas a serem executadas,
que estamos prestes a criar. Crie um método Worker1 que seja privado e retorne um valor de string.
Adicione o seguinte código a ele: private String Worker1()
[ 168 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 6
Isso deve ser tudo o que você precisa para ter um programa que execute três tarefas em threads
separados. Cada uma dessas tarefas girará por um tempo aleatório. O thread principal aguardará a
conclusão de todas essas tarefas e, em seguida, retornará uma string com o número aleatório usado pela
tarefa e a hora atual em milissegundos.
Finalmente, vamos compilar e executar nosso novo programa. A seguir está a aparência dos
círculos aleatórios quando iniciados:
[ 169 ]
www.it-ebooks.info
Machine Translated by Google
Agora, clique no botão Random All uma vez e veja como é a saída.
O seguinte é o que obtivemos quando o executamos:
Agora, clique no botão Random All uma segunda vez e veja como o programa se parece. A captura
de tela a seguir é o que obtivemos:
[ 170 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 6
Agora, vamos fazer algumas alterações em nosso programa e tentar executá-lo novamente. Faça as
seguintes alterações:
2. Adicione um novo controle de caixa de texto e nomeie-o como text4 e defina a propriedade
Text como uma string vazia.
3. Adicione outro controle de botão e nomeie-o como btnRandomFirst e defina o
Propriedade de conteúdo para RandomFirst.
4. Adicione um manipulador de eventos ao botão, btnRandomFirst, e nomeie-o
btnRandomFirst_Click. Adicione o seguinte código ao manipulador de eventos: private void
btnRandomFirst_Click(object sender, RoutedEventArgs e)
{
Task<String>[] tarefas = newTask<String>[3]; for (int i = 0; i< 3; i+
+) {
Agora, vamos compilar e executar nosso programa. A seguir está a saída antes de um
ação é tomada:
[ 171 ]
www.it-ebooks.info
Machine Translated by Google
Agora, clique no botão Random First uma vez e a saída deve se parecer com a seguinte
captura de tela:
Finalmente, vamos clicar no botão Random First uma segunda vez e ver os resultados.
Eles devem se parecer com a captura de tela a seguir:
[ 172 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 6
Como funciona
Vamos dedicar um minuto para analisar o que acabamos de fazer com este programa.
Primeiro, nosso método Worker1 que executa nossas tarefas é uma abelha operária simples.
Ele gera um número aleatório e, em seguida, executa um Thread.SpinWait para esse número de
iterações. Em seguida, ele retorna a hora atual em milissegundos. Portanto, toda vez que esse
método for executado como uma tarefa em um thread separado, ele será executado por um período de tempo aleatório.
Se várias cópias desse método estiverem em execução em diferentes threads, cada uma delas
terminará em um momento diferente porque SpinWait será executado para um número diferente de
iterações geradas aleatoriamente.
2. Criamos um array de tarefas com um valor de string TResult usando este comando:
Task<String>[] tarefas = newTask<String>[3];
4. Em seguida, exibimos os resultados de cada tarefa e vemos que cada uma tem um
números aleatórios diferentes e terminam em horários diferentes, mesmo que cada tarefa
esteja executando o mesmo método.
5. Em seguida, criamos um segundo botão que faz muito do mesmo trabalho, exceto
desta vez, em vez do comando Task.WaitAll , usamos este comando:
Portanto, neste caso, esperamos apenas que qualquer uma das três tarefas seja concluída antes de
continuar a execução em nosso thread principal. Você pode verificar isso clicando várias vezes no botão
Random First . Cada vez pode ser a tarefa 0, 1 ou 2 que termina primeiro.
Reserve algum tempo para brincar com esses dois comandos e certifique-se de ter um entendimento
completo de como cada um funciona.
Não gastaremos tempo neste livro brincando com os métodos Continue, ContinueAll, When e
WhenAll da classe Task , mas usaremos algum tempo para experimentar essas funcionalidades.
Esses métodos permitem que você execute uma tarefa quando outras tarefas forem concluídas.
Assim, você pode encadear as execuções de tarefas. A alternativa para isso é aguardar a
conclusão das tarefas e, em seguida, iniciar novas tarefas. Os métodos Continue e When
permitem que você combine isso em uma instrução.
[ 173 ]
www.it-ebooks.info
Machine Translated by Google
Existem duas diferenças principais para esses dois métodos de cancelamento. Se o delegado
apenas retornar da execução, acontecerá o seguinte:
[ 174 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 6
2. O código solicitante pode verificar esse status e determinar se a tarefa foi cancelada ou
acabou de ser concluída.
Então, agora vamos criar um aplicativo WPF para permitir que o usuário cancele tarefas. Demonstraremos
como criar tokens de cancelamento, monitorar o estado da tarefa para ver se ela foi cancelada e fazer
algum processamento quando ela for cancelada. Isso também usará o método Task.ContinueWith . Este
método nos permite executar uma tarefa quando outra tarefa for concluída.
Em nosso aplicativo, iniciaremos 10 tarefas. Cada tarefa receberá um parâmetro inteiro como entrada e o
multiplicará pelos números de 1 a 10 milhões e obterá uma soma dos resultados. Em seguida, retornará um
valor duplo que é o resultado. Embora este possa não ser o cálculo mais empolgante do mundo, levará um
pouco de tempo, dependendo da velocidade do processador.
Iniciaremos cada tarefa com um passe de token de cancelamento de tarefa para ela e
criaremos tarefas ContinueWith para ela. Um continuará se a tarefa for concluída e o outro continuará se a
tarefa for cancelada. Também teremos um botão Cancelar que permite cancelar a qualquer momento e
interromper as tarefas em andamento.
Vamos começar.
[ 175 ]
www.it-ebooks.info
Machine Translated by Google
Agora, vá para o código MainWindow.xaml.cs atrás do arquivo. Vamos adicionar os seguintes itens:
2. Em seguida, dentro da definição de classe no topo, vamos adicionar nosso token de cancelamento:
CancellationTokenSourcets;
3. Em seguida, vamos adicionar um método que será executado dentro de nossas 10 tarefas. vamos ligar
este método AddMultiple. Ele recebe um parâmetro inteiro e retorna um valor que é o tipo de dados
double. Adicione o seguinte código a ele: public double AddMultiple(int number) { double result = 1; for
(int i = 1; i< 100000000; i++)
{
ts.Token.ThrowIfCancellationRequested(); resultado =
resultado + (número * i);
}
resultado de retorno;
}
[ 176 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 6
4. Em seguida, vamos adicionar o código ao manipulador de eventos de clique do btnStart que iniciará
as 10 tarefas:
{ ts = new CancellationTokenSource();
textBlock1.Text = "";
tarefas.Adicionar(adicionador);
{ ts.Cancelar(); }
[ 177 ]
www.it-ebooks.info
Machine Translated by Google
Isso é tudo o que temos que fazer. Agora, vamos compilar e executar nosso aplicativo. Mas
desta vez, não vamos executá-lo no Visual Studio. Vamos executar o aplicativo diretamente. Vá
para a pasta bin\Release em nosso diretório de projeto e clique duas vezes no arquivo
TaskCancel. aplicativo exe . Explicaremos brevemente porque estamos fazendo isso e o que
acontece quando o executamos no Visual Studio.
Agora, clique no botão Iniciar . Em seguida, clique no botão Cancelar . Você pode decidir
com que rapidez clicar no botão Cancelar , mas deverá ver uma saída como a seguinte
captura de tela:
Se você tentar executá-lo diretamente no Visual Studio, notará que, ao clicar no botão
Cancelar , ocorre uma exceção não tratada. Você pode pressionar F5 algumas vezes e ele
continuará. Mas por que isso acontece?
[ 178 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 6
cancelamos com sucesso as tarefas em execução, vamos ver como fizemos isso. Primeiro,
definimos um token de cancelamento usando CancellationTokenSourcets. Em seguida,
instanciamos usando ts = new CancellationTokenSource(). Isso nos dá um token de cancelamento
com o qual podemos trabalhar.
Vamos pular, brevemente, para o final. Para cancelar tarefas, basta chamar o método Cancel do
token, ts.Cancel(), no manipulador de eventos de clique do botão Cancelar . Isso indicará a qualquer
tarefa que estiver usando esse token de cancelamento que ela precisa cancelar. Agora cabe às tarefas
monitorar o token e realizar o cancelamento.
Então, vamos dar uma olhada no método de clique do botão Iniciar . Aqui, criamos e iniciamos 10
tarefas usando o seguinte código:
Aqui, chamamos o comando Task.Factory.StartNew e passamos a ele o delegado para a tarefa a ser
executada e o token de cancelamento.
Em seguida, também chamamos o comando ContinueWith para informar à tarefa o que fazer
após sua conclusão. Configuramos dois deles, dependendo de como a tarefa termina:
[ 179 ]
www.it-ebooks.info
Machine Translated by Google
TaskContinuationOptions.OnlyOnCanceled, TaskScheduler.
FromCurrentSynchronizationContext());
Na saída, você pode ver quais tarefas são executadas até a conclusão e quais tarefas são
canceladas. Além disso, observe que a saída não está em ordem sequencial. Como as 10 tarefas
são agendadas com o threadpool e os diferentes núcleos da máquina que executamos têm cargas
de trabalho diferentes, as tarefas são concluídas em ordens diferentes.
Uma tarefa tem um ciclo de vida de estados em que pode estar. Uma tarefa pode estar
ociosa (agendada), em execução, pendente, cancelada, com falha ou concluída. Cada
tarefa tem uma propriedade de status que define o estado atual da tarefa. Vimos isso na última
seção e usamos OnlyOnCanceled e OnlyOnRanToCompletion como opções para
TaskContinuationOptions para nossos métodos ContinueWith .
Portanto, quando falamos sobre tratamento de exceções com TPL, estamos nos referindo a
tarefas com falha. Há várias maneiras de uma tarefa atingir um status de falha, por exemplo,
quando o delegado da tarefa falha e lança uma exceção. Uma tarefa com falha é basicamente
uma tarefa que resultou em um erro e carrega a exceção com ela.
[ 180 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 6
Seu código incorrerá em uma exceção se você executar uma das seguintes ações em uma
tarefa com falha:
Portanto, ao usar os métodos Wait, WaitAll, Await ou Result de uma tarefa, você deve cercar
essas instruções com um bloco try..catch . Os dois primeiros sempre lançarão uma exceção do
tipo AggregateException , que é um contêiner para várias exceções. O Await lançará a exceção real
ocorrida pelo delegado da tarefa.
Esses são os princípios básicos do tratamento de exceções de tarefa. Queremos fazer um try..catch em
torno do Result, Wait ou Await de uma tarefa.
Mas e se iniciarmos uma tarefa e não nos preocuparmos em esperar por ela ou seus resultados,
mas quisermos saber se incorre em uma exceção? Como sabemos se uma tarefa de iniciar e
esquecer tem um problema? Neste caso, podemos utilizar uma técnica simples com a ajuda do comando
ContinueWith . Observe o seguinte comando:
Nesse caso, criamos uma tarefa para executar em um comando ContinueWith se a tarefa estiver em um
estado OnlyOnFaulted .
Portanto, você pode executar o tratamento de exceção com um try..catch no resultado enquanto
aguarda uma tarefa ou, se não estiver aguardando a tarefa ou o resultado, pode criar uma tarefa
ContinueWith para tratar a exceção.
Resumo
Neste capítulo, examinamos maneiras de aguardar uma tarefa, cancelá-la, encadeá-la e executar o
tratamento de exceções com tarefas. Agora abordamos todos os aspectos que precisam trabalhar com
tarefas no TPL e as técnicas necessárias para lidar com todos os cenários que seu projeto pode encontrar.
Incentivamos você a explorar ainda mais as técnicas apresentadas neste capítulo e explorar o mundo
do paralelismo baseado em tarefas. Os dias de trabalho direto com threads e o componente
BackgroundWorker logo se tornarão uma memória distante.
Esses dois últimos capítulos foram uma visão geral do TPL e do paralelismo baseado em tarefas.
No próximo capítulo, abordaremos o paralelismo baseado em dados e o trabalho com dados
coleções de maneira simultânea.
[ 181 ]
www.it-ebooks.info
Machine Translated by Google
www.it-ebooks.info
Machine Translated by Google
Paralelismo de Dados
A execução simultânea de uma tarefa ou um conjunto de operações em uma coleção de dados é
chamada de paralelismo de dados. Por exemplo, se temos uma lista de arquivos em uma pasta e
queremos renomear todos, podemos criar um loop For que percorra a coleção e, a cada iteração,
o loop executa um comando de renomeação. Também podemos iterar por meio de um tipo de
dados de coleção, como List ou DataView, usando uma instrução foreach . Essas são instruções
For e ForEach especializadas que fazem parte da Task Parallel Library (TPL) no namespace
System.Threading.Tasks.Parallel .
Neste capítulo, aprenderemos como processar itens de uma fonte de dados em paralelo usando
os métodos Parallel.For e Parallel.ForEach . Também examinaremos a classe
ParallelLoopState , que nos permite examinar os resultados de um loop simultâneo e executar
ações com os resultados. Por fim, aprenderemos como cancelar um loop concorrente antes
que ele seja concluído. Neste capítulo, abordaremos:
www.it-ebooks.info
Machine Translated by Google
Paralelismo de Dados
// Método nomeado.
Parallel.For(0, i, DoWork);
// Método anônimo
Parallel.For(0, i, delegate(int j) {
// Trabalhe.
});
// Expressão lambda.
Parallel.For(0, i, j => {
// Faça o trabalho
});
Em cada exemplo, o método ou a expressão lambda usa um único parâmetro que é o valor da iteração.
Se você precisar de mais controle sobre a execução do loop simultâneo, existem métodos de sobrecarga
que usam um parâmetro ParallelLoopState gerado internamente pelo .NET. Falaremos sobre isso mais
adiante neste capítulo, mas ele nos permite fazer coisas como cancelar um loop paralelo ou executar
uma ação para cada iteração do loop depois de concluído.
[ 184 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 7
Além disso, assim como um parâmetro ParallelLoopState , algumas sobrecargas permitem que o método
retorne uma variável local segura para thread.
Agora, vamos dar uma olhada em um exemplo. Construiremos um aplicativo Windows Presentation
Foundation (WPF) simples que recebe uma matriz de números inteiros, executa um cálculo sobre eles
e, em seguida, atualiza o item na matriz. Isso será feito simultaneamente em vez de sequencialmente.
[ 185 ]
www.it-ebooks.info
Machine Translated by Google
Paralelismo de Dados
Parallel.For(0, 9, CalculateNumbers);
tb10.Text = números[9].ToString();
int j = numeros[i];
[ 186 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 7
j *= k;
}
números[i] = j;
Isso deve ser tudo. Agora, vamos executar nosso aplicativo e ver o que acontece. Lembre-
se de que não inserimos nenhum tratamento de erro. O aplicativo espera um número e
apenas um número em cada caixa de texto quando o botão Calcular é clicado. Se não
estiver lá, o aplicativo lançará uma exceção de argumento fora do intervalo.
[ 187 ]
www.it-ebooks.info
Machine Translated by Google
Paralelismo de Dados
[ 188 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 7
Como funciona
No exercício anterior, inserimos 10 números em 10 caixas de texto e depois clicamos em
Calcular. O programa então pegou cada número, multiplicou-o pelos números de 1 a 10 e
somou-os. Em seguida, ele colocou o resultado de volta na caixa de texto
Veio de.
Isso tudo foi feito simultaneamente. Cada caixa de texto foi processada em paralelo.
Isso pode ter ocorrido em 10 threads separados ou menos, dependendo do hardware em
que executamos o programa. Ao contrário de usar threads diretamente usando a classe
Parallel e TPL, o .NET gerencia o pool de threads e maximiza quantos threads executar a
operação simultânea, usando os núcleos de processamento disponíveis na máquina.
Existem dois parâmetros na versão mais básica deste método. Há uma coleta de
dados e um delegado Action para executar uma tarefa em um item do
dataCollection. O delegado Action usa um único parâmetro que é um item na coleção.
[ 189 ]
www.it-ebooks.info
Machine Translated by Google
Paralelismo de Dados
Referência—http://msdn.microsoft.com/en-us/library/system.threading.tasks.parallel.foreach(v=vs.110).aspx
Como você pode ver, há muitas sobrecargas diferentes para esse método. Eles nos
permitem usar um objeto ParallelLoopState ou uma variável local thread-safe.
[ 190 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 7
Vamos nos concentrar na forma simples de apenas executar o processamento simultâneo em uma coleta
de dados. Para reiterar ainda mais esse ponto, vamos revisitar um projeto no qual trabalhamos anteriormente
neste livro. No Capítulo 4, Advanced Thread Processing, escrevemos um aplicativo que pegou uma imagem
JPG, dividiu-a em bitmaps separados e, em seguida, executou funções paralelas em cada bitmap para
localizar estrelas antigas. Em seguida, ele reagrupou os bitmaps individuais em uma única imagem.
Vamos reescrever este aplicativo usando paralelismo de dados e o TPL em vez de threads
diretamente. Isso demonstrará como o TPL pode simplificar o desenvolvimento de código multithread
Não precisamos mais gerenciar threads (iniciá-los, esperar que eles sejam concluídos ou rastreá-
los). Não precisamos mais gerenciar o número de núcleos de processamento que nossa máquina
tem para maximizar o desempenho sem iniciar muitos threads individuais.
Tudo o que precisamos fazer é separar nossa imagem grande em uma coleção de bitmaps menores
e usar um loop simultâneo Parallel.ForEach para processar cada bitmap. É isso.
Vamos começar.
nosso aplicativo OldStarsFinder Windows Form original e alterá-lo. Para isso vamos realizar os seguintes
passos:
2. Vamos adicionar uma nova instrução using para que possamos acessar a biblioteca Parallel :
usando System.Threading.Tasks;
[ 191 ]
www.it-ebooks.info
Machine Translated by Google
Paralelismo de Dados
proBitmap.PixelFormat);
return loCroppedBitmap;
}
// Iterar através de cada linha da matriz de pixels (bitmap) for (liRow = 0; liRow <
loBitmap.Height; liRow++) {
// Iterar através de cada matriz de pixels (bitmap) cols for (liCol = 0; liCol <
loBitmap.Width; liCol++) {
bloqueio (prsOldStarsCount) {
int i = Converter.
ToInt32(prsOldStarsCount);
[ 192 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 7
i = i + 1;
prsOldStarsCount = i.ToString();
}
}
}
}
}
int liThreadNumber;
// Cada parte do bitmap
Bitmap loBitmap;
// A linha inicial em cada iteração int liStartRow = 0;
InterpolationMode.HighQualityBicubic;
{
// Desenha cada porção em seu absoluto correspondente
linha inicial
g.DrawImage(BitmapList[liThreadNumber], 0,
liStartRow);
// Aumenta a linha inicial liStartRow +=
liEachBitmapHeight;
}
// Mostra o bitmap na PictureBox picStarsBitmap picStarsBitmap.Image = loBitmap; //
picStarsBitmap.Image.Save("c:\\packt\\resulting_
image.png", ImageFormat.Png);
tbCount.Text = prsOldStarsCount;
[ 193 ]
www.it-ebooks.info
Machine Translated by Google
Paralelismo de Dados
g.Dispose();
}
ShowBitmapWithOldStars();
}
[ 194 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 7
9. Em seguida, adicione um controle de rótulo com o texto, Número de bitmaps para dividir
para processamento:.
10. Além disso, adicione um controle de caixa de texto e defina sua propriedade Name como tbTasks.
Isso será usado para permitir que você designe o número de seções que deseja que o bitmap
dividido em.
Isso deve ser tudo o que você precisa fazer para executar este aplicativo usando o paralelismo de dados
com a Task Parallel Library.
Vamos compilar e executar nosso aplicativo. Você deve obter algo como isto:
[ 195 ]
www.it-ebooks.info
Machine Translated by Google
Paralelismo de Dados
Agora, digite o número de bitmaps para dividir a imagem e clique no botão Old Star Finder . A
aplicação agora ficará assim:
O que acabou de acontecer? Inserimos 8 para o número de bitmaps nos quais dividir.
O aplicativo divide a imagem JPG em 8 bitmaps de tamanho igual e, em seguida, em uma
coleção de lista de bitmaps. Em seguida, ele processa simultaneamente cada bitmap
procurando estrelas antigas. Por fim, ele remonta os bitmaps em uma imagem e a exibe novamente.
comparar as duas versões do programa, verá que a segunda versão é bem mais simples e com
menos código. Se examinarmos o método manipulador de eventos butOldStarsFinder_Click ,
veremos a maior parte do trabalho. Primeiro, dividimos nossa imagem em uma coleção List de
bitmaps menores com base no número que inserimos. Aqui está o código que faz isso:
[ 196 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 7
// A altura do último bitmap talvez seja menor que // a altura dos outros
bitmaps
CadaBitmapHeight = HeightToAdd;
}
Por fim, quando esse loop estiver concluído, exibimos a imagem com as estrelas antigas com este
método:
ShowBitmapWithOldStars();
É isso. Não precisamos mais descobrir quantos núcleos o processador possui e criar tantos
threads. Não importa quantos itens haja em nossa coleção, o .NET maximiza os encadeamentos
no pool de encadeamentos para atingir o desempenho ideal. Ele criará threads, se necessário, ou
reutilizará threads existentes, se possível. Isso economiza a sobrecarga de iniciar mais threads do que
pode ser efetivamente usado pelo número de núcleos na máquina.
Agora você pode ver por que escrever código multithread usando TPL é chamado de simultaneidade
leve. Esta versão do Old Stars Finder é definitivamente "mais leve" no código e na lógica do que a
versão anterior escrita diretamente com threads ou simultaneidade pesada.
[ 197 ]
www.it-ebooks.info
Machine Translated by Google
Paralelismo de Dados
Para realizar uma quebra ou parada de um loop paralelo, precisamos usar o objeto
ParallelLoopState . Isso significa que temos que usar uma das sobrecargas ou o método
Parallel.For ou Parallel.ForEach que usa um parâmetro ParallelLoopState .
Os métodos break e stop são os métodos que você usará com mais frequência,
assim como as propriedades IsStopped e IsExceptional . Essas propriedades permitem
verificar se alguma iteração do loop chamou Stop ou lançou uma exceção.
Agora, vamos pegar nosso exemplo ParallelMath1 e alterá-lo para parar o loop após
sete iterações. Isso é arbitrário, para fins de exemplo. Mas, em um exemplo real, há
muitas condições em que você deseja interromper ou interromper um loop paralelo.
{
int j = numeros[i];
se (i < 7) {
j *= k;
}
números[i] = j;
}
[ 198 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 7
outro
{
pls.Stop(); retornar;
}
}
2. Em seguida, altere nosso comando Parallel.For para chamar este novo método:
Parallel.For(0, 9, CalculateNumbers2);
É isso. Agora, vamos executar nosso aplicativo e colocar números em cada uma das caixas para que
fique parecido com a captura de tela a seguir:
[ 199 ]
www.it-ebooks.info
Machine Translated by Google
Paralelismo de Dados
O que você vê? Sim, após sete iterações do loop paralelo, o loop é interrompido e as
últimas três iterações não são concluídas. Vamos examinar o porquê.
Como funciona
Adicionando o parâmetro ParallelLoopState ao método chamado pelo método parallel
For , na verdade alteramos a sobrecarga do método que é chamado. Agora estamos
chamando essa sobrecarga:
[ 200 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 7
Você notará que não criamos essa variável ParallelLoopState e a passamos para o método
CalculateNumbers2 . É feito pelo .NET e podemos apenas usá-lo.
Muito útil!
Agora, em nosso Action delegate, CalculateNumbers2, chamamos o método Stop deste objeto
utilizando o seguinte comando:
pls.Stop();
Depois que esse método é chamado, o restante das iterações do loop não é executado e o loop é
concluído com as iterações já concluídas.
Este não é um exemplo muito prático - por que executar o loop por 10 iterações e parar após
sete? Por que não executar o paralelo para sete iterações em primeiro lugar? Este é apenas um
exemplo para fins de demonstração. Em seus aplicativos, você encontrará muitas condições pelas
quais desejará sair de um loop paralelo antes de concluir todas as iterações, exatamente como em
um loop For normal.
O .NET fornece a classe AggregateException exatamente para esse propósito. Isso nos permite coletar
todas as exceções no objeto AggregateException e, em seguida, "capturá-lo" assim que o loop for
concluído.
Pense nisso como se fosse uma situação normal de tratamento de erros. Normalmente, você coloca
um bloco try em torno de uma seção do código e, em seguida, um bloco catch após ele para
processar quaisquer exceções que ocorram no bloco try do código. Isso vai se comportar da mesma
maneira. Colocaremos um bloco try em torno de nosso comando de loop paralelo e, em seguida,
capturaremos as exceções que ocorrerem em todas as iterações do loop. Somente neste caso, nosso
bloco catch irá capturar a AggregateException, que é apenas uma coleção de exceções.
Além disso, em nosso delegado Action , vamos capturar todas as exceções que ocorrerem e adicioná-
las ao objeto AggregateException .
Isso é tudo! Vamos tentar isso por nós mesmos modificando nosso projeto ParallelMath1 para gerar
uma exceção se qualquer uma das iterações produzir uma soma superior a 5 milhões.
[ 201 ]
www.it-ebooks.info
Machine Translated by Google
Paralelismo de Dados
1. Estaremos usando uma fila Simultânea para coletar as exceções, então adicione
esta instrução using :
usando System.Collections.Concurrent;
{
int j = numeros[i];
tentar
{
for (int k = 1; k <= 10; k++) {
j *= k;
} catch(Exceção e) {
exceções.Enfileirar(e);
}
números[i] = j;
[ 202 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 7
if (ex é ArgumentException) {
tbMensagens.Texto += ex.Mensagem;
tbMessages.Text += "\r\n";
}
outro
jogue ex;
}
}
Agora nossas mudanças estão completas. Quando qualquer uma das iterações do loop paralelo atingir
acima de 5 milhões, lançaremos uma exceção. Todas as exceções serão coletadas em um
ConcurrentQueue e adicionadas ao objeto AggregateException . Uma vez concluída a execução do loop
paralelo, processaremos a AggregateException, se houver, e gravaremos suas mensagens de exceção
no bloco de texto Messages .
[ 203 ]
www.it-ebooks.info
Machine Translated by Google
Paralelismo de Dados
Compile e execute seu aplicativo. Agora, digite os números em cada uma das caixas.
Você deve ter uma tela parecida com a captura de tela a seguir:
Agora, clique no botão Calcular e você verá resultados semelhantes à captura de tela a
seguir:
[ 204 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 7
Como você pode ver na saída, cada caixa com um total superior a 5 milhões tem uma linha
impressa em nosso bloco de texto Mensagens . Neste exemplo, qualquer número que
ultrapasse 5 milhões gera uma exceção. Depois que todas as iterações do loop paralelo forem
concluídas, processamos essas exceções e imprimimos suas mensagens no bloco de texto
tbMessages .
A seguir, em nosso Action delegado do comando parallel for , que nesta versão executa
CalculateNumbers3, verificamos se há números maiores que 5 milhões através de uma exceção
utilizando o seguinte comando:
if (j > 5000000) throw new ArgumentException(String.Format("O valor da caixa de texto {0} é {1}. ", i, j));
catch(Exceção e) {
exceções.Enfileirar(e);
}
Fazemos isso porque não queremos interromper outras iterações do loop paralelo em
execução em diferentes threads. Queremos que o loop termine o processamento e, em
seguida, lide com as exceções. Como cada iteração do loop está sendo executada
simultaneamente e foi projetada para não afetar outras iterações, não devemos interromper
todas as iterações porque uma tem um problema.
tentar
{
Parallel.For(0, 10, CalculateNumbers3);
[ 205 ]
www.it-ebooks.info
Machine Translated by Google
Paralelismo de Dados
{
// Aqui é onde você pode escolher quais exceções
lidar.
foreach (var ex em ae.InnerExceptions) {
if (ex é ArgumentException) {
tbMensagens.Texto += ex.Mensagem;
tbMessages.Text += "\r\n";
} outro
jogue ex;
}
}
Também poderíamos ter executado ações diferentes com base no tipo de exceção de
cada exceção. Você pode brincar com seu código e tentar isso.
Pelo que aprendemos até agora, criaríamos uma variável de classe antes do loop e a
acessaríamos a cada iteração do loop usando uma instrução lock para que permanecesse thread-
safe. Isso leva tempo de sobrecarga e coordenação. Para melhorar o desempenho, podemos
implementar nosso loop paralelo usando uma variável local de thread.
Nessas sobrecargas dos métodos de loop paralelo, há três funções que são passadas para o
loop, bem como os parâmetros de iteração. Para um loop For , os parâmetros de iteração são
os valores inicial e final do índice do loop e, para o loop ForEach , é a coleção de origem.
[ 206 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 7
A primeira função inicializará a variável thread-local. A segunda função é o delegado Action que o loop
executa. A terceira função é o delegado Action que é executado quando todas as iterações do loop são
concluídas e recebe a variável thread-local para cada iteração do loop. Ele pode então processar os
resultados, o que geralmente significa combinar os resultados.
Vamos dissecar isso por um minuto. Vamos pegar cada parte da definição do método e explicar
seu papel:
Embora este seja um conceito bastante direto, ele leva a uma sobrecarga de método que parece
muito complicada. Para ter certeza de que entendemos, vamos ao nosso projeto ParallelMath1 e
veremos como ele funciona no aplicativo de amostra que estamos construindo.
2. Agora, vamos adicionar um controle de caixa de texto ao lado dele e definir a propriedade Name
como tbSum e deixar a propriedade Text vazia.
[ 207 ]
www.it-ebooks.info
Machine Translated by Google
Paralelismo de Dados
usando System.Threading;
4. Além disso, adicione uma variável de classe abaixo de nossa declaração ConcurrentQueue para obter uma soma
variável que chamaremos de total:
longo total = 0;
6. É mais fácil para esta versão de um Parallel.For usar uma expressão lambda
em vez dos métodos nomeados para os delegados Action . Portanto, use o seguinte comando
Parallel.For :
j *= k;
}
números[i] = j; subtotal
+= j; subtotal de retorno;
},
(finalResult) => Interlocked.Add(ref
total, resultado final)
);
7. Finalmente, logo após esta instrução, adicione a seguinte instrução para que possamos ver o total
na interface do usuário:
tbSum.Text = total.ToString();
Essas são todas as alterações que precisamos fazer para que possamos usar nossa variável thread-local
com o loop Parallel.For para calcular a soma de nossa caixa de texto.
Depois que essas alterações forem feitas, crie e execute o aplicativo. Digite os números nas caixas de texto
e você deve ter uma tela parecida com a seguinte captura de tela:
[ 208 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 7
Agora, clique no botão Calcular e veja o que acontece. Os resultados devem se parecer
com a captura de tela a seguir:
[ 209 ]
www.it-ebooks.info
Machine Translated by Google
Paralelismo de Dados
Como você pode ver no exemplo, agora temos uma soma de todas as caixas depois que o loop
paralelo as processou. Podemos fazer isso sem ter que bloquear continuamente a variável de classe
em cada iteração quando ela deseja atualizar o loop.
Podemos fazer a soma uma vez no final do loop paralelo usando o valor local da thread de cada iteração
do loop.
nas versões anteriores deste projeto, pegamos os números em 10 caixas de texto diferentes e os multiplicamos
pelos números de 1 a 10 e os somamos. O resultado é então colocado de volta na caixa de texto. Mas desta
vez, pegamos os novos resultados nas 10 caixas de texto e os somamos, e o total final é exibido na caixa de
texto tbSum .
A única diferença real nesta versão é o comando Parallel.For . Vamos dar uma olhada mais profunda
nisso:
j *= k;
}
números[i] = j; subtotal
+= j; subtotal de retorno;
},
(finalResult) => Interlocked.Add(ref
total, resultado final)
);
Primeiro, agora temos um parâmetro TResult que é um Parallel.For<long> longo. Isso nos diz que o Action
delegado que cada iteração do loop executa retornará um valor com o tipo de dados long. Este delegado
Action é implementado como uma expressão lambda desta vez e é o quarto parâmetro do nosso método
Parallel.For :
[ 210 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 7
{
j *= k;
}
números[i] = j; subtotal
+= j; subtotal de retorno;
Vamos criar um backup; os dois primeiros parâmetros são os índices iniciais e finais de nossa
iteração, 0 e 10. O terceiro parâmetro é nosso delegado Action que inicializa a variável thread-
local. É implementado com uma expressão lambda:
() => 0
Então, nosso parâmetro final para o método Parallel.For é o Action delegate que é executado
na variável thread-local de cada iteração:
Escolhemos usar a expressão lambda para os três delegados Action neste exemplo em vez
de métodos nomeados ou anônimos porque é mais fácil ver o que está acontecendo e o
que está sendo passado para o quê. No entanto, podemos usar métodos nomeados para obter
os mesmos resultados.
Resumo
Neste capítulo, cobrimos todos os aspectos do paralelismo de dados imperativo. No
Capítulo 10, Parallel LINQ – PLINQ, abordaremos o paralelismo de dados declarativo com
a discussão do PLINQ. O paralelismo de dados usando TPL no .NET realmente se resume
a executar loops paralelos usando os métodos Parallel.For e Parallel.ForEach . Esses loops
paralelos nos permitem iterar por um conjunto ou coleção de dados e executar a mesma
função em cada membro do conjunto simultaneamente.
[ 211 ]
www.it-ebooks.info
Machine Translated by Google
Paralelismo de Dados
Na última seção, vimos como usar variáveis locais de thread em nossos loops para ter uma
cópia local thread-safe de uma variável e, em seguida, usar os resultados de todas essas
cópias locais no final do processamento do loop.
No próximo capítulo, vamos dedicar algum tempo para explorar o Visual Studio Debugger e os
recursos que ele fornece para depurar um aplicativo paralelo que possui vários threads em
execução ao mesmo tempo.
[ 212 ]
www.it-ebooks.info
Machine Translated by Google
Depurando Multithread
Aplicações com Visual
Estúdio
Aplicativos multithread apresentam seu próprio conjunto de desafios para desenvolver, como vimos
até agora neste livro, mas eles também têm um conjunto exclusivo de desafios para depurar.
O Visual Studio, começando com o Visual Studio 2010, desenvolveu adições ao depurador para
auxiliar nas tarefas de depuração e na biblioteca Parallel .
O Visual Studio tem a janela Threads em seu depurador desde 2003 para ajudar na depuração de
threads. No entanto, a partir do Visual Studio 2010, eles adicionaram a janela Parallel Stacks , a janela
Parallel Watch e a janela Tasks para ajudar na depuração de aplicativos usando a Task Parallel Library
(TPL). Neste capítulo, examinaremos todas essas janelas e demonstraremos como percorrer um aplicativo
multithread e multitarefa para encontrar problemas de fluxo de programa e condições de corrida/bloqueio.
Uma condição de corrida é um tipo de erro que ocorre quando vários threads interferem uns com os
outros e é baseado no tempo e na execução dos diferentes threads. Por causa disso, a condição não
ocorre toda vez que o aplicativo é executado. Leva um certo tempo de execução dos diferentes threads.
Uma condição de corrida acontece quando dois ou mais threads acessam uma variável de dados e
tentam modificá-la aproximadamente ao mesmo tempo. Como o agendador do sistema operacional
pode alternar entre threads a qualquer momento, você não pode prever a ordem na qual os threads
acessarão os dados. O resultado da alteração nos dados depende do algoritmo de agendamento do
sistema operacional. Ambos os segmentos estão "correndo" para acessar os dados.
Podem ocorrer problemas quando um thread examina o conteúdo de uma variável e executa uma
ação, mas outro thread altera o conteúdo da mesma variável durante o tempo em que o primeiro
thread o examina e então age sobre ele.
www.it-ebooks.info
Machine Translated by Google
Uma condição de bloqueio ocorre quando dois ou mais threads estão aguardando um recurso que está
bloqueado por outro thread. Portanto, se o thread 1 estiver esperando por um recurso bloqueado pelo
thread 2 e o thread 2 estiver esperando por um recurso bloqueado pelo thread 1, ambos os threads
permanecerão "pendurados" para sempre esperando um pelo outro para liberar um recurso. Este é um tipo
de condição de bloqueio chamada deadlock.
uma tarefa
Em segundo lugar, a depuração de um aplicativo simultâneo requer muitas informações. Ao usar o depurador
em um aplicativo TPL, você pode ter as janelas Local, Imediata, Threads, Tarefa, Pilha Paralela, Tarefa
Paralela, Process Explorer, Código-fonte e Object Explorer todas abertas ao mesmo tempo. Isso ocupa
muito espaço na tela ou requer janelas em movimento constante para ver as informações de que você
precisa. Eu recomendo fazer isso em uma configuração de monitor duplo. Você nunca pode ter muito espaço
na tela ao depurar um aplicativo paralelo. Existem poucas coisas mais frustrantes do que não poder ver
facilmente todas as informações de que você precisa ao percorrer um aplicativo.
[ 214 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 8
Se você iniciar o depurador em um aplicativo e depois acessar Depurar | Windows, você pode
selecionar a janela Threads . Se você fizer isso, deverá ver algo como o seguinte; esta é a janela
Threads para a versão do thread do programa OldStarFinder que fizemos no Capítulo 7, Paralelismo
de dados:
Vamos dar uma olhada nesta janela e examiná-la em detalhes. Contém as seguintes
colunas:
•
Sinalizador • Ativo
• identificação
• ID Gerenciado •
Categoria
• Nome
• Localização
• Prioridade
• Máscara de Afinidade
[ 215 ]
www.it-ebooks.info
Machine Translated by Google
• Contagem Suspensa
• Nome do processo
• ID do processo
• Qualificador de transporte
A coluna Sinalizador permite que você designe um encadeamento como aquele que deseja observar.
Se você clicar com o botão direito do mouse em um tópico nesta janela, poderá marcá-lo ou desmarcá-lo como
um tópico sinalizado.
A coluna Ativo indica qual encadeamento é o encadeamento ativo no momento. Uma seta amarela designa o thread
ativo no depurador. Também é importante observar que, ao percorrer um thread, outros threads continuam sua
operação, a menos que um ponto de interrupção seja definido. Todos os encadeamentos não são bloqueados enquanto
você percorre um encadeamento específico.
A coluna Name exibe a propriedade Name do thread, se estiver definida ou, caso contrário, exibe <No Name>.
A coluna Affinity Mask mostra essa máscara para cada encadeamento. A afinidade é uma máscara bit a bit que indica
em quais processadores um thread pode ser executado. Em nosso exemplo, temos um sistema de quatro processadores
e todos os threads, exceto um, têm uma máscara que termina em 1111. Esses são os quatro bits que representam nossos
quatro processadores. Isso indica que esses threads podem ser executados em qualquer um dos processadores. Existem
métodos na classe Threads que permitem definir programaticamente em quais processadores um thread pode ser
executado, se desejar.
[ 216 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 8
Agora que analisamos todas as informações fornecidas pela janela Threads , quais são
algumas das funções que você pode fazer com ela? Você pode mostrar ou ocultar colunas na exibição
clicando no menu suspenso Colunas no cabeçalho desta janela; você também pode fazer uma
operação Group By dos threads para agrupá-los para melhor gerenciamento. Já vimos como você pode
clicar duas vezes em uma linha na coluna Location e ver a pilha de chamadas de uma thread. Você
também pode tornar qualquer encadeamento o encadeamento ativo clicando com o botão direito nele
e selecionando Alternar para encadeamento.
[ 217 ]
www.it-ebooks.info
Machine Translated by Google
Vamos examinar as informações e funções para as quais você pode usar a janela Tarefas :
• Sinalizadores: esta coluna mostra as tarefas que você sinalizou ou desmarcou. Ao clicar nesta
coluna para uma tarefa, você pode sinalizar os encadeamentos nesta tarefa. Você pode sinalizar
várias tarefas e depois classificar ou usar a função Agrupar por para mostrar as tarefas
sinalizadas na parte superior. Você também pode filtrar a janela Pilhas paralelas por tarefas
sinalizadas para ver apenas as pilhas paralelas das tarefas nas quais está interessado.
• Ícones: Esta coluna mostra qual é a tarefa atual presente no thread atual. Isso é mostrado por
uma seta amarela. Uma seta branca indica que a tarefa atual está em um ponto de interrupção
no depurador. Um símbolo de pausa indica que uma tarefa foi congelada.
• ID: Esta coluna é um número de identificação gerado pelo Windows para uma tarefa. Em nosso
exemplo, inseri 8 na caixa de texto Número de bitmaps a serem divididos para processamento
e, em seguida, defini um ponto de interrupção após o loop Parallel.ForEach . Você pode ver que
criamos oito tarefas que estão executando um método anônimo e que ele é iniciado a partir do
manipulador de eventos click do botão Find Old Stars . Seus IDs são números de 1 a 8.
• Status: Esta coluna mostra o estado da tarefa, que pode ser qualquer um dos seguintes: ° Active: A
tarefa está atualmente ativa na pilha ° Deadlocked: A tarefa está atualmente em um impasse
estiver disponível
Uma tarefa é considerada em estado de deadlock se seu thread estiver em uma condição
de bloqueio com outro thread no aplicativo. Uma tarefa é considerada em espera se estiver
aguardando um bloqueio para ser liberado ou outra tarefa para ser concluída.
• Local: esta coluna exibe a pilha de chamadas para essa tarefa específica. Ao contrário da janela
Threads , para ver toda a pilha de chamadas, você precisa passar o mouse sobre esta coluna
em vez de clicar duas vezes nela. Além disso, se uma tarefa não foi iniciada, mas está agendada,
esta coluna ainda não será preenchida. • Pai: Esta coluna exibe o ID da tarefa que a criou. •
no qual a tarefa está sendo executada. Em nosso exemplo, você pode ver que usamos um número
para nomear o thread de nossas tarefas nesta linha de código:
[ 218 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 8
Thread.CurrentThread.Name = ThreadName;
• APPDomain: Esta coluna mostra o domínio do aplicativo no qual a tarefa está sendo executada. A
coluna Processo exibe o ID do Windows do processo do Windows no qual a tarefa está sendo
executada. • AsyncState: esta coluna, se estiver usando as palavras-chave async ou await para
Ao depurar aplicativos TPL, esta janela é muito útil para obter uma visão geral de todas as tarefas
que seu código gerou, que estão ativas, e a pilha de chamadas e informações de encadeamento de cada
uma delas. Combinar isso com a janela Threads lhe dará uma boa visão de todas as entidades em
execução em seu aplicativo e onde elas estão em sua execução.
As janelas Threads e Tasks são uma boa maneira tabular de exibir todas as entidades de seu aplicativo
multithread, mas não fazem um bom trabalho de exibição de informações de relacionamento e fluxo do
programa.
As informações das Pilhas Paralelas têm uma visualização de Threads e uma visualização de Tarefas . Cada uma dessas
[ 219 ]
www.it-ebooks.info
Machine Translated by Google
[ 220 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 8
A captura de tela a seguir mostra a exibição de tarefas alternada para a exibição de método de
nosso aplicativo OldStarsFinder após a execução do comando Parallel.ForEach e o bitmap é dividido
em oito seções:
Há vários itens a serem observados sobre esta janela. A primeira é a barra de ferramentas. Ele
permite que você selecione Threads ou Tasks no menu suspenso. Ele também permite que você mostre
apenas threads sinalizados na janela. Em seguida, ele permite alternar para a exibição Método e também
ampliar uma área.
Nas visualizações Threads e Tasks , o caminho de chamada do thread atual é realçado em azul. É por
isso que você pode ver mais de uma caixa destacada. As setas conectadas às caixas mostram o caminho
de chamada da tarefa ou thread, e cada caixa no diagrama representa uma pilha de chamadas. Além
disso, se você passar o mouse sobre o cabeçalho de uma caixa (também conhecido como nó) ou uma
linha em um nó, obterá uma dica de ferramenta. A dica de ferramenta do cabeçalho do nó mostra o ID do
encadeamento e o nome de todos os encadeamentos no caminho de chamada que leva a esse nó. Na dica
de linha ou método, você verá a pilha do método.
A visualização Method que mostramos anteriormente mostra todos os métodos que chamam ou são
chamados pelo método atual. O método atual é mostrado no meio com os métodos que o chamam abaixo
e os métodos que ele chama acima. Essa é uma ótima maneira de obter uma exibição atual do tempo de
execução da função do Visual Studio, Localizar todas as referências. Não é exatamente o mesmo, mas
é semelhante.
[ 221 ]
www.it-ebooks.info
Machine Translated by Google
A janela Parallel Stacks é provavelmente onde você passará a maior parte do tempo no
modo de depuração, exceto para percorrer o código. Normalmente, você terá as janelas
Threads e Tasks abertas para uma referência tabular, o código para definir pontos de
interrupção e percorrer as linhas e a janela Parallel Stacks para seguir todos os threads,
tarefas e suas relações.
Se você clicar na barra <Add Watch> , poderá digitar uma expressão para assistir. Isso
permite que você veja o valor da mesma expressão em vários threads. Você pode exibir até
quatro janelas de Observação Paralela exatamente como a janela de Observação normal
se desejar observar mais de uma expressão. Você também pode filtrar a lista de observação
usando a caixa Filtrar por expressão booleana no canto superior direito da janela. Se você
digitar uma expressão lá, ele mostrará apenas os tópicos em que essa expressão é verdadeira:
A janela Parallel Watch é realmente apenas a janela Watch , mas permite que você
observe a mesma expressão em todos os threads em seu aplicativo. Qualquer expressão
C# é válida nesta janela.
[ 222 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 8
Esta janela é uma maneira muito boa em um loop Parallel.For ou Parallel.ForEach que está
executando o mesmo delegado várias vezes em várias tarefas para exibir o valor de uma expressão
no delegado. Permite ver o valor atual que cada tarefa tem naquele momento de execução.
[ 223 ]
www.it-ebooks.info
Machine Translated by Google
2. Agora vamos colocar uma segunda quebra no método ThreadOldStarsFinder que cada
uma das tarefas paralelas está executando. Vamos colocá-lo no início do método
onde definimos o nome do thread usando o número aleatório que passamos do
comando Parallel.ForEach :
[ 224 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 8
[ 225 ]
www.it-ebooks.info
Machine Translated by Google
4. Em seguida, vamos clicar no botão Old Star Finder . Veremos que o código
entra no manipulador de eventos de clique do botão e chega ao primeiro ponto de
interrupção que definimos no comando Parallel.ForEach . A captura de tela a seguir
mostra a aparência do depurador junto com as janelas Thread, Tasks, Parallel
Stacks e Parallel Watch :
Logo antes de passarmos pelo comando Parallel.ForEach , você pode ver que ainda
não há tarefas:
[ 226 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 8
Você também verá que não há threads extras para essas tarefas porque
ainda não executamos o loop de dados paralelo:
[ 227 ]
www.it-ebooks.info
Machine Translated by Google
5. Agora, vamos passar por cima do comando Parallel.ForEach e observar esses mesmos
janelas enquanto estamos em um ponto de interrupção no delegado:
6. Agora, se você passar o mouse sobre a linha na janela Parallel Stacks com
na seta amarela, você verá a linha de código que cada thread está executando.
Isso está usando a visualização Threads da janela Parallel Stacks :
[ 228 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 8
8. Além disso, ao mesmo tempo, vamos examinar nossa janela Parallel Watch :
[ 229 ]
www.it-ebooks.info
Machine Translated by Google
10. Em seguida, comece a percorrer o delegado e observe o que acontece. Aqui está
uma visão do nosso exemplo:
[ 230 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 8
11. Observe como as informações mudam nas janelas conforme você começa a
percorrer o delegado de uma tarefa em um encadeamento. Você notará que todas
as tarefas estão, ou podem estar, em linhas diferentes depois de passar por uma linha
neste encadeamento. Além disso, a janela Parallel Watch mostrará diferentes valores
da expressão à medida que você avança.
12. Agora, deixe o programa rodar até completar o loop paralelo e estar pronto para
remontar as peças.
13. Como você pode ver, ainda temos vários threads em execução em nosso aplicativo
mas sem tarefas. Isso indica que todas as tarefas que criamos no Parallel.
O loop ForEach concluiu suas funções de delegação e as tarefas não estão mais no
threadpool. Mas o aplicativo ainda tem threads que está usando e alguns precisam ser
limpos.
Agora vimos como abrir todas as várias janelas no depurador para depuração paralela e
percorremos nosso aplicativo multithread baseado em TPL.
Vamos falar sobre o que acabou de acontecer e o que vimos.
[ 231 ]
www.it-ebooks.info
Machine Translated by Google
Como funciona
Você pode ver em nosso exemplo anterior que antes de inserirmos o comando
Parallel.ForEach , não há tarefas mostradas em nossa janela Tarefas ou na janela Pilhas
Paralelas na exibição Tarefas ; isso é mostrado na captura de tela a seguir:
Ainda não há tarefas porque não criamos nenhuma. Mas o aplicativo já tem vários
tópicos. Existe um para o thread principal e é aquele em que temos um ponto de
interrupção, um para lidar com eventos do sistema .NET e outro para o depurador do
visual studio em que nosso aplicativo está sendo executado.
[ 232 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 8
Agora, você pode ver que a seta amarela está no manipulador de eventos click do botão Old Stars
Finder na linha 139 exatamente onde está nosso ponto de interrupção. A variável prsOldStarsCount
está definida como 0 porque foi assim que a inicializamos e nenhuma das versões do delegado foi
executada ainda para atualizá-la.
[ 233 ]
www.it-ebooks.info
Machine Translated by Google
Agora, passei pela linha do delegado duas vezes para permitir que duas das tarefas paralelas
sejam iniciadas. Vejamos os resultados até agora:
Lembre-se de que quando passamos por cima de uma linha em um thread, os outros threads
continuam a operar até que o thread que passamos seja concluído e o controle seja devolvido ao
depurador. Então, muitas outras coisas podem acontecer enquanto executamos uma linha em um
thread. Como vemos aqui, estamos atualmente em uma tarefa de número 3 na linha 49. Também
podemos ver que a tarefa 1 está aguardando na linha 57. Também notamos na janela Parallel
Watch que ambas as tarefas mostram o valor de 369 na variável , prsOldStarsCount.
O que isso nos diz?
Isso nos diz que, embora ainda estejamos em uma das primeiras linhas do método delegado na
tarefa 3 e ainda não tenha iniciado nenhum processamento real, outras tarefas já encontraram
369 estrelas antigas. Você também notará que ambas as tarefas na janela Parallel Watch
mostram o mesmo valor para prsOldStarsCount. Isso ocorre porque esta é uma variável global e
todas as tarefas estão olhando e atualizando a mesma cópia. Agora, vamos passar por uma linha
de código no delegado da tarefa 3 e ver o que acontece:
[ 234 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 8
Agora, estamos na linha 49 na tarefa de número 4 e o valor de prsOldStarsCount é 605. O que acabou
de acontecer? Passamos uma linha na tarefa 3 e terminamos na tarefa
4 na mesma linha.
Lembre-se de que temos um ponto de interrupção definido no método delegado de cada uma das seis
tarefas que nosso loop Parallel.ForEach criou. Quando você passa por cima de uma linha ou continua
no depurador, ele para no próximo ponto de interrupção encontrado. Além disso, lembre-se de que,
quando ultrapassamos uma linha, todos os nossos threads e tarefas podem ser executados durante esse tempo.
Portanto, quando ultrapassamos a linha na tarefa 3, o próximo ponto de interrupção que o depurador
encontrou foi a mesma linha na tarefa 4. Você pode ver várias coisas em nossa janela. As tarefas 2, 3 e
4 estão todas na linha 49 e não chegaram à linha 51 (a próxima linha executável).
Isso nos diz que, quando ultrapassamos a linha 49 na tarefa 3 , o depurador encontrou uma interrupção
na linha 49 da tarefa 4 antes que a execução da linha 49 na tarefa 3 fosse concluída. Também vemos que,
no tempo que levou para alcançar o próximo ponto de interrupção, nossas tarefas contaram 605 estrelas
antigas, acima das 369. Como a tarefa 1 é a única tarefa que vemos além da linha 49, podemos supor que
é a única que contou todas essas estrelas.
[ 235 ]
www.it-ebooks.info
Machine Translated by Google
Agora, vamos passar por cima desta linha e ver o que acontece a seguir:
Desta vez, passamos para a linha 49 da tarefa 2 e nenhuma estrela antiga foi contada.
Vamos passar por mais uma linha e examinar:
[ 236 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 8
Agora paramos na linha 49 da tarefa 5. Você também pode ver que ambas as tarefas 1 e 3
passaram da linha 49 e agora contamos 869 estrelas antigas. Vamos passar por mais uma
linha:
Agora você pode ver que iniciamos todas as seis tarefas que solicitamos em nosso Parallel.
ForEach loop e já temos até 1331 estrelas antigas. Mas mesmo com 1.331 estrelas antigas
já encontradas, não passamos da linha 49 em três de nossas seis tarefas. Vamos passar por
mais uma linha. Como atingimos essa linha de ponto de interrupção em todas as seis tarefas,
agora devemos avançar para a tarefa atual em que estamos, que é a tarefa 6.
[ 237 ]
www.it-ebooks.info
Machine Translated by Google
Mas conforme fazemos isso, todas as outras tarefas podem ser executadas. Dito isto, a máquina em que estamos
rodando tem quatro núcleos de processamento, então nem todos eles podem obter tempo de CPU ao mesmo tempo,
conforme mostrado:
Como você pode ver, agora podemos começar a percorrer a tarefa atual sem parar porque o delegado que todas as
tarefas estão executando não tem mais pontos de interrupção que possam interferir. Você também pode ver que, no tempo
que levou para executar esta linha na tarefa 6, contamos 1686 estrelas antigas e movemos algumas linhas em algumas
outras tarefas.
Neste ponto, continue avançando e veja o que acontece; então pare sua aplicação e refaça o exercício. Cada
vez que você executar o aplicativo no depurador, deverá ver resultados ligeiramente diferentes porque temos vários
encadeamentos em execução e competindo pelo tempo de processamento. Cada vez que você percorre o código, diferentes
threads obtêm diferentes quantidades de tempo de CPU, dependendo da disponibilidade e de outros processos concorrentes
em execução em sua máquina. Lembre-se de que seu aplicativo e a depuração não são a única coisa em execução em sua
máquina Windows.
[ 238 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 8
Resumo
Neste capítulo, examinamos toda a funcionalidade que o Visual Studio fornece aos desenvolvedores
de aplicativos multithread e paralelos para ajudá-los a solucionar problemas e depurar seu código-
fonte. Examinamos a janela Threads , a janela Tasks , a janela Parallel Tasks no modo Threads ,
a janela Parallel Stacks no modo Tasks e a janela Parallel Stacks . Também examinamos nosso
aplicativo OldStarsFinder usando essas janelas e, em seguida, passamos por sua execução.
[ 239 ]
www.it-ebooks.info
Machine Translated by Google
www.it-ebooks.info
Machine Translated by Google
Padrões de design do
consumidor de pipeline e produtor
Neste capítulo, exploraremos dois padrões de projeto populares para resolver problemas
simultâneos — Pipeline e produtor-consumidor, que são usados no desenvolvimento de aplicativos
paralelos usando o TPL. Um design de pipeline é aquele em que um aplicativo é projetado com
várias tarefas ou estágios de funcionalidade com filas de itens de trabalho entre eles. Portanto, para
cada estágio, o aplicativo lerá uma fila de trabalho a ser executado, executará o trabalho nesse item
e enfileirará os resultados para o próximo estágio. Ao projetar o aplicativo dessa maneira, todos os
estágios podem ser executados em paralelo.
Cada estágio apenas lê de sua fila de trabalho, executa o trabalho e coloca os resultados do trabalho
na fila para o próximo estágio.
Cada estágio é uma tarefa e pode ser executado independentemente dos outros estágios ou
tarefas. Eles continuam executando até que sua fila esteja vazia e marcada como concluída. Eles
também bloqueiam e aguardam mais itens de trabalho se a fila estiver vazia, mas não concluída.
www.it-ebooks.info
Machine Translated by Google
biblioteca
[ 242 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 9
Entrada
Arquivo
Estágio 1
Ler dados
Tampão 1
Estágio 2
criptografar dados
Tampão 2
Estágio 3
Descriptografar dados
Saída
Arquivo
Vamos começar.
[ 243 ]
www.it-ebooks.info
Machine Translated by Google
1. Crie uma nova classe chamada Stages.cs. Em seguida, verifique se ele tem o seguinte
usando declarações.
usando Sistema;
usando System.Collections.Concurrent; usando
System.Collections.Generic; usando System.IO;
usando System.Linq; usando System.Text; usando
System.Threading.Tasks; usando System.Threading;
3. Em seguida, adicionaremos um método para cada um dos três estágios em nosso pipeline.
Primeiro, criaremos um método chamado FirstStage. Serão necessários dois parâmetros:
um será um objeto BlockingCollection que será o buffer de saída deste estágio, e o segundo
será uma string apontando para o arquivo de dados de entrada. Este será um arquivo de
texto contendo alguns parágrafos de texto a serem criptografados. Colocaremos este
arquivo de texto na pasta de projetos em C:. O método FirstStage terá o seguinte código:
public void FirstStage(BlockingCollection<char> output, String PipelineInputFile) {
[ 244 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 9
} finalmente
{
output.CompleteAdding();
}
}
tentar
[ 245 ]
www.it-ebooks.info
Machine Translated by Google
} finalmente
{
output.CompleteAdding();
}
}
{
String OutputString = "";
String DisplayData = "";
//Descriptografar os dados.
char descriptografado = Descriptografar(C);
[ 246 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 9
6. Agora que temos métodos para os três estágios de nosso pipeline, vamos adicionar alguns
métodos utilitários. O primeiro desses métodos será aquele que lê o arquivo de dados de
entrada e coloca cada caractere no arquivo de dados em um objeto List . Este método receberá
um parâmetro de string que possui um nome de arquivo e retornará um objeto List de caracteres.
Ele terá o seguinte código: public List<char> GetData(String PipelineInputFile)
{
List<char> Dados = new List<char>();
Data.Add((char)inputfile.Read());
}
dados de retorno;
}
7. Agora vamos precisar de um método para criptografar os caracteres. Este será um método de
criptografia simples. O método de criptografia não é realmente importante para este exercício.
Este exercício foi desenvolvido para demonstrar o design do Pipeline, não para implementar a
criptografia mais rígida do mundo. Essa criptografia simplesmente pegará cada caractere e
adicionará um ao seu valor numérico ASCII. O método usará um tipo de caractere como
parâmetro de entrada e retornará um caractere. O código
pois será assim:
int i = (int)C; i = i + 1; C
= Convert.ToChar(i);
retornar C;
}
[ 247 ]
www.it-ebooks.info
Machine Translated by Google
retornar C;
}
9. Agora que terminamos com a classe Stages , vamos mudar nosso foco de
volta para o arquivo MainWindow.xaml.cs . Primeiro, você precisará
adicionar três instruções using . Eles são para as classes StreamReader,
StreamWriter, Threads e BlockingCollection :
usando System.Collections.Concurrent; usando System.IO;
usando System.Threading;
10. No topo da classe MainWindow , precisamos de quatro variáveis disponíveis para toda
a classe. Precisamos de três strings que apontem para nossos três arquivos de dados
— os dados de entrada, os dados criptografados e os dados de saída. Então vamos
precisar de um objeto Stages . Essas declarações terão a seguinte aparência: private
static String PipelineResultsFile = @"c:\projects\ OutputData.txt";
[ 248 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 9
dois objetos BlockingCollection para duas filas. Uma fila entre os estágios
1 e 2 e uma fila entre os estágios 2 e 3. Esse método criará uma tarefa
para cada estágio que executa os métodos correspondentes das classes
Stages . Em seguida, ele iniciará essas três tarefas e aguardará a conclusão
delas. Por fim, ele gravará a saída de cada estágio nos arquivos de dados de
entrada, criptografados e de resultados e blocos de texto para visualização. O
código para ele será semelhante ao seguinte código: private void
butEncrpt_Click(object sender, RoutedEventArgs e)
{
//Padrão de Projeto PipeLine
[ 249 ]
www.it-ebooks.info
Machine Translated by Google
}
}
13. Uma última coisa. Vamos adicionar três blocos de texto para exibir as saídas. Vamos
chame-os de tbStage1, tbStage2 e tbStage3. Também adicionaremos três controles
de rótulo com o texto Arquivo de entrada, Arquivo criptografado e Arquivo de saída.
Estes serão colocados pelos blocos de texto correspondentes. Agora, o MainWindow.
xaml deve se parecer com a captura de tela a seguir:
[ 250 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 9
14. Agora vamos precisar de um arquivo de dados de entrada para criptografar. Chamaremos este arquivo
InputData.txt e coloque-o na pasta C:\projects do nosso computador.
Para o nosso exemplo, adicionamos o seguinte texto a ele:
[ 251 ]
www.it-ebooks.info
Machine Translated by Google
16. Agora, clique no botão Criptografar arquivo e você deverá ver a seguinte saída:
Como você pode ver, os arquivos de entrada e saída são iguais e o arquivo criptografado é diferente. Lembre-se
que Input File é o texto que colocamos no arquivo de texto de dados de entrada; esta é a entrada do final do
estágio 1 depois de lermos o arquivo em uma lista de caracteres.
Arquivo criptografado é a saída do estágio 2 depois de criptografarmos cada caractere.
Arquivo de saída é a saída do estágio 3 depois de descriptografarmos os caracteres novamente.
Deve corresponder ao arquivo de entrada.
método do manipulador de eventos de clique butEncrypt no arquivo MainWindow.xaml. cs , pois é aqui que
ocorre grande parte da ação. Vamos examinar as seguintes linhas de código:
[ 252 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 9
Primeiro, criamos duas filas que são implementadas usando objetos BlockingCollection .
Cada um deles é definido com um tamanho de 20 itens. Essas duas filas recebem um tipo de dados de caractere.
Em seguida, criamos um objeto TaskFactory e o usamos para iniciar três tarefas. Cada
tarefa usa uma expressão lambda que executa um dos métodos de estágios da classe
Stages — FirstStage, StageWorker e FinalStage.
Portanto, agora temos três tarefas separadas em execução além do thread principal da interface do
usuário. Stage1 lerá o arquivo de dados de entrada caractere por caractere e colocará cada
caractere na fila Buffer1. Lembre-se de que esta fila pode conter apenas 20 itens antes de bloquear o
método FirstStage esperando na sala na fila. É assim que sabemos que o Stage2 começa a ser
executado antes que o Stage1 seja concluído. Caso contrário, o Stage1 só entrará na fila
os primeiros 20 caracteres e depois bloquear.
finalmente
{
output.CompleteAdding();
}
Isso permite que a instância BlockingCollection , Buffer1, saiba que não há mais itens a serem
colocados na fila. Portanto, quando o Stage2 tiver esvaziado a fila após o Stage1 ter chamado
esse método, ele não será bloqueado, mas continuará até a conclusão. Antes da chamada do
método CompleteAdding , o Stage2 bloqueará se o Buffer1 estiver vazio, esperando até que mais
itens sejam colocados na fila. É por isso que uma instância BlockingCollection foi desenvolvida para
aplicativos Pipeline e produtor-consumidor. Ele fornece o mecanismo perfeito para essa funcionalidade.
TaskCreationOptions.LongRunning
[ 253 ]
www.it-ebooks.info
Machine Translated by Google
Isso informa ao pool de encadeamentos que essas tarefas podem ser executadas por um longo
período de tempo e podem, ocasionalmente, bloquear a espera em suas filas. Dessa forma, o threadpool
pode decidir como gerenciar melhor os threads alocados para essas tarefas.
Agora, vamos ver o código em Stage2 — o método StageWorker . Precisamos de uma maneira de
remover itens de maneira enumerável para que possamos iterar os itens das filas com um loop foreach
porque não sabemos quantos itens esperar. Além disso, como os objetos BlockingCollection oferecem
suporte a vários consumidores, precisamos de uma maneira de remover itens que nenhum outro consumidor
possa remover. Usamos este método da classe BlockingCollection :
Isso permite que vários consumidores removam itens de uma instância BlockingCollection enquanto
mantêm a ordem dos itens. Para melhorar ainda mais o desempenho desse aplicativo (supondo que
tenhamos núcleos de processamento suficientes disponíveis), poderíamos criar uma quarta tarefa que
também executa o método StageWorker . Então, teríamos dois estágios e duas tarefas em execução. Isso
pode ser útil se houver núcleos de processamento suficientes e o estágio 1 for executado mais rápido que
o estágio 2. Se isso acontecer, ele preencherá continuamente a fila e bloqueará até que haja espaço
disponível. Mas se executarmos várias tarefas do estágio 2, seremos capazes de acompanhar o estágio 1.
Isso diz ao nosso manipulador de botão para esperar até que todas as tarefas sejam concluídas. Depois de
chamarmos o método CompleteAdding em cada instância BlockingCollection e os buffers forem esvaziados,
todos os nossos estágios serão concluídos e o TaskFactory.
O comando WaitAll será atendido e este método no thread da interface do usuário pode concluir seu
processamento, que neste aplicativo é atualizar a interface do usuário e os arquivos de dados:
{
while (inputfile.Peek() >= 0) {
[ 254 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 9
{
while (inputfile.Peek() >= 0) {
{
while (inputfile.Peek() >= 0) {
Em seguida, experimente corridas mais longas, estágios mais complexos e vários estágios de
consumo. Além disso, tente percorrer o aplicativo com o depurador do Visual Studio usando
as técnicas que aprendemos no Capítulo 8, Depurando aplicativos multithread com o Visual
Studio. Certifique-se de entender a interação entre os estágios e os buffers.
Os blocos de mensagens da biblioteca Dataflow são muito úteis para padrões de design,
como Pipeline e produtor-consumidor, nos quais você tem vários produtores produzindo
dados que podem ser consumidos por vários consumidores. Os dois que veremos são
BufferBlock e ActionBlock.
[ 255 ]
www.it-ebooks.info
Machine Translated by Google
BufferBlock O objeto
[ 256 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 9
A biblioteca TPL DatafLow fornece interfaces para três tipos de objetos — blocos de origem,
blocos de destino e blocos propagadores. BufferBlock é um bloco de mensagem de uso geral
que pode atuar como um buffer de mensagem de origem e de destino, o que o torna perfeito
para um projeto de aplicativo produtor-consumidor. Para atuar como origem e destino, ele
implementa duas interfaces definidas pela biblioteca TPL Dataflow — ISourceBlock<TOutput>
e ITargetBlock<TOutput>. Portanto, na aplicação que desenvolveremos na seção padrão de
projeto produtor-consumidor deste capítulo, você verá que o método produtor implementa
BufferBlock usando a interface ITargetBlock e o consumidor implementa BufferBlock com a interface
ISourceBlock .
Este será o mesmo objeto BufferBlock no qual eles atuarão, mas ao definir seus objetos locais com
uma interface diferente, haverá diferentes métodos disponíveis para uso. O método produtor terá
os métodos Post e Complete , e o método consumidor usará os métodos OutputAvailableAsync e
Receive .
O objeto BufferBlock tem apenas duas propriedades, a saber, Count, que é uma contagem do
número de mensagens de dados na fila, e Completion, que obtém uma tarefa que é uma operação
assíncrona e a conclusão do bloco de mensagens.
Referenciado em http://msdn.microsoft.com/en-us/library/hh160414(v=vs.110).aspx
[ 257 ]
www.it-ebooks.info
Machine Translated by Google
Aqui está uma lista dos métodos de extensão fornecidos pelas interfaces que ele implementa:
Referenciado em http://msdn.microsoft.com/en-us/library/hh160414(v=vs.110).aspx
Referenciado em http://msdn.microsoft.com/en-us/library/hh160414(v=vs.110).aspx
Portanto, como você pode ver, essas interfaces facilitam muito o uso do objeto BufferBlock
como uma fila de uso geral entre os estágios de um pipeline. Essa técnica também é útil entre produtores
e consumidores em um padrão de design produtor-consumidor.
[ 258 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 9
ActionBlock Outro
objeto muito útil na biblioteca do Dataflow é o ActionBlock. Sua sintaxe é
ActionBlock<TInput>, onde TInput é um objeto Action . ActionBlock é um bloco de destino
que executa um delegado quando uma mensagem de dados é recebida. O seguinte é um
exemplo muito simples de usar um ActionBlock:
ActionBlock<int> action = new ActionBlock<int>(x =>
Console.WriteLine(x));
ação.Post(10);
Neste exemplo, como este é um bloco de destino, precisaríamos chamar o método Complete
para garantir que o bloco de mensagem seja concluído.
Outro método útil do BufferBlock é o método LinkTo . Esse método permite que você
vincule ISourceBlock a ITargetBlock. Assim, você pode ter um BufferBlock que é
implementado como um ISourceBlock e vinculá-lo a um ActionBlock , pois é um ITargetBlock.
Dessa forma, um Action delegate pode ser executado quando um BufferBlock recebe dados.
Isso não desenfileira os dados do bloco de mensagem. Ele apenas permite que você execute
alguma tarefa quando os dados são recebidos no buffer.
ActionBlock tem apenas duas propriedades, a saber, InputCount, que é uma contagem do
número de mensagens de dados na fila, e Completion, que obtém uma tarefa que é uma
operação assíncrona e a conclusão do bloco de mensagens. Possui os seguintes métodos:
Referenciado em http://msdn.microsoft.com/en-us/library/hh194684(v=vs.110).aspx
[ 259 ]
www.it-ebooks.info
Machine Translated by Google
Referenciado em http://msdn.microsoft.com/en-us/library/hh194684(v=vs.110).aspx
Referenciado em http://msdn.microsoft.com/en-us/library/hh194684(v=vs.110).aspx
[ 260 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 9
Agora, vamos dar uma olhada no aplicativo que desenvolvemos anteriormente como um
design de Pipeline e modificá-lo usando a biblioteca Dataflow . Também removeremos um
estágio para que ele tenha apenas dois estágios, um produtor e um consumidor.
coisa que precisamos fazer é abrir o Visual Studio e criar um novo aplicativo de console
chamado ProducerConsumerConsoleApp. Usaremos um aplicativo de console desta vez apenas
para facilitar. Nosso principal objetivo aqui é demonstrar como implementar o padrão de projeto
produtor-consumidor usando a biblioteca TPL Dataflow .
Depois de abrir o Visual Studio e criar o projeto, precisamos executar as seguintes etapas:
3. Agora, vamos adicionar duas strings estáticas que apontarão para nosso arquivo de dados de entrada e
para o arquivo de dados criptografados que geramos: private static String PipelineEncryptFile = @"c:
\projects\ EncryptData.txt";
[ 261 ]
www.it-ebooks.info
Machine Translated by Google
4. Em seguida, vamos adicionar um método estático que atuará como nosso produtor. Este método irá
tem o seguinte código:
// Nosso método Producer.
static void Produtor(ITargetBlock<char> Alvo) {
tentar
}
}
finalmente
{
Target.Complete();
}
char C = Source.Receive();
[ 262 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 9
{
outfile.Write(DisplayData);
}
return DisplayData.Length;
}
6. Em seguida, vamos criar um método auxiliar estático simples para ler nosso arquivo de dados
de entrada e colocá-lo em uma coleção de listas caractere por caractere. Isso nos dará uma
lista de caracteres para nosso produtor usar. O código neste método ficará assim:
public static List<char> GetData(String PipelineInputFile) {
Data.Add((char)inputfile.Read());
}
dados de retorno;
}
[ 263 ]
www.it-ebooks.info
Machine Translated by Google
int i = (int)C; i = i + 1; C
= Convert.ToChar(i);
retornar C;
}
8. Em seguida, precisamos adicionar o código do nosso método Main . Este método irá
iniciar nossas tarefas de consumidor e produtor. Então, quando eles concluírem o
processamento, ele exibirá os resultados no console. O código para este método se parece
com isto:
Console.Write((char)inputfile.Read());
}
[ 264 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 9
Console.Write((char)encryptfile.Read());
}
9. Esse é todo o código necessário. Agora, vamos construir e executar o aplicativo usando
o seguinte arquivo de dados de entrada:
[ 265 ]
www.it-ebooks.info
Machine Translated by Google
Agora, tente isso com seus próprios arquivos de dados e entradas. Vamos examinar o que aconteceu e como
isso funciona.
Como funciona
Primeiro vamos passar pelo método Main . A primeira coisa que Main faz é criar um objeto BufferBlock
chamado buffer. Isso será usado como a fila de itens entre nosso produtor e consumidor. Este BufferBlock é definido
para aceitar tipos de dados de caracteres.
Além disso, observe que quando esse objeto de buffer vai para a tarefa do consumidor, ele é convertido
como ISourceBlock. Observe o cabeçalho do método do nosso consumidor:
Em seguida, nosso método Main inicia nossa tarefa de produtor usando o seguinte comando:
Produtor(buffer);
[ 266 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 9
Em seguida, esperamos até que nossa tarefa de consumidor termine, usando este comando:
consumidor.Wait();
Então, agora nosso método Main apenas espera. Seu trabalho está feito por enquanto. Ele iniciou as tarefas de produtor
e consumidor. Agora nosso consumidor está esperando que os itens apareçam em seu BufferBlock para poder processá-
los. O consumidor permanecerá no loop a seguir até que todos os itens sejam removidos do bloco de mensagem e o
bloco seja concluído, o que é feito por alguém chamando seu método Complete :
Portanto, agora nossa tarefa consumidor fará um loop de forma assíncrona, removendo itens da fila de
mensagens conforme eles aparecem. Ele usa o seguinte comando no loop while para fazer isso:
await Source.OutputAvailableAsync())
Da mesma forma, outras tarefas do consumidor podem ser executadas ao mesmo tempo e fazer a mesma coisa.
Se o produtor estiver adicionando itens ao bloco mais rapidamente do que o consumidor pode processá-los, adicionar
outro consumidor melhorará o desempenho. Assim que um item estiver disponível, o consumidor chama o seguinte
comando para obter o item do buffer:
char C = Source.Receive();
Como o buffer contém itens do tipo caractere, colocamos o item recebido em um valor de caractere. Em seguida,
o consumidor o processa criptografando o caractere e anexando-o à nossa string de exibição:
Agora, vamos olhar para o consumidor. O consumidor primeiro obtém seus dados chamando o seguinte
comando:
GetData(PipelineInputFile)
[ 267 ]
www.it-ebooks.info
Machine Translated by Google
Esse método retorna uma coleção List de caracteres que possui um item para cada caractere no
arquivo de dados de entrada. Agora o produtor itera através da coleção e usa o seguinte comando para
colocar cada item no bloco de buffer:
Target.Post(C);
Além disso, observe no cabeçalho do método para nosso consumidor que lançamos nosso buffer como um
Tipo de ITargetBlock :
Depois que o produtor termina de processar os caracteres e adicioná-los ao buffer, ele fecha oficialmente
o objeto BufferBlock usando este comando:
Target.Complete();
Isso é tudo para o produtor e consumidor. Depois que o método Main termina de esperar que o consumidor
termine, ele usa o seguinte código para gravar o número de caracteres processados, os dados de entrada e
os dados criptografados:
{
while (inputfile.Peek() >= 0) {
Console.Write((char)inputfile.Read());
}
[ 268 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 9
Console.Write((char)encryptfile.Read());
}
Além disso, tente converter nosso aplicativo Pipeline original do início do capítulo em um aplicativo
produtor-consumidor TPL Dataflow com dois conjuntos de produtores e consumidores. O primeiro
atuará como estágio 1 e estágio 2, e o segundo atuará como estágio 2 e estágio 3. Assim, com
efeito, o estágio 2 será tanto um consumidor quanto um produtor.
Resumo
Cobrimos muito neste capítulo. Aprendemos os benefícios e como implementar um padrão
de projeto Pipeline e um padrão de projeto produtor-consumidor.
Como vimos, ambos são padrões de design muito úteis ao criar aplicativos paralelos e
simultâneos que requerem vários processos assíncronos de dados
entre tarefas.
No projeto Pipeline, podemos executar várias tarefas ou estágios simultaneamente, mesmo que
os estágios dependam de dados sendo processados e gerados por outros estágios. Isso é muito
útil para o desempenho, pois todas as funcionalidades não precisam esperar em cada estágio para
concluir o processamento de cada item de dados. Em nosso exemplo, podemos começar a
descriptografar caracteres de dados enquanto um estágio anterior ainda está criptografando dados
e colocando-os na fila.
[ 269 ]
www.it-ebooks.info
Machine Translated by Google
A maioria dos aplicativos que se beneficiam do paralelismo será favorável a alguma variação
de um padrão produtor-consumidor ou Pipeline. Além disso, os objetos de bloco de
mensagem BlockingCollection e Dataflow são mecanismos úteis para coordenar dados entre
tarefas paralelas, independentemente do padrão de design usado no aplicativo. Será muito útil
se familiarizar com essas classes de mensagens e filas.
[ 270 ]
www.it-ebooks.info
Machine Translated by Google
• Executando um PLINQ
• Ordenando em PLINQ •
Mesclando em PLINQ •
Cancelando um PLINQ •
Compreendendo as melhorias de desempenho em PLINQ
www.it-ebooks.info
Machine Translated by Google
Executando um PLINQ
Agora, vamos dar uma olhada em um PLINQ e sintaxe básicos. Usaremos três métodos diferentes
para executar o PLINQ e, em seguida, examinaremos os resultados. A primeira usará o método ForAll
da classe ParallelQuery . O próximo usará o método AsParallel da biblioteca LINQ na coleção
Enumerable . A forma final usará a sintaxe do método LINQ padrão.
Em seguida, exibiremos os resultados das três consultas em três caixas de listagem e também
exibiremos o tempo necessário para executar cada consulta.
começar abrindo o Visual Studio e criando um novo projeto de aplicativo WPF chamado WpfPLINQQuery.
Uma vez aberto este projeto, vamos realizar os seguintes passos:
// Inicia o cronômetro.
Cronômetro sw1 = new Cronômetro(); sw1.Start();
lb1.Items.Add(i);
}
[ 272 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 10
//Para o cronômetro.
sw1.Stop();
tbTime1.Text = sw1.ElapsedMilliseconds.ToString();
// Inicia o cronômetro.
Cronômetro sw2 = new Cronômetro(); sw2.Start();
lb2.Items.Add(i);
}
//Para o cronômetro.
sw2.Stop();
tbTime2.Text = sw2.ElapsedMilliseconds.ToString();
}
// Inicia o cronômetro.
Cronômetro sw3 = new Cronômetro(); sw3.Start();
[ 273 ]
www.it-ebooks.info
Machine Translated by Google
lb3.Items.Add(i);
}
//Para o cronômetro.
sw3.Stop();
tbTime3.Text = sw3.ElapsedMilliseconds.ToString();
5. Em seguida, adicione três caixas de texto e nomeie-as como tbTime1, tbTime2 e tbTime3.
Além disso, limpe suas propriedades de texto .
6. No arquivo MainWindow.xaml.cs , precisaremos adicionar uma instrução using para que a classe Stopwatch
esteja disponível para nós. Portanto, adicione a seguinte instrução na parte superior do arquivo com o
restante das instruções using : using System.Diagnostics;
7. Em seguida, precisamos adicionar um método vazio para servir como nosso delegado para a expressão
lambda. Vamos chamá-lo de DoWork. Adicione-o ao final da classe MainWindow com o seguinte
código:
static void DoWork(int i)
{}
apresentação"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Janela Principal" Altura="759.12" Largura="725.431">
<Grade>
<ListBox x:Name="lb1" HorizontalAlignment="Esquerda"
Height="557" Margin="38,78,0,0" VerticalAlignment="Top"
Largura="150"/>
<ListBox x:Name="lb2" HorizontalAlignment="Esquerda"
Height="557" Margin="285,78,0,0" VerticalAlignment="Top"
Largura="150"/>
[ 274 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 10
</Grid>
</Janela>
Agora nosso projeto está concluído. Vamos construir e executar o projeto. O programa deve
ficar assim quando for executado:
[ 275 ]
www.it-ebooks.info
Machine Translated by Google
Agora, vamos clicar nos próximos dois botões, Executar Método 2 e Executar Método
3 e ver como fica o programa:
[ 276 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 10
Como você pode ver, executamos a consulta paralela PLINQ usando três métodos diferentes e os
resultados são diferentes a cada vez. A primeira coisa que você notará se percorrer os resultados é que os
números na caixa de listagem não estão ordenados. A próxima coisa que você notará é que o tempo de
execução de cada um não é exatamente o mesmo. Concedido, o primeiro levará mais tempo para ser
exibido porque dividimos por 5 em vez de 10, então há mais resultados. Mas a consulta em si deve levar
aproximadamente o mesmo tempo. Você pode brincar com isso movendo o comando para parar o objeto
Cronômetro .
[ 277 ]
www.it-ebooks.info
Machine Translated by Google
Como funciona
Neste projeto, realizamos uma consulta paralela PLINQ usando três métodos diferentes.
Na primeira, definimos um objeto ParallelQuery usando esta linha de código, mas ainda não
o executamos:
Isso diz ao .NET para tentar executar esta consulta simultaneamente se determinar que
melhorará o desempenho. Podemos forçar o .NET a operar essa consulta simultaneamente
usando o método de extensão paralela WithExecutionMode(ParallelExecutionMode.
ForçaParalelismo).
Para usar este comando, precisamos passar uma expressão lambda. Estamos apenas
usando uma expressão lambda que passa um delegado para um método que não faz nada.
Não precisamos trabalhar no método delegado porque estamos fazendo todo o trabalho na
consulta paralela.
No segundo método, criamos uma consulta paralela que é executada quando o comando
é processado e retorna um array. Este comando é o seguinte:
int[] PQ2 = (de num na coleção2.AsParallel()
onde num % 10 == 0
selecione num).ToArray();
[ 278 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 10
No método final, usamos a sintaxe do método LINQ. Aqui está a instrução que executa a consulta
usando esta sintaxe:
Além disso, observe que em todos os três métodos os resultados não são retornados em ordem.
Veremos mais adiante no capítulo como garantir a ordem dos resultados se isso for necessário.
Por enquanto, estamos apenas tentando executar uma consulta paralela. Vamos deixar o .NET
determinar como executar a consulta (simultaneamente ou sequencialmente) e em qualquer ordem.
Pedidos em PLINQ
Agora, e se a ordem dos nossos resultados for importante? Em muitos casos, queremos que os
resultados voltem em uma determinada ordem. Vamos examinar como podemos fazer isso.
Existem vários métodos contidos nos métodos de extensão paralela da interface IEnumerable .
Os dois que mais usamos são AsOrdered() e AsOrderedBy(). Esses dois métodos garantem a
preservação da ordem nos resultados da consulta paralela. Isso adiciona algum custo e
sobrecarga.
Vamos pegar nosso projeto da última seção e atualizá-lo para retornar os resultados ordenados.
2. Agora, faça uma alteração semelhante à instrução no método btnMethod2_Click . Faça com
que sua instrução de consulta paralela se pareça com o seguinte:
int[] PQ2 = (de num na coleção2.AsParallel().AsOrdered()
onde num % 10 == 0
selecione num).ToArray();
[ 279 ]
www.it-ebooks.info
Machine Translated by Google
3. Por fim, vamos fazer uma alteração semelhante no método btn3Method_Click e em sua instrução
de consulta:
Essas são todas as mudanças; parece bastante simples. Agora, vamos construir e executar
nosso aplicativo. Você deve obter um programa que se parece com a seguinte captura de tela:
Agora, vamos clicar em cada um dos três botões para executar todas as três consultas.
Seu programa agora deve se parecer com a seguinte captura de tela:
[ 280 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 10
Como você pode ver, os resultados agora estão ordenados. Este é um exemplo simples; no entanto,
em um exemplo mais complexo, você também verá que os tempos de execução são mais longos.
Como seria de esperar, ordenar os resultados retardará o processamento da consulta.
Além disso, você notou que se você executar as consultas no aplicativo em uma ordem diferente,
verá que normalmente a primeira leva mais tempo do que a última? Existem duas explicações para
o fato de o último a ser executado geralmente ser mais rápido. O .NET executou a consulta, portanto,
também levou tempo para determinar o melhor método de execução. Além disso, se ele optar por
executar a consulta simultaneamente, já assumiu a sobrecarga de criar as tarefas no threadpool.
[ 281 ]
www.it-ebooks.info
Machine Translated by Google
Como funciona
A mudança do primeiro projeto para este é simples. Acabamos de adicionar o método
AsOrdered() ao método de extensão paralela, AsParallel(), que executamos na coleção
IEnumerable .
Isso força o .NET a ordenar os resultados. Também podemos ordenar os resultados com a sintaxe
de consulta OrderBy . Deve-se observar que OrderBy é feito sequencialmente, não em paralelo.
A captura de tela a seguir explica os itens da própria consulta que ordenarão os resultados:
Referenciado em http://msdn.microsoft.com/en-us/library/dd460677(v=vs.110).aspx
Mesclando em PLINQ
Aprendemos como realizar uma consulta paralela e como ordenar os resultados.
Agora, vamos examinar a fusão com nossas consultas paralelas.
[ 282 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 10
Quando uma consulta paralela é executada, o .NET CLR particiona a coleção de origem para que várias
tarefas possam trabalhar seções da coleção simultaneamente. Depois de concluído, os resultados
precisam ser mesclados em uma coleção de resultados para processamento.
Dependendo dos operadores de consulta usados, os resultados podem ser mesclados de diferentes
maneiras. Os operadores de consulta que ditam uma nova ordem na coleção de resultados irão armazenar
em buffer todos os itens dos threads separados antes de mesclá-los novamente. Outros operadores de
consulta são parcialmente armazenados em buffer, enquanto o operador de consulta, ForAll<TSource>, não
é armazenado em buffer. Ele produz todos os itens de todas as tarefas assim que são processados.
Se uma consulta não puder executar a opção de mesclagem especificada, o .NET irá ignorá-la. Assim, você
pode ver que o .NET trata as opções de mesclagem como sugestões e não lançará um erro se a opção de
mesclagem não for compatível; ele simplesmente irá ignorá-lo. Da mesma forma, se você não especificar uma
opção de mesclagem, o .NET selecionará uma para você. Portanto, você pode usar uma opção de mesclagem
se tiver determinado que uma mesclagem específica é melhor para o seu desempenho ou apenas deixar que
o .NET decida por você. Esta é uma das coisas bonitas do TPL. Ele lida com muito do pensamento para você
quando se trata de problemas simultâneos comuns.
As diferentes opções com as quais a mesclagem paralela pode ser configurada estão listadas na captura
de tela a seguir:
Referenciado em http://msdn.microsoft.com/en-us/library/dd997424(v=vs.110).aspx
[ 283 ]
www.it-ebooks.info
Machine Translated by Google
Referenciado em http://msdn.microsoft.com/en-us/library/dd997424(v=vs.110).aspx
Para fazer isso, vamos abrir nosso projeto PLINQQuery e alterar a instrução de consulta paralela
no método btnMethod1_Click para ficar assim:
[ 284 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 10
[ 285 ]
www.it-ebooks.info
Machine Translated by Google
Agora, vamos tentar usar o método AutoBuffered . Para fazer isso, no método
btnMethod1_ Click , altere a instrução de consulta paralela para o seguinte:
ParallelQuery<int> PQ1 = de num na coleção1.AsParallel().
AsOrdered().WithMergeOptions(ParallelMergeOptions.AutoBuffered)
onde num % 5 == 0
selecione num;
[ 286 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 10
Agora, vamos fazer uma alteração final na consulta paralela neste método para usar a
opção AutoBuffered . Altere o método btnMethod1_click para ter a seguinte instrução de
consulta paralela:
Agora, vamos construir e executar este aplicativo uma última vez e clicar no botão
Execute Method 1 . Os resultados devem se parecer com a captura de tela a seguir:
[ 287 ]
www.it-ebooks.info
Machine Translated by Google
Como funciona
Como você pode ver, ditar ao .NET como mesclar os resultados de uma consulta em buffer é tão fácil
quanto chamar o método de extensão paralela WithMergeOptions() ; assim como ordenar os resultados é
tão fácil quanto chamar o método de extensão paralela AsOrdered() .
Mas você também notará que os melhores resultados vêm do uso da opção AutoBuffered ou sem
mesclagem. Embora o PLINQ forneça essas opções para ditar como a consulta paralela é mesclada, se
deixarmos o .NET determinar como fazer isso, os resultados geralmente serão muito melhores.
Para exemplos complexos, pode ser benéfico dizer ao .NET como mesclar os resultados, mas geralmente
é melhor deixar o CLR determinar isso sozinho. Esse é um dos benefícios de usar o PLINQ e o TPL —
muito do trabalho complexo e do raciocínio são feitos para você. Você pode concentrar seu tempo de
codificação na funcionalidade e não no desempenho.
Cancelando um PLINQ
O cancelamento de uma consulta paralela é muito semelhante ao cancelamento de uma tarefa usando
TPL. Primeiro, criamos um token de cancelamento e, em seguida, emitimos uma solicitação de
cancelamento para o token de cancelamento. Isso criará uma exceção de cancelamento de operação.
Então, quando executamos nossa consulta paralela, adicionamos o método de extensão paralela
WithCancellation() e passamos o token de cancelamento. Então, finalmente, capturamos a solicitação de
cancelamento da operação e a processamos.
A estrutura .NET não passa uma única OperationCanceledException para uma System.AggregateException;
a OperationCanceledException deve ser processada em um bloco catch separado .
1. Primeiro, adicione um novo controle de botão ao nosso arquivo MainWindow.xaml.cs e defina sua
propriedade Content como Cancel e nomeie o controle como btnCancel.
2. Em seguida, precisamos adicionar uma instrução using para o namespace Threading para que
possamos criar um objeto de token de cancelamento. Adicione a seguinte instrução using :
usando System.Threading;
[ 288 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 10
3. Em seguida, vamos criar um token de cancelamento na parte superior de nossa classe MainWindow com
a seguinte declaração:
CancellationTokenSource cs = novo
CancellationTokenSource();
4. Agora, vamos adicionar um método manipulador de evento de clique btnCancel_Click para nosso
Botão Cancelar . Deverá ter o seguinte código:
5. Além disso, vamos alterar nossa coleção IEnumerable para ter 500.000 itens, para que ela seja executada
por mais tempo e nos dê a chance de cancelar a operação:
IEnumerable<int> coleção1 = Enumerable.Range(10,
500000);
tentar
{
// Método 2 - Use um método TOArray padrão
para //retornar os resultados.
PQ2 = (de num na coleção2.AsParallel().AsOrdered().
WithCancelamento(cs.Token)
onde num % 10 == 0
selecione num).ToArray();
lb2.Items.Clear();
lb2.Items.Add(ex.Message);
retornar;
}
[ 289 ]
www.it-ebooks.info
Machine Translated by Google
Agora, vamos construir e executar nosso programa. Clique no botão Executar Método 2 e
imediatamente clique no botão Cancelar . Você deve ver a seguinte saída em seu aplicativo:
Você pode ver que cancelamos a consulta paralela antes de sua conclusão.
Agora, vamos agora dar uma olhada em como foi realizado.
Como funciona
Nós realizamos isso de forma muito simples. Primeiro, criamos um token de cancelamento para usar
com este comando:
[ 290 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 10
tentar
{
// Método 2 - Use um método TOArray padrão para retornar
os resultados.
PQ2 = (de num em collection2.AsParallel().
AsOrdered().WithCancellation(cs.Token)
onde num % 10 == 0
selecione num).ToArray();
lb2.Items.Clear();
lb2.Items.Add(ex.Message); retornar;
Isso é tudo.
Compreendendo as melhorias
de desempenho no PLINQ
Vimos como podemos implementar consultas PLINQ e especificar opções de ordenação,
mesclagem e execução paralela. Também vimos como executar um PLINQ com o método
ForAll() e como um loop foreach . Todos esses fatores influenciam no desempenho da
consulta. Também é importante examinar como o .NET decide particionar uma coleção de
dados de origem quando decide executar um PLINQ em paralelo. Lembre-se, um PLINQ
executado em paralelo é apenas um LINQ onde uma coleta de dados é particionada em
grupos e uma tarefa é criada para processar a ação Where da consulta em cada partição de
dados. Os diagramas a seguir descrevem as diferenças entre o processamento LINQ e o
processamento PLINQ.
[ 291 ]
www.it-ebooks.info
Machine Translated by Google
LINQ
Dados
Coleção
PLINQ
Partição 1 Onde
Tarefa Cláusula
1
Partição 2 Onde
Tarefa Cláusula
2
Resultados
Partição 3 Onde
Tarefa Cláusula
3
Dados
Coleção
Partição 4 Onde
Tarefa Cláusula
4
Além disso, quando realizamos uma consulta usando um loop ForAll versus foreach ,
o processamento é diferente. Podemos determinar quais destes são os melhores
métodos, geralmente descobrindo se a ordem dos resultados importa ou não. Se a ordem
dos resultados for importante, foreach é o melhor método de processamento porque os
resultados são mesclados após os processos de loop. Usando o loop ForAll , toda a
consulta é processada em cada partição de dados individualmente para que não haja
mesclagem dos resultados na ordem correta. Você pode colocar o método AsOrdered na
coleção IEnumerable para a consulta, mas basicamente armazena em buffer todos os
resultados para mesclá-los, para que você perca os ganhos de desempenho que o loop
ForAll oferece. Os diagramas a seguir descrevem as diferenças na forma como cada um deles é processado.
[ 292 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 10
Processamento Simultâneo
Dados
Coleção
Simultâneo Sequencial
Em processamento Em processamento
Tarefa 1
Partição de processo 1
Tarefa 2
Partição de processo 2
para cada
mesclar
Resultados Tarefa(n)
Tarefa 3
Partição de processo 3
Dados
Coleção
Tarefa 4
Partição de processo 4
[ 293 ]
www.it-ebooks.info
Machine Translated by Google
Agora, vamos dar uma olhada nos algoritmos que o PLINQ (ou mais apropriadamente o .NET
CLR) usa para particionar uma coleção de dados em grupos para processamento paralelo.
O .NET usa quatro algoritmos diferentes para particionar dados com base na coleção e na consulta.
A captura de tela a seguir detalha os dois primeiros algoritmos:
Referenciado em http://blogs.msdn.com/b/pfxteam/archive/2009/05/28/9648672.aspx
[ 294 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 10
Referenciado em http://blogs.msdn.com/b/pfxteam/archive/2009/05/28/9648672.aspx
Como você pode ver na captura de tela anterior, o PLINQ particiona a coleta de dados de
origem com base no tipo de dados e no tipo de consulta. Tudo isso faz parte da lógica que o .NET
executa, então não precisamos fazer isso. Assim como determinar se uma consulta deve ser
executada simultaneamente ou sequencialmente, também determina se a concorrência é a melhor
maneira de particionar os dados.
Como também vimos, dependendo se você precisa ou não dos resultados ordenados, uma
instrução ForAll ou foreach pode gerar um melhor desempenho.
[ 295 ]
www.it-ebooks.info
Machine Translated by Google
Resumo
Neste capítulo, você aprendeu como transformar uma consulta LINQ to Objects comum em uma
PLINQ simultânea. Você também aprendeu como ordenar resultados, mesclar resultados e cancelar
consultas. O PLINQ facilita muito o processamento simultâneo de consultas em qualquer coleção de
dados de memória que ofereça suporte a IEnumerable.
Também é importante lembrar que o PLINQ oferece suporte apenas a LINQ to Objects e não a outras
formas de LINQ, como LINQ to SQL ou LINQ to XML.
O PLINQ é tão fácil de implementar quanto adicionar um método de extensão paralela a uma
coleção de dados de origem em uma consulta LINQ — é realmente simples assim. Em seguida,
o .NET pode determinar se a consulta terá melhor desempenho em execução simultânea,
particionando a coleta de dados em partes e, em caso afirmativo, como particioná-la. Você como
programador não precisa se preocupar com nada disso através do seu código-fonte. PLINQ é
realmente um acéfalo ao executar uma consulta LINQ porque se .NET não puder melhorar o
desempenho tornando a consulta simultânea, ele apenas executará a consulta sequencialmente.
[ 296 ]
www.it-ebooks.info
Machine Translated by Google
O Assíncrono
Modelo de Programação
Neste capítulo, aprenderemos sobre as novas palavras-chave async e await fornecidas
no .NET 4.5, junto com o Modelo de Programação Assíncrona (APM).
As palavras-chave async e await são o método mais recente do .NET para simplificar a
programação assíncrona e multithread para o desenvolvedor. Essas novas palavras-chave
tornam o uso do componente BackgroundWorker mais difícil de justificar. A palavra-chave async
facilita a transformação de um método comum em um método assíncrono executado em um
thread separado. Então você pode continuar processando no thread principal.
Quando estiver pronto para aguardar os resultados do método assíncrono, você poderá
usar a palavra-chave await em seu thread principal para bloquear até que o método retorne.
www.it-ebooks.info
Machine Translated by Google
Vamos começar examinando o Modelo de programação assíncrona e como usar esse padrão de
design para executar um método em um thread separado do thread principal. Em seguida, faremos
com que o thread principal aguarde seus resultados.
Introdução ao Assíncrono
Modelo de Programação
O modelo de programação assíncrona é usado pelas classes .NET para implementar
projetos assíncronos. Um exemplo são os métodos BeginRead e EndRead de FileStream.
Isso permite que a classe FileStream implemente uma leitura de arquivo assíncrona. As
classes StreamReader e StreamWriter também possuem métodos assíncronos. Eles
implementam essa funcionalidade usando a interface IAsyncResult .
Em suas classes de clientes, você também pode implementar essa interface para permitir a
funcionalidade assíncrona.
A convenção de nomenclatura ao usar essa interface é prefixar seus nomes de método com
Begin e End. Portanto, você nomearia um BeginMyMethod e o outro EndMyMethod. O primeiro
método é aquele que você executa de forma assíncrona. O segundo método é o que você chama
para bloquear seu thread principal quando deseja aguardar o término do método Begin e fazer
com que ele também retorne os resultados.
[ 298 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 11
1. Adicione duas instruções using para as classes IO e Threading , conforme mostrado aqui:
usando System.Threading; usando
System.IO;
FS.Close();
[ 299 ]
www.it-ebooks.info
Machine Translated by Google
Console.ReadLine();
}
Agora, vamos construir e executar nosso aplicativo. Você deve ver uma tela como esta:
O programa está esperando que o usuário clique no botão voltar; assim que o botão for clicado,
ele executará a leitura assíncrona do arquivo de dados. Depois de clicar em retornar, você verá
os seguintes resultados:
[ 300 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 11
Se você clicar em voltar novamente, o aplicativo será concluído e encerrado. Então, vamos ver como
isso funciona.
exemplo muito simples de uma classe .NET existente, FileStream, que implementa o modelo
de programação assíncrona com os dois métodos, BeginRead e EndRead. Se você observar a
definição do método BeginRead , verá que ele implementa a interface IAsyncResult :
AsyncCallback userCallback,
Estado do objetoObjeto
)
[ 301 ]
www.it-ebooks.info
Machine Translated by Google
Você também pode visualizar a definição do método para o método EndRead da seguinte maneira:
Agora, vamos ver seu projeto de exemplo. Nas linhas de código a seguir, configuramos o objeto FileStream
chamado FS:
Aqui estamos declarando um novo objeto FileStream com parâmetros que definem o arquivo a ser
transmitido, o modo de abertura, o acesso de leitura e a opção de leitura assíncrona.
Em seguida, fazemos algum outro trabalho enquanto a operação de leitura está ocorrendo de forma
assíncrona em outro thread. Esse "outro" trabalho é representado pelos três consoles a seguir.
Instruções WriteLine :
Agora, bloqueamos o thread principal e esperamos que a gravação assíncrona seja concluída usando
esta instrução:
//Chamar EndRead bloqueará o thread principal até que o trabalho assíncrono seja
concluído. int num = FS.EndRead(resultado);
Essa instrução será concluída quando a leitura for concluída e retornará o objeto IAsyncResult e o número
de bytes lidos. Em seguida, gravamos esses resultados no console usando as seguintes instruções:
[ 302 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 11
Examinamos o método IsCompleted de IAsyncResult para garantir que a leitura foi concluída,
embora saibamos que foi porque bloqueamos a espera no método EndRead . Fizemos isso
para visualizar outro padrão de projeto para o Modelo de programação assíncrona, que não
demonstraremos neste capítulo — o método de votação. Em vez de chamar o método EndRead
para bloquear o thread principal aguardando a conclusão do método Main , podemos criar um
loop e verificar periodicamente a propriedade IsCompleted do objeto IAsyncResult retornado
pelo método BeginRead . Isso é útil se quisermos mostrar uma atualização de progresso
durante a operação de leitura.
Usando esse método, não precisamos bloquear a espera do thread principal ou executar o
trabalho de sondagem da propriedade IsCompleted para ver quando a leitura foi concluída.
Simplesmente executamos o método BeginRead e passamos a ele um método delegado. Em
seguida, seguimos nosso caminho e, quando a leitura for concluída, o método delegado será executado.
AsyncCallback userCallback,
Estado do objetoObjeto
)
Você verá que o quarto parâmetro passado para esse método é um delegado AsyncCallback .
Em nosso exemplo anterior, passamos um valor nulo para este parâmetro quando chamamos
este método. Desta vez, vamos usar um método delegado.
[ 303 ]
www.it-ebooks.info
Machine Translated by Google
Console.ReadLine();
}
3. Antes do método Main , precisamos declarar um array de bytes estáticos para que
esteja disponível tanto para o método Main quanto para nosso novo método delegado.
Adicione a seguinte instrução antes do método Main .
byte estático privado[] FileData = novo byte[1000];
[ 304 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 11
4. Em seguida, precisamos adicionar nosso método delegado. Adicione o seguinte código para nosso método
delegado: private static void ReadComplete(IAsyncResult AResult)
{
// Escreva o id da thread que está // executando
a lida.
Console.WriteLine("A operação de leitura está sendo executada no ID do encadeamento:
{0}.",
Thread.CurrentThread.ManagedThreadId);
Console.WriteLine(BitConverter.ToString(FileData));
}
Esse é todo o código que precisaremos para este exemplo. Ainda precisamos de nosso
InputData. txt em C:\projects. Ele ainda deve estar lá desde quando o criamos no último
exercício.
[ 305 ]
www.it-ebooks.info
Machine Translated by Google
Você verá que os resultados parecem quase idênticos aos resultados do exercício
anterior. Isto é verdade. Mas como os alcançamos é muito diferente. Desta vez, em vez
de bloquear o thread principal que está aguardando a conclusão da leitura, iniciamos
um método delegado em um thread separado e esperamos lá a conclusão da leitura e,
em seguida, exibimos os resultados. Vejamos como o fizemos.
[ 306 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 11
Como funciona
Primeiro, vamos ver nosso método Main . Desta vez, é mais simplificado e faz menos
trabalho. Basicamente, ele apenas cria o objeto FileStream , chama o método BeginRead e
passa a ele um método delegado. É isso. Em seguida, ele cuida de seus negócios.
Ele não espera a conclusão da leitura, não processa os resultados e não fecha o objeto
FileStream . O código a seguir mostra isso:
Console.ReadLine();
}
Agora, vamos ver onde o trabalho está sendo feito - o método delegado,
Ler Completo:
[ 307 ]
www.it-ebooks.info
Machine Translated by Google
A primeira coisa que você notará é que a definição do delegado AsyncCallback requer um método que
não retorne nenhum valor e receba um parâmetro IAsyncResult como entrada.
FileStream FS = (FileStream)AResult.AsyncState;
Isso é tudo. Passamos as duas últimas seções deste capítulo analisando três maneiras de implementar
o APM:
• Chame o nome do método Begin , execute alguma ação e, em seguida, bloqueie usando o
Nome do método final até que a ação assíncrona seja concluída
[ 308 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 11
O padrão de design para usar esse método é que seu programa execute um método que usa
a palavra-chave async executando-o em uma tarefa separada. É então livre para continuar. Se
o método assíncrono não tiver um valor de retorno, o thread principal poderá continuar. Se
houver um valor de retorno com o qual o método Main se preocupa ou deseja saber quando a
operação assíncrona foi concluída, ele pode aguardar a conclusão da tarefa assíncrona e exibir
os resultados. Na tarefa assíncrona, o método executará funções em seu próprio thread e, em
seguida, chamará a palavra-chave await quando desejar aguardar a conclusão de alguma ação.
Para demonstrar o uso dessas duas novas palavras-chave, reescreveremos nosso aplicativo
anterior para ler um arquivo de forma assíncrona usando as palavras-chave async e await .
Vamos começar.
[ 309 ]
www.it-ebooks.info
Machine Translated by Google
tarefa.Wait();
[ 310 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 11
// Lê o arquivo especificado.
String DadosLeitura = ""; usando
(leitor StreamReader = novo StreamReader(arquivo)) {
//Desacelera o processo.
System.Threading.Thread.Sleep(10000);
[ 311 ]
www.it-ebooks.info
Machine Translated by Google
Isso é tudo. Vamos construir nosso aplicativo e executá-lo. Devemos ver a seguinte
saída:
Neste ponto, o aplicativo está esperando por nós, então precisamos pressionar Enter.
Então veremos a seguinte saída:
[ 312 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 11
Finalmente, se pressionarmos o retorno uma última vez, o aplicativo será finalizado e encerrado.
Então, como isso tudo funcionou?
examinar como esse programa funciona. Primeiro, dê uma olhada no método Main :
[ 313 ]
www.it-ebooks.info
Machine Translated by Google
tarefa.Wait();
Este é um método simples que cria uma nova tarefa que executa um método assíncrono chamado
ProcessFileAsync. Em seguida, ele inicia a tarefa e espera que ela termine. Isso é tudo. Portanto, o
método Main apenas gera uma tarefa para fazer o trabalho.
// Escreva o id do thread da tarefa que chamará o método assíncrono para ler o arquivo.
[ 314 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 11
O principal objetivo desse método é executar o método assíncrono , ReadFileAsync. Isso é feito
com a seguinte declaração:
A instrução anterior informa ao método para iniciar uma tarefa assíncrona e aguardar sua
conclusão. Como o método que está chamando tem um valor de retorno de Task<String>, a
instrução await task retorna um valor de string. O método ReadFileAsync usa um nome de arquivo
como entrada e, em seguida, retorna uma cadeia de caracteres que contém o conteúdo do arquivo
como uma cadeia de caracteres. Este método lê o arquivo de forma assíncrona em um thread
separado do thread principal.
// Lê o arquivo especificado.
String DadosLeitura = ""; usando
(leitor StreamReader = novo StreamReader(arquivo)) {
//Desacelera o processo.
System.Threading.Thread.Sleep(10000);
[ 315 ]
www.it-ebooks.info
Machine Translated by Google
Aqui, usamos a palavra-chave async para designar isso como um método assíncrono.
Esse método cria um FileStream para o nome de arquivo passado e, em seguida, chama o
método StreamReader.ReadToEndAsync para ler o arquivo de forma assíncrona. Ele usa a palavra-
chave await nesta instrução para sinalizar ao .NET que esta é uma operação assíncrona:
Ao inserir a instrução wait para desacelerar um pouco o processamento, você pode ver o atraso na
execução entre as seguintes instruções de gravação:
Isso permite que você veja que o thread principal está disponível enquanto o arquivo está sendo lido
em um thread separado.
Outro ponto a observar é que, ao usar o método de programação async/await ou o padrão de design
APM, não precisamos lidar com o código subjacente de iniciar, parar e gerenciar threads. Isso é tratado
para nós pelo .NET. Nós apenas temos que escrever a lógica e usar o padrão de projeto apropriado.
Essa é uma maneira genérica de implementar facilmente qualquer operação assíncrona que
pode beneficiar o desempenho de um aplicativo sem ter que projetar todo o aplicativo para paralelismo
ou simultaneidade, como fizemos com os padrões de design produtor-consumidor ou Pipelining.
Podemos simplesmente codificar uma única tarefa para executar de forma assíncrona.
Resumo
Neste capítulo, você aprendeu duas maneiras muito importantes de fazer com que uma
determinada funcionalidade em um aplicativo seja executada de forma assíncrona a partir do thread
principal do aplicativo. Essas duas formas são o Application Programming Model e as palavras-chave
async/await . Essas duas técnicas de design permitem que você aproveite a execução assíncrona de
uma tarefa sem projetar um aplicativo baseado em simultaneidade. Isso é especialmente útil para
tarefas de execução longa ou tarefas que acessam recursos externos. Dessa forma, não amarramos
o thread principal de um aplicativo enquanto esperamos por algo como uma leitura de arquivo, uma
solicitação HTTP GET ou uma leitura de banco de dados. Todas essas são tarefas cuja duração está
fora de nosso controle e não é previsível. Portanto, faz sentido executar esses tipos de funções de
forma assíncrona para que o thread principal de nosso aplicativo possa continuar em execução.
[ 316 ]
www.it-ebooks.info
Machine Translated by Google
Capítulo 11
Essas técnicas são muito úteis quando não temos funcionalidade concorrente suficiente
para projetar um aplicativo paralelo completo, mas temos uma tarefa específica que pode levar
muito tempo. Esses dois métodos são usados da mesma forma que o componente BackgroundWorker
e continuam a torná-lo obsoleto.
O Application Programming Model é uma técnica de design usada quando você cria uma
classe. Você pode criar dois métodos e nomeá-los BeginMethodName e EndMethodName que
implementam a interface IAsyncResult . Em seguida, você pode usá-los para iniciar uma operação
assíncrona e, se necessário, bloquear o thread principal que aguarda a conclusão da operação
assíncrona. Essa técnica é usada em muitas classes .NET, como FileStream, StreamReader e
StreamWriter. Mas também pode ser usado por você ao projetar suas classes.
Agora que você sabe tudo o que o .NET tem a oferecer, nunca desperdice uma oportunidade
em seu desenvolvimento para maximizar o uso de seu hardware de computação ou sua
capacidade de resposta adotando uma abordagem de thread único.
[ 317 ]
www.it-ebooks.info
Machine Translated by Google
www.it-ebooks.info
Machine Translated by Google
Índice
A B
Objeto ActionBlock Componente BackgroundWorker 34-36, 50
usando 259, 260 Cancelamento de encadeamento
Coluna ativa, janela Threads 216 BackgroundWorker 55-57
Coluna Affinity Mask, janela Threads Objeto BufferBlock
216 usando 256-258
Lei de Amdahl 30, 31
Coluna APPDomain, janela de tarefas 219 C
aplicativo sobre 21 criação, com threads 78-85
depuração 223-238 design, com padrão de design retornos de
chamada 50 tokens de
Pipeline 242-255 design, com padrão de design
produtor-consumidor 260-269 cancelamento 174 Coluna de categoria, janela
Threads 216 composições de cores, pixel 112,
113 Common Language Runtime (CLR) 271
componentes, instância de cor (System.Drawing.Color)
brilho 112 matiz 112
CPU-Z
Coluna AsyncState, janela Tarefas 219
Classe AutoResetEvent cerca de 19
URL 19
usado, para lidar com sinais entre threads
126 await palavra-chave operações críticas 9
demonstrando 309-316
www.it-ebooks.info
Machine Translated by Google
D H
hardware
compartilhamento de dados , entre examinando 18-20
threads 86-95 paralelismo de dados, em thread de hardware 14
coleções com o comando Parallel.ForEach 189-197 simultaneidade pesada 71, 128
impasse 214 hyperthreading 13-15
delegado 157, 158
EU
E
Coluna Ícones, janela Tarefas 218
programa de criptografia 73 Coluna ID, janela Tarefas 218
threads de manipulação de Coluna ID, janela Threads 216 aplicativo
erros, usando 131-136 de processamento de imagem usado,
manipulações de espera de para pipelining 107-112 entrada-
evento 126 manipulação de processamento-saída (IPO) 8 barramento
exceções, em loops paralelos 201-206 entre processadores 29
Operações OR (XOR) exclusivas 78 Método IsAlive 113
núcleos de execução 16 Função IsBusy() 67
F eu
[ 320 ]
www.it-ebooks.info
Machine Translated by Google
O PLINQ
sobre 271
pedido, no PLINQ 279-282
cancelando 288-291
Operações do agendador do SO 21,
executando 272-279
22 overclocking 15
conceito de fusão 282-288
conceito de pedido 279-282
P desempenho, melhorando 291-295
classe paralela 154-157 Coluna de prioridade, janela Threads 216
Parallel.ForEach comando procedimentos, para distribuição de tarefas
cerca de 155 usado, para multiprocessamento assimétrico
paralelismo de dados em coleções (AMP ou ASMP) 11
189-197 Parallel Language multiprocessamento simétrico (SMP) 11
Integrated Query. Veja loops processos 21
paralelos PLINQ cancelando Explorador de processos
[ 321 ]
www.it-ebooks.info
Machine Translated by Google
Janela de tópicos
T sobre 213
Coluna ativa 216
tarefa
Coluna de máscara de afinidade 216
cancelando 174-179
Coluna de categoria 216
conclusão da tarefa
Coluna sinalizadora 216
aguardando 162-165
[ 322 ]
www.it-ebooks.info
Machine Translated by Google
coluna ID 216 EM
Coluna de localização 216
Coluna ID gerenciada 216 W5 107
Coluna de nome 216 Método WaitAll 126
Coluna de prioridade 216 Classe WaitHandle
Coluna Nome do Processo 217 usado, para verificar sinais entre threads
Coluna de contagem suspensa 217 126
usando 215 Windows Presentation Foundation (WPF)
TPL 33, 66, 71, 137, 138, 161, 183, 213 cerca de 34, 107, 185, 244
exemplo, com assíncrono
EM BackgroundWorker 44-49
exemplo, com síncrono
máquinas von Neumann 8
BackgroundWorker 49
gargalo de von Neumann 9
[ 323 ]
www.it-ebooks.info
Machine Translated by Google
www.it-ebooks.info
Machine Translated by Google
A Packt é uma editora moderna e exclusiva que se concentra na produção de livros de ponta e qualidade para
comunidades de desenvolvedores, administradores e novatos.
Para obter mais informações, visite nosso site em www.packtpub.com.
Não estamos apenas procurando por autores publicados; se você tem fortes habilidades técnicas, mas não tem experiência
em redação, nossos editores experientes podem ajudá-lo a desenvolver uma carreira de escritor ou simplesmente obter
alguma recompensa adicional por sua experiência.
www.it-ebooks.info
Machine Translated by Google
www.it-ebooks.info
Machine Translated by Google
Multithreading em C#
5.0 Cookbook
ISBN: 978-1-84969-764-4 Brochura: 268 páginas
1. Aprofunde-se na infra-estrutura de
encadeamento .NET e use a Biblioteca Paralela
de Tarefas para programação assíncrona.
www.it-ebooks.info
Machine Translated by Google
www.it-ebooks.info