Você está na página 1de 216

Machine Translated by Google

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:

return ((poPixelColor.GetHue() >= 150) &&


(poPixelColor.GetHue() <= 258) &&
(poPixelColor.GetSaturation() >= 0,10) &&
(poPixelColor.GetBrightness() <= 0,90));

Essa função é chamada para cada pixel no retrato de bitmap infravermelho e retorna um valor
bool .

Pausando e reiniciando threads


É importante, em muitos casos, coordenar e aguardar os encadeamentos. Tomemos, por
exemplo, um projeto de tubulação. Depois de iniciar um grupo de threads concorrentes,
precisamos esperar que todos terminem, agrupar seus resultados e passar para a seção não
concorrente. Precisamos saber quando os threads terminam e quais são seus resultados, para
que possamos determinar como proceder.

Existem várias maneiras de coordenar e aguardar entre as threads. No exemplo a seguir,


mostraremos como você pode verificar se um encadeamento ainda está ativo e processando e,
em seguida, esperar que todos os encadeamentos concluam seu trabalho de processamento de
bitmap antes de continuar o restante do aplicativo e remontar as partes do bitmap.

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

Processamento Avançado de Threads

Como fazer isso


Agora, vamos criar a interface do usuário e fazer algumas alterações para criar vários
threads dinamicamente para processar cada parte de um bitmap. Para isso, devemos
compartilhar dados entre as várias threads, como aprendemos nos capítulos anteriores.
Execute as seguintes etapas:

1. Fique no projeto, OldStarsFinder.


2. Abra o Windows Form, Form1 (frmStarsFinder), no designer de formulários; adicione
os seguintes controles e alinhe-os conforme mostrado na captura de tela a seguir:

A seguir estão os controles:

° Uma caixa de imagem (picStarsBitmap) mostrando um dos retratos


infravermelhos obtidos pelo Telescópio Espacial Spitzer da NASA
(você pode encontrar muitos deles em www.nasa.gov ou http://
www.nasa.gov/multimedia/imagegallery/), com sua propriedade
SizeMode definida como StretchImage.

[ 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;

4. Adicione o seguinte procedimento, WaitForThreadsToDie. Isso fará com que o thread


principal durma para esperar até que muitos threads simultâneos terminem seu
trabalho:

private void WaitForThreadsToDie() {

// Um sinalizador bool
bool lbContinue = true; int
liDeadThreads = 0; int liThreadNumber;
while (lbContinuar) {

for (liThreadNumber = 0; liThreadNumber


< priProcessorCount; liThreadNumber++)
{
if (prloThreadList[liThreadNumber].IsAlive) {

// Uma das threads ainda está ativa, // sai do loop for e


dorme 100 // milissegundos

quebrar;

} outro
{
// Aumenta a contagem de threads mortos liDeadThreads+
+;
}

} if (liDeadThreads == priProcessorCount) {

// Todas as threads estão mortas, saia do while // loop

quebrar;
}

[ 115 ]

www.it-ebooks.info
Machine Translated by Google

Processamento Avançado de Threads

Thread.Sleep(100);
liDeadThreads = 0;
}
}

5. Adicione o seguinte procedimento, ShowBitmapWithOldStars. Ele reconstruirá o


bitmap adicionando cada parte previamente separada:
private void ShowBitmapWithOldStars() {

int liThreadNumber;
// Cada parte do bitmap
Bitmap loBitmap;
// A linha inicial em cada iteração int liStartRow = 0;

// Calcula a altura de cada bitmap int


liEachBitmapHeight = ((int)(proOriginalBitmap.Height /
atProcessorCount)) + 1;

// Cria um novo bitmap com toda a largura e // altura loBitmap = new


Bitmap(proOriginalBitmap.Width,

proOriginalBitmap.Height);
Graphics g = Graphics.FromImage((Image)loBitmap); g.InterpolationMode
= System.Drawing.Drawing2D.
InterpolationMode.
Bicúbico de alta qualidade;

for (liThreadNumber = 0; liThreadNumber < priProcessorCount;


liThreadNumber++) {

// Desenha cada parte em sua // linha inicial absoluta


correspondente g.DrawImage(prloBitmapList[liThreadNumber],
0, liStartRow); // Aumenta a linha inicial liStartRow +=
liEachBitmapHeight;

}
// Mostra o bitmap na PictureBox picStarsBitmap picStarsBitmap.Image =
loBitmap;

g.Dispose();
}

[ 116 ]

www.it-ebooks.info
Machine Translated by Google

Capítulo 4

6. Abra o evento Click no botão butFindOldStars e digite o seguinte


código:
proOriginalBitmap = new Bitmap(picStarsBitmap.Image);

// 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;

int liEachBitmapHeight = ((int)(proOriginalBitmap.Height / priProcessorCount)) + 1;

int liHeightToAdd = proOriginalBitmap.Height; Bitmap loBitmap;

// Inicializa as threads for (liThreadNumber


= 0; liThreadNumber < priProcessorCount; liThreadNumber++) {

// Apenas para ocupar o número


prliOldStarsCount.Add(0);

if (liEachBitmapHeight > liHeightToAdd) {

// A altura do último bitmap talvez seja menor que a


altura de outros bitmaps
liEachBitmapHeight = liHeightToAdd;
}

loBitmap = CropBitmap(proOriginalBitmap, new Rectangle(0, liStartRow,


proOriginalBitmap.Width, liEachBitmapHeight));

liHeightToAdd -= liEachBitmapHeight; liStartRow +=


liEachBitmapHeight; prloBitmapList.Add(loBitmap);

// Adiciona o novo thread, com um início parametrizado // (para permitir


parâmetros) prloThreadList.Add(new Thread(new ParameterizedThreadStart

(ThreadOldStarsFinder)));

[ 117 ]

www.it-ebooks.info
Machine Translated by Google

Processamento Avançado de Threads

// Agora, inicie as threads para


(liThreadNumber = 0; liThreadNumber < priProcessorCount; liThreadNumber++) {

prloThreadList[liThreadNumber].Start(liThreadNumber);
}

WaitForThreadsToDie();

ShowBitmapWithOldStars();

7. Crie e execute o aplicativo.


8. Clique no botão Old Star Finder . Após alguns segundos (dependendo
nas capacidades de processamento paralelo do computador) na região de formação
de estrelas W5, um enorme retrato infravermelho será mostrado com suas prováveis
estrelas antigas em azul puro, conforme mostrado na captura de tela a seguir:

[ 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):

proOriginalBitmap = new Bitmap(picStarsBitmap.Image);

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:

prloThreadList = new List<Thread>(priProcessorCount); prliOldStarsCount =


new List<long>(priProcessorCount); prloBitmapList = new
List<Bitmap>(priProcessorCount);

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;

Em seguida, devemos determinar o número aproximado de linhas para cada bitmap:

int liEachBitmapHeight = ((int)(proOriginalBitmap.Height /


atProcessorCount)) + 1;

[ 119 ]

www.it-ebooks.info
Machine Translated by Google

Processamento Avançado de Threads

No entanto, dependendo do número de núcleos e da altura do bitmap original, o resultado


dessa divisão pode não ser exato. Esse é outro problema. Portanto, usamos outra variável int
para calcular a altura a ser adicionada a cada iteração para resolver esse problema:

int liHeightToAdd = proOriginalBitmap.Height;

Então, o algoritmo é simples; para cada iteração de liThreadNumber, faça o seguinte:

if (liEachBitmapHeight > liHeightToAdd) {

liEachBitmapHeight = liHeightToAdd;

} loBitmap = CropBitmap(proOriginalBitmap, new Rectangle(0, liStartRow, proOriginalBitmap.Width,


liEachBitmapHeight)); liHeightToAdd -= liEachBitmapHeight; liStartRow += liEachBitmapHeight;
prloBitmapList.Add(loBitmap);

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.

Sinais entre threads


Para ajudar na coordenação da atividade entre os vários threads, podemos criar variáveis
acessíveis a toda a classe e, portanto, a cada thread. Em seguida, os encadeamentos podem gravar
e ler essas variáveis para ajudá-los a coordenar a atividade e manter o controle geral do que está
acontecendo entre todos os encadeamentos.

Usamos threads com parâmetros, como aprendemos nos capítulos anteriores, e os iniciamos com
uma execução assíncrona usando o seguinte loop:

for (liThreadNumber = 0; liThreadNumber < priProcessorCount; liThreadNumber++) {

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.

O código no procedimento WaitForThreadsToDie é complexo porque temos que verificar cada


thread criado e sabemos o número de threads em tempo de execução, pois eles são alinhados
dinamicamente com o número de núcleos disponíveis. Usamos um sinalizador bool para determinar se
o loop while deve continuar em execução ou não. Porém, neste caso, não alteramos o valor do
sinalizador, mas existem outros casos em que este padrão de código pode ser útil para modificar o
valor desta variável utilizada como sinalizador.

Uma vez no loop while (lbContinue), devemos verificar se cada thread termina seu trabalho.
Usamos a conhecida propriedade IsAlive :

for (liThreadNumber = 0; liThreadNumber < priProcessorCount;


liThreadNumber++)
{
if (prloThreadList[liThreadNumber].IsAlive)

[ 121 ]

www.it-ebooks.info
Machine Translated by Google

Processamento Avançado de Threads

{
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.

Se ainda houver um thread em execução, chamamos o método Sleep para o thread


principal e o deixamos inativo por 100 milissegundos (0,1 segundos) e, em seguida,
redefinimos a variável liDeadThreads :

Thread.Sleep(100);
liDeadThreads = 0;

A linha com a chamada ao método Sleep é indispensável.

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.

Conforme mencionado anteriormente, quando a chamada para o método WaitForThreadsToDie


retorna, podemos mostrar com segurança o bitmap resultante na interface do usuário porque todos os
threads concluíram seu trabalho. Portanto, chamamos o procedimento ShowBitmapWithOldStars .

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:

loBitmap = new Bitmap(proOriginalBitmap.Width, proOriginalBitmap.Height);

[ 122 ]

www.it-ebooks.info
Machine Translated by Google

Capítulo 4

Graphics g = Graphics.FromImage((Image)loBitmap); g.InterpolationMode =


System.Drawing.Drawing2D.InterpolationMode.
Bicúbico de alta qualidade;

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:

for (liThreadNumber = 0; liThreadNumber < priProcessorCount;


liThreadNumber++)
{
g.DrawImage(prloBitmapList[liThreadNumber], 0, liStartRow); liStartRow += liEachBitmapHeight;

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;

Ao desacoplar a interface do usuário, podemos gerar melhorias de desempenho


impressionantes, alterando os algoritmos básicos de programação linear.

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

Processamento Avançado de Threads

Execute o aplicativo alterando o valor da variável privada priProcessorCount de 1


para o número de núcleos disponíveis em seu computador e compare os resultados.

Como fazer isso


Agora, vamos fazer algumas alterações no código para usar os manipuladores de
espera do evento em vez de usar um loop para verificar os threads que estão ativos:

1. Fique no projeto, OldStarsFinder.


2. Abra o código para Program.cs.
3. Substitua a linha [STAThread] pela seguinte linha (antes da declaração do
método Main ):
[MTAThread]

4. Abra o código para Windows Form, Form1 (frmStarsFinder).


5. Adicione a seguinte variável privada: // O array de
instâncias AutoResetEvent private AutoResetEvent[]
praoAutoResetEventArray;

6. Substitua o código no método WaitForThreadsToDie pela seguinte linha:

// Apenas espere que as threads sinalizem que cada // item de trabalho terminou
WaitHandle.WaitAll(praoAutoResetEventArray);

7. Adicione a seguinte linha de código na declaração de variáveis locais do


Método ShowBitmapWithOldStars , antes da linha int liStartRow = 0; (devemos
criar o array de acordo com o número de núcleos disponíveis):
// Cria o array AutoResetEvent com o número de // núcleos disponíveis
praoAutoResetEventArray = new AutoResetEvent[priProcessorCount];

8. Adicione a seguinte linha de código no loop de criação de thread no


Método ShowBitmapWithOldStars , antes da linha prloThreadList.Add(new
Thread(new ParameterizedThreadStart(ThreadOldStarsFinder))); (devemos criar
uma instância de AutoResetEvent com um estado inicial falso para cada thread):

// Cria uma nova instância de AutoResetEvent para esse thread com


seu estado inicial definido como falso
praoAutoResetEventArray[liThreadNumber] = novo
AutoResetEvent(falso);

[ 124 ]

www.it-ebooks.info
Machine Translated by Google

Capítulo 4

9. Adicione a seguinte linha de código ao final do método ThreadOldStarsFinder


(devemos sinalizar que o item de trabalho foi concluído):
// A thread terminou seu trabalho. Sinal de que o // item de trabalho foi concluído.
praoAutoResetEventArray[liThreadNumber].Set();

10. Crie e execute o aplicativo.


11. Clique no botão Old Star Finder . Após alguns segundos (dependendo da capacidade
de processamento paralelo do computador), a região de formação estelar W5 será
mostrada no enorme retrato infravermelho com as prováveis estrelas antigas mostradas
em azul puro. Você não notará nenhuma alteração no aplicativo.

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

de AutoResetEvent nos permite notificar um thread em espera de que ocorreu um evento.


É uma subclasse das classes WaitHandle e EventWaitHandle .

[ 125 ]

www.it-ebooks.info
Machine Translated by Google

Processamento Avançado de Threads

Os manipuladores de espera de eventos encapsulam objetos específicos do sistema


operacional que aguardam acesso exclusivo a recursos compartilhados. Usando-os, é
mais fácil esperar que os sinais dos threads continuem funcionando quando seus trabalhos
estiverem concluídos.

Primeiro, temos que criar manipuladores de espera de evento correspondentes ao número de threads.
Fazemos isso na seguinte linha, no método FindOldStarsAndShowResult :

praoAutoResetEventArray = new AutoResetEvent[atProcessorCount];

Usamos um array porque o método WaitAll recebe um array de handles de espera como
parâmetro.

Antes de criar cada nova instância da classe Thread , criamos um novo


Instância de AutoResetEvent para cada thread, com seu estado inicial (um estado bool ) definido como falso:

praoAutoResetEventArray[liThreadNumber] = novo AutoResetEvent(false);

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();

O estado inicial do identificador de espera do evento era falso; agora é verdade.

Usando a classe WaitHandle para


verificar sinais
Por outro lado, o thread principal da interface do usuário precisa esperar até que todos os threads do localizador de
estrelas simultâneos terminem seu trabalho para mostrar o bitmap modificado final na interface do usuário. Assim, ele deve
esperar que todos os manipuladores de eventos tenham seu estado definido como verdadeiro, em vez do falso inicial.

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

Devemos alterar o modelo de encadeamento do aplicativo para um apartamento multithread para


poder usar o método WaitHandle.WaitAll . Se não fizermos isso, a chamada do método falhará e gerará
uma exceção. Portanto, temos que substituir a linha [STAThread], antes da declaração do método Main ,
por [MTAThread].

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.

Como fazer Se quisermos

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();

Então, agora o loop for fica assim:

// Agora, inicie as threads para


(liThreadNumber = 0; liThreadNumber <
priProcessorCount; liThreadNumber++)
{
prloThreadList[liThreadNumber].Start(liThreadNumber);

//Espere aqui no Thread que você acabou de criar para // completar.


prloThreadList[liThreadNumber].Join();

[ 127 ]

www.it-ebooks.info
Machine Translated by Google

Processamento Avançado de Threads

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

threads, causando condições de corrida e outros comportamentos inesperados .

A sintaxe do bloqueio é a seguinte:

bloqueio (objVariável) {

O código entre colchetes é executado de maneira thread-safe e não permite


outros threads operam no objeto que está sendo bloqueado até que a execução do bloqueio seja concluída.

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:

var temp = objeto1;

Monitor.Enter(temp);

tentar

// corpo

} finalmente
{
Monitor.Exit(temp);
}

No C# 4.0, ele é tratado da seguinte forma:

bool bloqueado = false; var


temp = objeto1; tentar {

Monitor.Enter(temp, ref bloqueado); // corpo

} 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

Processamento Avançado de Threads

Como fazer isso


Agora, vamos dar uma olhada em nosso código e fazer uma alteração simples para usar uma única
variável para manter nossa antiga contagem de estrelas. Vamos fazer com e sem trava e ver os
diferentes comportamentos.

1. Primeiro, vamos adicionar o seguinte ao topo de nossa classe nas


instruções de declaração:

//Contagem de estrelas antigas usando um bloqueio para proteger a segurança do encadeamento.


private String prsOldStarsCount = "0";

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();
}

Como funciona Como você

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á.

Tratamento de erros com threads


Nesta seção, discutiremos várias maneiras de usar o bloco try..catch para executar o tratamento de
erros e a coordenação de vários threads.

[ 130 ]

www.it-ebooks.info
Machine Translated by Google

Capítulo 4

Neste exemplo, vamos escrever um aplicativo de console simples que demonstra um


modelo típico de produtor-consumidor. Também demonstraremos o uso de um try..catch
para coordenar atividades, bem como o mecanismo de junção e bloqueio discutido anteriormente.
Este exercício consolidará tudo o que aprendemos neste capítulo até agora.

Além disso, como veremos em capítulos futuros, o padrão de projeto produtor-consumidor


é um dos padrões de projeto mais comumente usados na resolução de problemas
multithread e é muito usado com a Task Parallel Library. Ele descreve um design em que
um thread (ou um grupo de threads) "produz" alguma saída e um segundo thread (ou
grupo de threads) "consome" essa saída e executa alguma lógica nela. Normalmente,
você fará o multithread da "produção" de alguns resultados em uma fila e, em seguida, fará
o multithread do "consumo" dos resultados produzidos da fila.

Como fazer isso


Execute as seguintes etapas:

1. Abra o Visual Studio e crie um novo projeto chamado ProducerConsumer.


2. Vamos criar três classes, uma classe Producer , uma classe Consumer e uma
classe Cell . A classe Producer produzirá células e a classe Consumer
consumirá células. No método Main , iniciaremos um thread produtor e um thread
consumidor e depois os uniremos.
3. Adicione o seguinte código no arquivo Program.cs para que fique assim:
usando Sistema;
usando System.Threading;

public class ProdutorConsumidor {

public static void Main(String[] args) {

result = 0; // Saída de resultados int


Célula célula = new Célula();

Produtor produtor = new Produtor(célula, 5);


Consumidor consumidor = new Consumidor(célula, 5);

Thread produtorThread = new Thread(new


ThreadStart(produtor.ThreadRun)); Thread consumidorThread = new
Thread(new ThreadStart(consumer.ThreadRun));

tentar

[ 131 ]

www.it-ebooks.info
Machine Translated by Google

Processamento Avançado de Threads

produtorThread.Start();
consumidorThread.Start();

// Junte ambos os threads.


produtorThread.Join();
consumidorThread.Join();

} catch (ThreadStateException e) {

System.Diagnostics.Debug.WriteLine(e); // Texto de saída da exceção.

resultado = 1; // Define o resultado para indicar um


erro.

} catch (ThreadInterruptedException e) {

System.Diagnostics.Debug.WriteLine(e); // Texto de saída


notando uma interrupção. resultado
= 1; // Define o resultado para indicar um
erro.
}

Environment.ExitCode = resultado;
}
}

public class Produtor {

Célula celular;
quantidade int = 1;

public Producer(Cell box, int request) {

célula = caixa;
quantidade = pedido;

} public void ThreadRun() {

for (int looper = 1; looper <= quantidade; looper++) célula.Write(looper); //


"produzindo"
}
}

consumidor de classe pública

[ 132 ]

www.it-ebooks.info
Machine Translated by Google

Capítulo 4

{
Célula celular;
quantidade int = 1;

public Consumer(Cell box, int request) {

célula = caixa;
quantidade = pedido;

} public void ThreadRun() {

valor int; for (int


looper = 1; looper <= quantidade; looper++)
valor = célula.Read(); // Consuma o resultado por
colocando em valor }

classe pública Célula {

int cellContents; bool


Estado = false; public int Ler()
{

bloquear (este) // Sincronizando bloco de código.


{
if (!Estado) {
// Espera até Cell.Write terminar
produzindo
tentar

{
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

Processamento Avançado de Threads

estado = falso; // O consumo é feito.

Monitor.Pulse(this); // Pulse diz a Cell.Write que


Cell.Read está concluído.
}

return conteúdo da célula;


}

public void Write(int n) {

lock (this) // Bloco de sincronização {

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.

Monitor.Pulse(this); // Pulse diz a Cell.Read that


Cell.Write está concluído.

}
}
}

[ 134 ]

www.it-ebooks.info
Machine Translated by Google

Capítulo 4

4. Vamos executar este aplicativo de console. No Visual Studio, verifique se você


estão exibindo a janela Saída . Você pode fazer isso acessando o menu Exibir
e selecionando Saída. Depois de executar o aplicativo, você deve ver algo como
o seguinte na janela Saída :

Como você pode ver em nosso aplicativo, coordenamos entre o produtor e o


consumidor e, usando o bloco try..catch e os bloqueios, somos capazes de produzir um
item e, em seguida, consumir esse item.

Como funciona Aqui,


neste exemplo, mostramos como a sincronização é feita usando bloqueios e um
objeto Monitor . O método Pulse notifica um thread, que está na fila de espera, sobre uma
mudança no estado do objeto.

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

Processamento Avançado de Threads

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.

Como discutimos, esse tipo de programação paralela é chamada de simultaneidade


pesada porque o trabalho pesado é feito por você, o programador. Você precisa gerenciar e
controlar os diferentes encadeamentos para obter o comportamento pretendido. Você precisa
rastrear quais threads são executados e quando e quais threads estão esperando e por quê.
Isso cria complexidade e trabalho para o programador, mas também permite um controle muito
rígido da execução. Há momentos em que esta é a melhor maneira de garantir o comportamento
adequado do seu aplicativo.

A seguir, no Capítulo 5, Lightweight Concurrency – Task Parallel Library (TPL), começaremos


a examinar a simultaneidade leve e a Task Parallel Library. Isso permite que o .NET lide com
parte dessa coordenação para você. É importante que você tenha total compreensão da classe
Thread antes de passar para a biblioteca Task e a simultaneidade leve.

[ 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.

Depois de concluir este capítulo, você:

• 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

• Compreender como as tarefas são gerenciadas no .NET e no pool de threads •


Entender a classe Parallel e como iniciar tarefas usando-a • Conhecer a evolução
do multithreading de peso pesado para simultaneidade leve

www.it-ebooks.info
Machine Translated by Google

Simultaneidade Leve – Biblioteca Paralela de Tarefas (TPL)

• Aprenda quando usar tarefas em vez de Threads

• Compreender o paralelismo de tarefas versus o paralelismo de

dados • Compreender a coleta de dados simultâneos e o processamento de dados simultâneos

Biblioteca Paralela de Tarefas


A Task Parallel Library foi introduzida como parte do .NET com o lançamento da Versão 4.0. Originalmente,
foi desenvolvido sob o nome de Parallel Extensions, que foi um esforço conjunto da Microsoft Research
e da equipe CLR. As extensões paralelas consistiam em TPL e LINQ paralelo (PLINQ), que abordaremos
em um capítulo posterior.
TPL agora é preferido sobre threads e componentes BackgroundWorker para desenvolver aplicativos
multithread.

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ê.

A seguir, examinaremos o centro da TPL, a classe Task .

[ 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.

A classe Task possui um conjunto completo de métodos para atualizações de status,


cancelamento, tratamento de exceções, agendamento e espera que permite que ela seja "leve"
em comparação com o thread. Ele também pode fazer uso mais eficiente dos recursos do sistema devido
à funcionalidade que o TPL fornece para gerenciar o ThreadPool nos bastidores.

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.

Como fazer isso


Começaremos criando um novo aplicativo de console usando o Visual Studio 2013. Chamaremos nosso
aplicativo de TaskExample.

1. Primeiro, adicionaremos duas instruções using ao arquivo Program.cs para nos permitir
trabalhar com as classes TPL.

usando System.Threading; usando


System.Threading.Tasks;

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";

for (int i = 0; i < 20; i++) {

Console.WriteLine("Nome do thread {0}, Número: {1}", Thread.


CurrentThread.Name, i);
Thread.Sleep(2000);

[ 139 ]

www.it-ebooks.info
Machine Translated by Google

Simultaneidade Leve – Biblioteca Paralela de Tarefas (TPL)

}
}

static void WriteWords() {

//Configura o nome do thread.


Thread.CurrentThread.Name = "Tópico 2";

String localString = "Este é um exemplo para usar


tarefas";
String[] localWords = localString.Split(' '); foreach (String s em palavras
locais) { Console.WriteLine("Nome do thread {0}, Word: {1}", Thread.

CurrentThread.Name, s);
Thread.Sleep(2000);
}

estático void WriteColors()


{
//Configura o nome do thread.
Thread.CurrentThread.Name = "Thread 3";

String[] localColors = {"vermelho", "laranja", "azul", "verde", "amarelo", "branco", "preto"}; foreach
(String s em localColors) {

Console.WriteLine("Nome do thread {0}, Cores: {1}",Thread.


CurrentThread.Name, s);
Thread.Sleep(2000);
}
}

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.

5. Agora, vamos substituir o código no método Main pelo seguinte código:


//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();

[ 141 ]

www.it-ebooks.info
Machine Translated by Google

Simultaneidade Leve – Biblioteca Paralela de Tarefas (TPL)

Parallel.Invoke (

nova ação(WriteNumbers), nova


ação(WriteWords), nova ação(WriteColors)

);
Console.ReadLine();

6. Agora, reconstrua e execute o aplicativo novamente. Você deve ver um resultado


idêntico ou quase idêntico.

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.

O construtor Task recebe um delegado Action :

public delegate void Action<in T>(


Obj
)

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.

No segundo exemplo, usamos outra classe no TPL e executamos as tarefas usando o


método Parallel.Invoke() . Aqui, conseguimos colocar todas as três tarefas no ThreadPool
de uma só vez usando este método e a classe Action .

Usamos o comando Console.ReadLine() apenas para manter a janela de comando aberta


após a conclusão da execução dos threads. Isso nos permite estudar os resultados e
controlar o fechamento da janela. Para fechar a janela, basta pressionar a tecla Enter ; isso
completará a instrução ReadLine . O console está esperando para ler uma linha de entrada.

[ 142 ]

www.it-ebooks.info
Machine Translated by Google

capítulo 5

Tarefas com valores de retorno


Agora, veremos como iniciar uma tarefa que retorna um valor. Na maioria dos casos, se nos
preocupamos com o resultado de uma tarefa ou se uma tarefa faz algum trabalho para ser
consumido pelo restante do programa, queremos que a tarefa retorne alguns valores para usarmos.
Demonstraremos isso desenvolvendo um aplicativo de console simples que inicia três tarefas e
imprime os valores de retorno dessas três tarefas.

Como fazer isso


Começaremos criando um novo aplicativo de console usando o Visual Studio 2013. Chamaremos
nosso aplicativo de TaskExampleWithReturnValues.

Em seguida, coloque o seguinte código na classe Program de Program.cs:

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();

string static WriteNumbers() {

//Configura o nome do thread.


Thread.CurrentThread.Name = "Tarefa 1";

for (int i = 0; i < 20; i++) {

[ 143 ]

www.it-ebooks.info
Machine Translated by Google

Simultaneidade Leve – Biblioteca Paralela de Tarefas (TPL)

Console.WriteLine("Nome do encadeamento {0}, Número: {1}",Thread.CurrentThread.


Nome, i);
Thread.Sleep(2000);
}

return String.Format("Esta tarefa foi concluída - {0}", Thread.


CurrentThread.Name);
}

static String WriteWords() {

//Configura o nome do thread.


Thread.CurrentThread.Name = "Tarefa 2";

String localString = "Este é um exemplo de uso de tarefas"; String[] localWords =


localString.Split(' '); foreach (String s em localWords) {

Console.WriteLine("Nome do encadeamento {0}, Palavra: {1}", Thread.CurrentThread.


Nome, s);
Thread.Sleep(2000);
}

return String.Format("Esta tarefa foi concluída - {0}", Thread.


CurrentThread.Name);
}

static String WriteColors() {

//Configura o nome do thread.


Thread.CurrentThread.Name = "Tarefa 3";

String[] localColors = { "vermelho", "laranja", "azul", "verde", "amarelo", "branco", "preto" };

foreach (String s em localColors) {

Console.WriteLine("Nome do encadeamento {0}, Cores: {1}",Thread.CurrentThread.


Nome, s);
Thread.Sleep(2000);
}

return String.Format("Esta tarefa foi concluída - {0}", Thread.


CurrentThread.Name);
}
}

[ 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 :

Task<String> t1 = new Task<String>(() => WriteNumbers());

Isso é muito útil porque nos permite retornar qualquer tipo de objeto.

[ 145 ]

www.it-ebooks.info
Machine Translated by Google

Simultaneidade Leve – Biblioteca Paralela de Tarefas (TPL)

Usando a linha Console.WriteLine(t1.Result);, dizemos automaticamente ao .NET para bloquear


ou interromper o thread principal e aguardar a conclusão da tarefa t1 e retornar o valor
t1.Result. .NET é inteligente o suficiente para saber que queremos esperar até que um valor esteja
presente antes de executar esta instrução. Caso contrário, se for executado imediatamente, o valor
pode ou não estar lá. Se não estivesse lá, receberíamos um erro de referência nula. Essa é outra
maneira de dizer que usar TPL é mais fácil do que usar threads. A API TPL cuida desses detalhes
para você, desenvolvedor, gerenciando-os no próprio código.

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.

Coleções simultâneas Outro namespace que foi

introduzido no .NET 4.0 é o Systems.Collections.


Simultâneo. Isso não faz parte diretamente do TPL, mas é frequentemente usado em conjunto
com o TPL para fornecer muitos dos padrões de projeto paralelos comuns, como produtor-
consumidor, que discutiremos no Capítulo 9, Pipeline e padrões de projeto produtor - consumidor .

System.Collections.Concurrent fornece uma versão thread-safe de classes


de coleção no namespace Systems.Collections . Estes funcionam muito bem
em conjunto com as tarefas. Esse namespace tem ConcurrentBag, que é uma
coleção de objetos como ConcurrentDictionary, ConcurrentQueue,
ConcurrentStack e BlockingCollection para citar os mais populares.
Todas essas coleções simultâneas implementam interfaces para a coleção subjacente.
Isso essencialmente envolve a coleção e fornece um mecanismo de segurança de thread. Isso é
útil para o desenvolvedor multithread porque você pode usá-los e não precisa projetar uma lógica
thread-safe em torno deles.

Nesta seção, veremos a classe ConcurrentQueue e mostraremos um exemplo de como usá-la


com tarefas para realizar um exemplo multithread simples. Isso demonstrará o poder e a
simplicidade que o TPL fornece para o processamento multithread.
Não precisamos nos preocupar em bloquear recursos para torná-los thread-safe. Não precisamos nos
preocupar com o número de núcleos de processamento em nosso hardware. Não precisamos nos
preocupar com condições de corrida entre variáveis. E não precisamos nos preocupar em usar
variáveis globais em uma classe para fornecer thread-safe. Tudo isso é tratado por nós com as aulas.
Só temos que nos preocupar com a lógica.

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.

Como fazer isso


Vamos abrir o Visual Studio e criar um novo projeto de aplicativo de console chamado
ConcurrentCollection; em seguida, execute as seguintes etapas:

1. Coloque o seguinte código no arquivo Program.cs :


usando Sistema;
usando System.Collections.Concurrent; usando
System.Threading; usando System.Threading.Tasks;

classe ConcurrentCollection
{
static void Main()
{
ConcurrentQueue<int> fila = new ConcurrentQueue<int>();

//Soma de uma única thread somando os números enquanto enfileiramos


eles.
int SingleThreadSum = 0;

// Preenche a fila. for (int i = 0; i <


5000; i++) {

SingleThreadSum += i;
fila.Enfileirar(i);
}

//Imprime a Soma de 0 a 5000.


Console.WriteLine("Soma de Thread Simples = {0}", SingleThreadSum);

//Soma de uma adição multithread dos números.

[ 147 ]

www.it-ebooks.info
Machine Translated by Google

Simultaneidade Leve – Biblioteca Paralela de Tarefas (TPL)

int MultiThreadSum = 0;

//Cria um delegado Action para desenfileirar itens e somá-los.


Ação localAction = () => {

int Somalocal = 0; int valor


local;

while (queue.TryDequeue(out localValue)) localSum +=


localValue;

Interlocked.Add(ref MultiThreadSum, localSum);


};

// Executa 3 tarefas simultâneas.


Parallel.Invoke(localAction, localAction, localAction);

// Imprime a soma de 0 a 5000 feita por 3 threads separadas.


Console.WriteLine("Soma MultiThreaded = {0}", MultiThreadSum);

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:

ConcurrentQueue<int> fila = new ConcurrentQueue<int>();

[ 148 ]

www.it-ebooks.info
Machine Translated by Google

capítulo 5

Em seguida, colocamos na fila os números de 1 a 5000 usando o seguinte loop:

para (int i = 0; i < 5000; i++)


{
SingleThreadSum += i;
fila.Enfileirar(i);
}

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:

queue.TryDequeue(out localValue)) localSum += localValue;

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.

Explorando a classe TaskFactory


Uma classe chave do TPL no namespace System.Threading.Tasks é a classe TaskFactory .
TaskFactory é usado na criação e agendamento de tarefas.
A classe TaskFactory possui vários métodos que facilitam muito o agendamento e o gerenciamento de
tarefas. Isso inclui métodos iniciais e contínuos, bem como uma série de métodos que estão em
conformidade com o modelo de programação assíncrona que abordaremos no Capítulo 11, O modelo de
programação assíncrona. Essencialmente, essa classe agrupa muitos dos padrões de design de tarefas
comuns em métodos para facilitar o uso e o desenvolvimento. Essa é mais uma maneira pela qual o TPL
torna o desenvolvimento multithread "leve".

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

Simultaneidade Leve – Biblioteca Paralela de Tarefas (TPL)

Como fazer isso


Vamos começar abrindo o Visual Studio e criando um novo aplicativo de console chamado
TaskFactoryExample. Agora, vamos adicionar o seguinte código ao nosso arquivo Program.cs :

usando Sistema;
usando System.Collections.Generic; usando
System.Linq; usando System.Text; usando
System.Threading; usando System.Threading.Tasks;

namespace TaskFactoryExample {

class TaskFactoryExample {

static TaskFactory TF = new TaskFactory(TaskScheduler.


Padrão);

static void Main(string[] args) {

List<Task> tasklist = new List<Task>();

tasklist.Add(TF.StartNew(() => Worker("Task 1"),


CancellationToken.None, TaskCreationOptions.PreferFairness, TaskScheduler.Default));

tasklist.Add(TF.StartNew(() => Worker("Task 2"),


CancellationToken.None, TaskCreationOptions.PreferFairness, TaskScheduler.Default));
tasklist.Add(TF.StartNew(() => Trabalhador("Tarefa 3"),

CancellationToken.None, TaskCreationOptions.PreferFairness, TaskScheduler.Default));


tasklist.Add(TF.StartNew(() => Worker("Task 4"),

CancellationToken.None, TaskCreationOptions.PreferFairness, TaskScheduler.Default));


tasklist.Add(TF.StartNew(() => Worker("Task 5"),

CancellationToken.None, TaskCreationOptions.PreferFairness, TaskScheduler.Default));

//aguarda a conclusão de todas as tarefas.


Task.WaitAll(tasklist.ToArray());

//Aguarda a entrada antes de terminar o programa.

[ 150 ]

www.it-ebooks.info
Machine Translated by Google

capítulo 5

Console.ReadLine();
}

static void Worker(String taskName) {

Console.WriteLine("Esta é a Tarefa - {0}", taskName);


}
}
}

Agora, vamos compilar e executar este aplicativo. Você deve ver os resultados conforme mostrado
na captura de tela a seguir:

Como funciona Vamos

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:

static TaskFactory TF = new TaskFactory(TaskScheduler.Default);

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.

Em seguida, criamos e executamos as cinco tarefas usando o método StartNew() da classe


TaskFactory , conforme mostrado na linha de código a seguir. Existem muitas sobrecargas para esse
método para permitir que você crie e inicie tarefas de acordo com seus requisitos e padrão de design.
No Capítulo 6, Paralelismo baseado em tarefas, e no Capítulo 7, Paralelismo de dados, examinaremos
mais deles:

TF.StartNew(() => Worker("Task 1"), CancellationToken.None, TaskCreationOptions.PreferFairness,


TaskScheduler.Default)

[ 151 ]

www.it-ebooks.info
Machine Translated by Google

Simultaneidade Leve – Biblioteca Paralela de Tarefas (TPL)

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:

() => Trabalhador("Tarefa 1")

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.

Agendadores de tarefas Um dos

principais benefícios da Biblioteca Paralela de Tarefas em relação ao desenvolvimento usando


a classe Thread é o TaskScheduler. Essa classe faz muito da lógica que você tinha que programar
em seu código multithread para obter desempenho e eficiência máximos. Isso é o que realmente
torna o uso da programação de simultaneidade "leve" do TPL. O trabalho principal da classe
TaskScheduler é lidar com o trabalho de enfileirar tarefas para threads, ou mais especificamente,
o ThreadPool, e gerenciar o ThreadPool para melhor utilizar o número de núcleos de processamento
na máquina em que está sendo executado.

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

Simultaneidade Leve – Biblioteca Paralela de Tarefas (TPL)

A seguir estão quatro maneiras pelas quais o TaskScheduler melhora o desempenho


do ThreadPool e remove o trabalho do desenvolvedor (referenciado em http://msdn.
microsoft.com/en-us/library/dd997402(v=vs.110).aspx):

Apresentando a classe Parallel


A última classe que abordaremos nesta cartilha TPL é a classe Parallel .
Essa classe será abordada em detalhes no Capítulo 7, Paralelismo de dados,
quando discutirmos o paralelismo de dados; mas vale uma introdução aqui. A
classe Parallel faz parte do namespace System.Threading.Tasks e fornece
funcionalidade para usar loops paralelos. Os dois métodos mais usados são
Parallel.For e Parallel.ForEach, que permitem percorrer uma coleção e executar
lógica em cada item da coleção simultaneamente.

[ 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.

Podemos chamar o método Parallel.For usando um método nomeado, um método anônimo


ou uma expressão lambda.

A seguir, exemplos das três maneiras:

// Método nomeado.
Parallel.For(0, 5, Método);

// Método anônimo.
Parallel.For(0, 5, delegate(int i) {

// Faça alguma coisa


});

//Expressão lambda.
Parallel.For(0, 5, i => {

// Faça alguma coisa


});

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:

Parallel.ForEach(IEnumerable<TSource> source, corpo Action<TSource>)

Como fazer isso


Vejamos um exemplo simples. Abra o Visual Studio, crie um novo aplicativo de console chamado
ParallelForEach e execute as seguintes etapas:

1. Coloque o seguinte código no arquivo Program.cs :


usando Sistema;
usando System.IO;
usando System.Threading; usando
System.Threading.Tasks;

[ 155 ]

www.it-ebooks.info
Machine Translated by Google

Simultaneidade Leve – Biblioteca Paralela de Tarefas (TPL)

classe SimpleForEach {

static void Main() {

string[] localStrings = "Estou fazendo um exemplo simples de um loop foreach paralelo".Split('


');

Parallel.ForEach(localStrings, currentString =>


{

Console.WriteLine("A palavra atual é - {0}, e o thread atual é - {1}",


currentString,
Thread.CurrentThread.
ManagedThreadId); }

);

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.

Algo a observar aqui é que você vê os IDs de encadeamento 8, 9, 10 e 11. Há um encadeamento


separado para cada iteração do loop e os IDs de encadeamento não começam com 0 ou 1.
Lembre-se de que o TPL usa ThreadPool. Portanto, o delegado Action é colocado na fila como uma
tarefa separada para o ThreadPool para cada iteração da lista, que tem 11 palavras.
O TaskScheduler e o .NET usam o ThreadPool da maneira mais eficiente possível para processar
essas tarefas enfileiradas simultaneamente. Na minha máquina particular, existem quatro núcleos de
processamento. Então, ele divide o trabalho entre quatro threads. Mas então, com base no restante do
trabalho que o computador está fazendo, o thread 8 lida com duas das tarefas, o thread 9 lida com sete
das tarefas e os threads 10 e 11 lidam com uma tarefa cada.

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.

Delegados e expressões lambda


Neste capítulo, usamos delegados e expressões lambda. Esses dois conceitos são confusos para alguns
novos desenvolvedores, então vamos dedicar um momento para discuti-los com um pouco mais de
detalhes.

Os delegados geralmente são usados ao criar manipuladores de eventos. Um delegado


define um tipo de referência que encapsula um método com um determinado conjunto de
parâmetros e um tipo de retorno. Ele funciona muito como um ponteiro de função em C++. Ele nos
permite passar um objeto delegado que pode ser usado para chamar um método sem precisar conhecer
o método em tempo de compilação.

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

Simultaneidade Leve – Biblioteca Paralela de Tarefas (TPL)

Desta forma, declaramos o Action delegate. Em seguida, instanciamos com o método


WriteNumbers e, por fim, instanciamos um objeto Task com o delegado. Posteriormente no
código, podemos sempre alterar o método que o Action delegate usa, com base na lógica
de negócios. Portanto, não estamos obrigados a que esta tarefa tenha que executar o
método WriteNumbers , toda vez que a tarefa for executada.

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:

Tarefa t1 = new Tarefa(() => WriteNumbers());

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 .

Você também aprendeu qual é o significado de simultaneidade leve versus simultaneidade


pesada e começou a ver os muitos benefícios para o desenvolvedor.

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:

• Que partes da funcionalidade do aplicativo podemos processar simultaneamente? • Como faço


para obter desempenho máximo sem saber de antemão em qual máquina ele estará rodando e
quantos núcleos de processamento ele pode ter?

• 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? •

Como gerencio e coordeno os diferentes segmentos?

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

Paralelismo baseado em tarefas


Neste capítulo, abordaremos a Task Parallel Library (TPL) e os detalhes do uso da classe Task para escrever código
simultâneo.

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.

Neste capítulo, você aprenderá como:

• Aguardar uma tarefa específica quando várias tarefas estiverem sendo executadas simultaneamente •

Aguardar todas ou muitas tarefas que estiverem sendo executadas simultaneamente


• Cancele uma ou mais tarefas quando ocorrerem certas condições

• Lidar com exceções lançadas por uma ou mais tarefas • Coordenar

totalmente a execução e a conclusão de tarefas em execução simultânea

www.it-ebooks.info
Machine Translated by Google

Paralelismo baseado em tarefas

Aguardando a conclusão de uma tarefa


A primeira coisa que precisamos fazer quando executamos uma tarefa é saber
quando ela foi concluída e, se retornar resultados, quais são os resultados. Vejamos
um exemplo simples criando uma tarefa, aguardando sua conclusão e verificando os resultados.

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.

Como fazer isso


Primeiro, vamos abrir o Visual Studio e criar um novo aplicativo de console chamado
WaitonTask. Uma vez criado o novo projeto, adicionaremos o seguinte código ao
arquivo Program.cs :

usando Sistema;
usando System.Threading; usando
System.Threading.Tasks;

class Program { static


Random run = new
Random(); static void Main(string[] args)

{
// Espera em uma única tarefa sem tempo limite.
Tarefa taskA = Task.Factory.StartNew(() => Worker(10000));
tarefaA.Wait();
Console.WriteLine("Tarefa A concluída.");

// Espera em uma única tarefa com um tempo limite.


Tarefa taskB = Task.Factory.StartNew(() => Worker(2000000));
tarefaB.Wait(2000); //Espera 2 segundos.

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

Trabalhador vazio estático (intwaitTime)


{
Thread.Sleep(waitTime); }

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 :

trabalhador duplo estático1()


{
int i = correu.Next(1000000);
Thread.SpinWait(i); retornar i; }

[ 163 ]

www.it-ebooks.info
Machine Translated by Google

Paralelismo baseado em tarefas

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 :

Tarefa<dupla> tarefaC = Tarefa<dupla>.Factory.StartNew(() => Worker1());

Console.WriteLine("TaskC concluída = o resultado é {0}.", taskC.Result);

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?

Como funciona No exercício

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:

Console.WriteLine("Tempo esgotado sem terminar a Tarefa B.");

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.

Aguardando a conclusão de várias tarefas


Na seção anterior, examinamos como esperar explicitamente e implicitamente a conclusão de uma tarefa.
Agora, vamos ver como podemos controlar a execução e a espera quando iniciamos muitas tarefas em
vez de apenas uma.

[ 165 ]

www.it-ebooks.info
Machine Translated by Google

Paralelismo baseado em tarefas

À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:

Os métodos When são como mostrados:

[ 166 ]

www.it-ebooks.info
Machine Translated by Google

Capítulo 6

Ambas as imagens são referências de http://msdn.microsoft.com/en-us/library/


system.threading.tasks.task_methods(v=vs.110).aspx.

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.

Como fazer Para começar,

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:

1. Vá para o modo de design no Visual Studio e adicione os seguintes controles a


o arquivo MainWindow.xaml :

° Adicione três controles Ellipse e nomeie-os como Círculo1,


Círculo2 e Círculo3.
° Defina suas propriedades de altura e largura para 120 para todos os três círculos.
Isso é feito no arquivo MainWindow.xaml usando o modo Design
no Visual Studio.

° 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

Paralelismo baseado em tarefas

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;

O namespace System.Threading.Tasks já deve estar incluído


quando você criou o projeto WPF, mas verifique se ele está
lá.

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()

{ resultado int = executado.Next(10000000);


Thread.SpinWait(resultado);
return String.Format("Número aleatório é {0} e Hora é {1}.",resultado,
DateTime.Now.Millisecond);
}

[ 168 ]

www.it-ebooks.info
Machine Translated by Google

Capítulo 6

SpinWait é usado aqui para demonstrar como a velocidade do


processador pode afetar a velocidade de uma função. SpinWait é muito
ineficiente e consome mais recursos da CPU do que Sleep().

4. Em seguida, vamos criar um manipulador de eventos para btnRandomAll — um evento de


clique chamado btnRandomAll_Click. Dentro deste método, adicione o seguinte código:

private void btnRandomAll_Click(remetente do objeto, RoutedEventArgs e)

{ // Aguarde a conclusão de todas as tarefas.


Task<String>[] tarefas = newTask<String>[3]; for (int i = 0; i< 3; i++)
{

tarefas[i] = Tarefa<String>.Factory.StartNew(() => Worker1());


}
Task.WaitAll(tarefas);

text1.Text = tarefas[0].Result.ToString(); text2.Text =


tarefas[1].Result.ToString(); text3.Text = tarefas[2].Result.ToString();

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

Paralelismo baseado em tarefas

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:

1. Adicione um novo controle Ellipse e nomeie-o Circle4.

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+
+) {

tarefas[i] = Tarefa<String>.Factory.StartNew(() => Worker1());


}
//Task.WaitAll(tarefas); índice int
= Task.WaitAny(tasks);

text4.Text = "Tarefa" + index.ToString() + " finalizado


primeiro."; }

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

Paralelismo baseado em tarefas

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.

1. Quando você clica no botão Random All , o manipulador de eventos é acionado.


O manipulador de eventos inicia três tarefas separadas, cada uma executando o método
Worker1 usando o seguinte comando: Task<String>.Factory.StartNew(() => Worker1());

2. Criamos um array de tarefas com um valor de string TResult usando este comando:
Task<String>[] tarefas = newTask<String>[3];

3. Em seguida, esperamos todas as tarefas na matriz de tarefas usando este comando:


Task.WaitAll(tarefas);

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:

índice int = Task.WaitAny(tasks);

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

Paralelismo baseado em tarefas

Na próxima seção, exploraremos o cancelamento de tarefas. Dependendo de seus requisitos, você


pode querer cancelar uma tarefa que iniciou, antes que ela termine.

Cancelando uma tarefa


Vamos explorar o cancelamento de uma tarefa. Agora que sabemos como iniciar uma ou mais tarefas
e como coordenar a espera em uma ou mais dessas tarefas, e se algo acontecer depois de iniciarmos
um grupo de tarefas e precisarmos interrompê-las?

O cancelamento da tarefa é realizado na estrutura .NET usando tokens de cancelamento. O


delegado usado ao executar uma tarefa precisa ter um código para oferecer suporte ao
cancelamento e, em seguida, qualquer outro código que precise cancelar uma tarefa pode
solicitar o cancelamento da tarefa.

A estrutura CancellationToken no .NET gerencia a notificação de que uma


operação deve ser cancelada. Ele tem uma propriedade, IsCancellationRequested,
que você pode usar para ver se uma solicitação de cancelamento foi emitida.
Essa estrutura é membro do namespace System.Threading e é usada por threads
e tarefas para gerenciar solicitações de cancelamento em um ambiente multithread.

Apenas solicitar o cancelamento de uma tarefa não cancelará a tarefa. O delegado de


tarefa deve oferecer suporte a tokens de cancelamento. Há duas maneiras que um delegado
pode optar por cancelar. O delegado pode simplesmente retornar da execução ou pode lançar
uma OperationCanceledException usando o método ThrowIfCancellationRequested .

Existem duas diferenças principais para esses dois métodos de cancelamento. Se o delegado
apenas retornar da execução, acontecerá o seguinte:

1. A tarefa é colocada em um estado de TaskStatus.RanToCompletion em vez de


TaskStatus.Canceled.

2. O código que solicita o cancelamento da tarefa não sabe se a tarefa simplesmente


terminou ou foi cancelado antes de terminar.

[ 174 ]

www.it-ebooks.info
Machine Translated by Google

Capítulo 6

Se o delegado de tarefa implementa o último método para cancelamento (lançando um


OperationCanceledException), então acontece o seguinte:

1. A tarefa é colocada em um estado de TaskStatus.Canceled.

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.

Como fazer isso


Abra o Visual Studio e crie um novo aplicativo WPF chamado TaskCancel.
Na janela do designer no arquivo MainWindow.xaml , vamos adicionar os seguintes itens.

1. Adicione dois controles de botão: um denominado btnStart e outro denominado btnCancel.

2. Em seguida, adicione um manipulador de eventos de clique para cada botão: btnStart_Click


e btnCancel_Click, respectivamente.
3. Adicione um controle de bloco de texto chamado textBlock1.

[ 175 ]

www.it-ebooks.info
Machine Translated by Google

Paralelismo baseado em tarefas

Seu arquivo MainWindow.xaml deve se parecer com a captura de tela a seguir:

Agora, vá para o código MainWindow.xaml.cs atrás do arquivo. Vamos adicionar os seguintes itens:

1. Adicione uma instrução using para o namespace System.Threading : using


System.Threading;

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:

private void btnStart_Click(remetente do objeto, RoutedEventArgs e)

{ ts = new CancellationTokenSource();

textBlock1.Text = "";

List<Task> tarefas = new List<Task>();

for (int i = 2; i<= 10; i++)


{
int tmp = i;

Task<double> adder = Task.Factory.StartNew(() =>AddMultiple(tmp), ts.Token);

tarefas.Adicionar(adicionador);

var show = adder.ContinueWith(resultTask => textBlock1.Text += tmp. +


adder.Result.ToString() + Environment.
" - "
ToString() +
NewLine,
CancellationToken.None,
TaskContinuationOptions.OnlyOnRanToCompletion,
TaskScheduler.FromCurrentSynchronizationContext());

var showCancel = adder.ContinueWith(resultTask =>


textBlock1.Text +=
tmp.ToString() + " cancelado" + Ambiente.NovaLinha,
CancellationToken.None,
TaskContinuationOptions.OnlyOnCanceled, TaskScheduler.
FromCurrentSynchronizationContext());
}
}

5. Por fim, vamos adicionar o código ao manipulador de eventos click de btnCancel:


private void btnCancel_Click(objeto remetente, RoutedEventArgs e)

{ ts.Cancelar(); }

[ 177 ]

www.it-ebooks.info
Machine Translated by Google

Paralelismo baseado em tarefas

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.

Ao executar o aplicativo, ele deve se parecer com a seguinte captura de tela:

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

O método ThrowIfCancellationRequested tem cancelamento cooperativo. Isso lança uma exceção


para indicar que aceitou o cancelamento solicitado e será interrompido.
Essa exceção é tratada pelo .NET no TPL e não deve ser tratada pelo seu código.
Como o Visual Studio procura todas as exceções não tratadas e as mostra no modo de
depuração, você as verá ao executar o aplicativo no modo de depuração por meio do Visual
Studio. Existe uma maneira de desativar isso por meio das configurações de Depuração de opções
em Ferramentas, mas não recomendamos porque pode mascarar outras exceções que você deseja
ver. No entanto, a exceção que encontramos aqui é um incômodo durante a depuração.

Como funciona Agora que

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:

Task<double> adder = Task.Factory.StartNew(() =>AddMultiple(tmp), ts.Token);

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:

var show = adder.ContinueWith(resultTask =>


textBlock1.Text += tmp.ToString()
+ " - "
+ adder.Result.ToString() + Environment.NewLine,
CancellationToken.None,
TaskContinuationOptions.OnlyOnRanToCompletion,
TaskScheduler.FromCurrentSynchronizationContext());

[ 179 ]

www.it-ebooks.info
Machine Translated by Google

Paralelismo baseado em tarefas

var showCancel = adder.ContinueWith(resultTask =>


textBlock1.Text += tmp. " cancelado"
ToString() + + Ambiente.NovaLinha,
CancellationToken.None,

TaskContinuationOptions.OnlyOnCanceled, TaskScheduler.
FromCurrentSynchronizationContext());

Para o primeiro, passamos o parâmetro TaskContinuationOptions.


OnlyOnCanceled, e para o segundo, passamos o parâmetro,
TaskContinuationOptions.OnlyOnRanToCompletion.
Assim, se a tarefa for cancelada, o segundo comando ContinueWith é executado e, se a tarefa
for concluída, o primeiro comando ContinueWith é executado. Em cada um desses comandos
ContinueWith , passamos uma expressão lambda para executar, que imprime os resultados da
tarefa. É assim que atualizamos a interface do usuário com o status de cada tarefa, depois de
concluída.

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.

Em seguida, veremos o tratamento de erros com tarefas.

Tratamento de exceção de tarefa


Bem, aprendemos como iniciar, coordenar e cancelar tarefas. Agora, vamos falar sobre
tratamento de erros com tarefas. Em C#, quando discutimos o tratamento de erros, estamos
realmente falando sobre a estrutura try..catch..finally . Neste livro, presumimos que você esteja
familiarizado com a operação de um bloco try..catch .

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:

• Aguardando uma tarefa

• Usando o valor do resultado da tarefa

• Executando um Await na tarefa

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:

task.ContinueWith(resultTask =>DoSomething(), TaskContinuationOptions.


OnlyOnFaulted);

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 .

O TPL fornece a biblioteca Parallel para facilitar a execução de operações simultâneas


em um conjunto de dados ou coleta de dados usando as diferentes sobrecargas dos
métodos Parallel.For e Parallel.ForEach .

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:

• Processamento de dados paralelo com Parallel.For •


Processamento de dados paralelo de coleções IEnumerable •
Usando os resultados de loops de dados simultâneos •
Cancelando uma operação de loop paralelo

www.it-ebooks.info
Machine Translated by Google

Paralelismo de Dados

Processamento de loop paralelo


Nesta primeira seção, examinaremos o método Parallel.For e diversas variações dele. A
sobrecarga mais básica para esse método usa um índice inicial, um índice final e um delegado
Action . No método Parallel , o delegado Action pode ser implementado com um método nomeado,
método anônimo ou uma expressão lambda. A seguir está a sintaxe básica para cada método:

// 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.

Aqui está uma lista de todas as sobrecargas do método Parallel.For :

[ 184 ]

www.it-ebooks.info
Machine Translated by Google

Capítulo 7

Referência do MSDN — http://msdn.microsoft.com/en-us/library/system.threading.tasks.parallel.for(v=vs.110).


aspx

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.

Como fazer isso


Para este exemplo, criaremos um aplicativo WPF que permite ao usuário inserir números em 10
caixas e clicar em um botão. Depois que o botão é clicado, ele simultaneamente pega cada número
e o multiplica pelos números de 1 a 10 e soma os resultados. O resultado de cada cálculo será
colocado de volta em cada caixa.
Execute as seguintes etapas para fazer isso:

1. Abra o Visual Studio e crie um aplicativo WPF chamado ParallelMath1.

2. Na exibição de design MainWindow.xaml , altere o título da página para ParallelMath:


Title="ParallelMath" Height="350" Width="525">

[ 185 ]

www.it-ebooks.info
Machine Translated by Google

Paralelismo de Dados

3. Agora, crie 10 controles de caixa de texto e coloque-os no arquivo MainWindow.xaml


com os nomes tb1, tb2, tb3, tb4, tb5, tb6 , tb7, tb8, tb9 e tb10 , respectivamente.
Além disso, defina suas propriedades de texto como 0.
4. Agora, coloque um controle de botão no arquivo MainWindow.xaml e defina o
Propriedade Content para Calculate e a propriedade Name para btnCalculate.
5. Em seguida, crie um manipulador de eventos para o evento de clique do botão chamado
btnCalculate_Click.
6. Dentro da definição de classe, coloque uma linha de código para criar uma matriz de
10 inteiros: int[]
números = new int[10];

7. Agora, coloque o seguinte código dentro do manipulador de eventos btnCalculate_Click :


int[] números = novo int[10]; números[0] =
Convert.ToInt32(tb1.Text); números[1] = Convert.ToInt32(tb2.Text);

números[2] = Convert.ToInt32(tb3.Text); números[3] =


Convert.ToInt32(tb4.Text); números[4] = Convert.ToInt32(tb5.Text);
números[5] = Convert.ToInt32(tb6.Text);

números[6] = Convert.ToInt32(tb7.Text); números[7] =


Convert.ToInt32(tb8.Text); números[8] = Convert.ToInt32(tb9.Text);
números[9] = Convert.ToInt32(tb10.Text);

Parallel.For(0, 9, CalculateNumbers);

tb1.Text = números[0].ToString(); tb2.Text =


números[1].ToString(); tb3.Text = números[2].ToString();
tb4.Text = números[3].ToString(); tb5.Text =
números[4].ToString(); tb6.Text = números[5].ToString();
tb7.Text = números[6].ToString(); tb8.Text =
numeros[7].ToString(); tb9.Text = números[8].ToString();

tb10.Text = números[9].ToString();

8. Finalmente, crie um método chamado CalculateNumbers e coloque o seguinte


código nele:

private void CalculateNumbers(int i) {

int j = numeros[i];

[ 186 ]

www.it-ebooks.info
Machine Translated by Google

Capítulo 7

for (int k = 1; k <= 10; k++) {

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.

Você deve ver resultados semelhantes a este antes de clicar no botão:

[ 187 ]

www.it-ebooks.info
Machine Translated by Google

Paralelismo de Dados

Agora, vamos inserir 10 números em nossas caixas de texto para que o


aplicativo se pareça com o seguinte:

Agora, clique no botão Calcular e você verá os seguintes resultados muito


rapidamente, pois estamos fazendo esses cálculos simultaneamente:

[ 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.

Vejamos como o loop simultâneo é executado. É o único comando Parallel.For(0,


9, CalculateNumbers);. Este comando enfileira 10 tarefas para o threadpool e cada
tarefa executará o método CalculateNumbers com um parâmetro inteiro.

Agora, vamos ver o comando Parallel.ForEach .

Paralelismo de dados em coleções usando Parallel.ForEach A forma de

paralelismo de dados que considero mais útil é executar operações simultâneas


em coleções de dados. Isso nos permite pegar coleções de dados como listas,
exibições de dados, dicionários e assim por diante e executar uma tarefa em cada item da
coleção em paralelo com uma única linha de código! Isso simplifica o uso do paralelismo de
dados; você não precisa fazer nada além do processamento normal de dados. Esta é uma
das razões pelas quais o TPL é um aprimoramento tão maravilhoso para o .NET.

Muito parecido com o método Parallel.For , a estrutura de Parallel.ForEach se parece com


o seguinte:
Parallel.ForEach(dataCollection, item => DoWork(item));

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

A seguir estão todas as diferentes sobrecargas do método ForEach :

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.

Como fazer Vamos pegar

nosso aplicativo OldStarsFinder Windows Form original e alterá-lo. Para isso vamos realizar os seguintes
passos:

1. Primeiro, vamos abrir nosso aplicativo OldStarsFinder no Visual Studio.

2. Vamos adicionar uma nova instrução using para que possamos acessar a biblioteca Parallel :
usando System.Threading.Tasks;

3. No início da definição de classe, remova todas as declarações de variáveis antigas e substitua-


as apenas por estas: private int Count = 0; // Número de bitmaps para dividir o original e
adicionar à // lista de bitmaps. lista privada<Bitmap> BitmapList; //Lista de bitmaps para
usar o ParallelForEach.

Bitmap OriginalBitmap; private


String prsOldStarsCount = "0";

//Contagem de estrelas antigas usando um bloqueio para proteger a segurança do encadeamento.

[ 191 ]

www.it-ebooks.info
Machine Translated by Google

Paralelismo de Dados

4. Substitua o método CropBitmap por esta definição de método:


Private Bitmap CropBitmap(Bitmap proBitmap, Retângulo
proRetângulo)
{
// Cria um novo bitmap copiando a parte do // original definido por proRectangle //
e mantendo seu PixelFormat Bitmap loCroppedBitmap =
proBitmap.Clone(proRectangle,

proBitmap.PixelFormat);

return loCroppedBitmap;
}

5. Você pode deixar o método IsOldStar como está.

6. Altere o método ThreadOldStarsFinder para ficar assim:


private void ThreadOldStarsFinder(Bitmap loBitmap) {

int liRow; // A matriz de pixels


(bitmap) número da linha (Y) int
liCol; (bitmap) // A matriz de pixels
número da coluna (X)
Cor loPixelColor; // A cor do pixel

// 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++) {

// Obtém a cor do pixel para liCol e liRow loPixelColor =


loBitmap.GetPixel(liCol,
liRow);
if (IsOldStar(loPixelColor)) {

// A gama de cores corresponde a um antigo


estrela
// Muda sua cor para um azul puro loBitmap.SetPixel(liCol,
liRow, Color.
Azul);

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();
}

}
}
}
}

7. Em seguida, altere o método ShowBitmapWithOldStars para ficar assim:


private void ShowBitmapWithOldStars() {

int liThreadNumber;
// Cada parte do bitmap
Bitmap loBitmap;
// A linha inicial em cada iteração int liStartRow = 0;

// Calcula a altura de cada bitmap int liEachBitmapHeight


= ((int)(OriginalBitmap.Height
/ Contagem)) + 1;

// Cria um novo bitmap com toda a largura e altura loBitmap = new


Bitmap(OriginalBitmap.Width, OriginalBitmap.Height); Graphics g =
Graphics.FromImage((Image)loBitmap); g.InterpolationMode = System.Drawing.Drawing2D.

InterpolationMode.HighQualityBicubic;

for (liThreadNumber = 0; liThreadNumber < Contagem; liThreadNumber++)

{
// 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();
}

8. Agora, altere o manipulador de eventos butFindOldStars_Click para


ter o seguinte código: private void butFindOldStars_Click(object
sender, EventArgs e)
{
Count = Convert.ToInt32(tbTasks.Text); OriginalBitmap =
new Bitmap(picStarsBitmap.Image); BitmapList = new List<Bitmap>(Contagem);

int Linhainicial = 0; int


EachBitmapHeight = ((int)(OriginalBitmap.Height / Count)) + 1;

int HeightToAdd = OriginalBitmap.Height; Bitmap loBitmap;

// Divida o bitmap em uma lista de bitmaps. for (int i = 0; i <


Contagem; i++) {

if (EachBitmapHeight > HeightToAdd) {

// A altura do último bitmap talvez seja menor que


a altura dos outros bitmaps
CadaBitmapHeight = HeightToAdd;
}

loBitmap = CropBitmap(OriginalBitmap, new Rectangle(0,


StartRow, OriginalBitmap.Width, EachBitmapHeight)); HeightToAdd -= CadaBitmapHeight;
StartRow += EachBitmapHeight; BitmapList.Add(loBitmap);

//Percorra a lista de bitmaps com o comando Parallel.ForEach.

Parallel.ForEach(BitmapList, item =>


ThreadOldStarsFinder(item));

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.

11. Finalmente, removemos o botão butFindOldStarsBatch porque não


precisar dele nesta aplicação.

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.

Vamos dar uma olhada no que acabou de acontecer.

Como funciona Se você

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:

// Divida o bitmap em uma lista de bitmaps. for (int i = 0; i <


Contagem; i++) {

[ 196 ]

www.it-ebooks.info
Machine Translated by Google

Capítulo 7

if (EachBitmapHeight > HeightToAdd) {

// A altura do último bitmap talvez seja menor que // a altura dos outros
bitmaps
CadaBitmapHeight = HeightToAdd;
}

loBitmap = CropBitmap(OriginalBitmap, new Rectangle(0,


StartRow, OriginalBitmap.Width, EachBitmapHeight));
HeightToAdd -= CadaBitmapHeight; StartRow
+= EachBitmapHeight; BitmapList.Add(loBitmap);

Em seguida, pegamos nossa coleção de listas, BitmapList, e a usamos em um comando


ForEach paralelo nesta linha de código:

Parallel.ForEach(BitmapList, item => ThreadOldStarsFinder(item));

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.

Cancelando um loop paralelo


Agora que aprendemos como executar um loop paralelo usando uma biblioteca de classes Parallel ,
vamos dar uma olhada em como podemos interromper ou interromper um loop, se necessário. Com um
loop For ou ForEach normal , podemos usar um comando Continue para interromper um loop.

[ 197 ]

www.it-ebooks.info
Machine Translated by Google

Paralelismo de Dados

Se interrompermos um loop paralelo, concluímos todas as iterações nos threads que


estão em execução no momento e paramos. Se pararmos um loop paralelo, paramos
todas as iterações do loop em execução o mais rápido possível, mas não as executamos
até a conclusão. Em ambos os casos, não agendaremos tarefas no threadpool para o
restante das iterações do loop paralelo com as quais ainda não começamos.

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 .

O que é a classe ParallelLoopState? Esta classe não pode ser


instanciada em seu código de usuário. Ele é fornecido pelo TPL e .NET e,
portanto, é uma classe especial. Este objeto fornece ao seu loop paralelo
um mecanismo para interagir com outras iterações no loop.

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.

Como fazer isso


Só precisamos fazer alguns ajustes em nosso programa anterior. Vamos começar
abrindo o aplicativo ParallelMath1 WPF no Visual Studio e fazendo as seguintes
alterações:

1. Crie um novo método chamado CalculateNumbers2 e coloque o seguinte


código nele:

private void CalculateNumbers2(int i, ParallelLoopState


por favor)

{
int j = numeros[i];

se (i < 7) {

for (int k = 1; k <= 10; k++) {

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

Agora, clique no botão Calcular e seus resultados devem se parecer com a


seguinte captura de tela:

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.

Manipulando exceções em loops paralelos


Bem, não importa o quanto tentemos escrever um código livre de erros, o mundo real intervém e
inevitavelmente haverá exceções que nosso código encontrará, por exemplo, arquivo não encontrado,
argumento fora ou intervalo e assim por diante. Quando estamos processando um comando de loop
paralelo, uma vez que todas as iterações estão sendo executadas em threads potencialmente diferentes,
precisamos de uma maneira de reunir todas as exceções que qualquer uma das iterações do loop pode
produzir.

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

Como fazer isso


Para começar, vamos abrir o projeto ParallelMath1 no Visual Studio e fazer as
seguintes alterações:

1. Estaremos usando uma fila Simultânea para coletar as exceções, então adicione
esta instrução using :
usando System.Collections.Concurrent;

2. Em seguida, precisamos declarar nossa instância ConcurrentQueue onde ela esteja


visível para toda a classe. Adicione a seguinte linha logo após a declaração no
array inteiro do número:
ConcurrentQueue<Exception> exceções = novo
ConcurrentQueue<Exception>();

3. Em seguida, adicionaremos um novo método a ser chamado pelo delegado Action


do nosso comando de loop paralelo. Chamaremos esse método de
CalculateNumbers3. Adicione o seguinte código a este método: private void
CalculateNumbers3(int i, ParallelLoopState
por favor)

{
int j = numeros[i];

tentar

{
for (int k = 1; k <= 10; k++) {

j *= k;

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);
}

números[i] = j;

[ 202 ]

www.it-ebooks.info
Machine Translated by Google

Capítulo 7

4. Em seguida, vamos alterar nosso manipulador de eventos btnCalculate_Click . Altere o código


entre a população da matriz de números e a população das caixas de texto para incluir as
seguintes linhas de código:
tentar

Parallel.For(0, 10, CalculateNumbers3);

if (exceptions.Count > 0) lançar novo


AggregateException(exceções);
}

catch (AggregateException ae) {

// 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;
}
}

5. Por fim, na exibição do designer MainWindow.xaml , adicione um controle de bloco de texto;


defina sua propriedade Name como tbMessages e sua propriedade Text como uma string vazia.

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 .

Como funciona A primeira


coisa que mudamos foi adicionar um objeto que está em uma fila concorrente, para
conter todas as nossas exceções. Isso é feito com este comando:

ConcurrentQueue<Exception> exceções = novo


ConcurrentQueue<Exception>();

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));

Em seguida, capturamos essa exceção no delegado e a adicionamos à nossa fila simultânea


de objetos de exceção usando estas instruções:

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.

Depois que o loop for concluído, queremos verificar se há exceções e processá-las.


Aqui está o código que lida com essa funcionalidade:

tentar
{
Parallel.For(0, 10, CalculateNumbers3);

if (exceptions.Count > 0) lançar novo


AggregateException(exceções);
}

captura (AggregateException ae)

[ 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;
}
}

Se virmos alguma exceção em nosso objeto ConcurrentQueue , lançamos uma


AggregateException e damos a ela toda a fila de exceções. Em seguida, capturamos
essa AggregateException e processamos todas as exceções que ela contém.

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.

Usando variáveis locais de thread em loops


paralelos
Uma vez que você se sinta confortável usando loops paralelos, parando-os e executando o
tratamento de exceções com eles, vamos falar sobre como podemos usar variáveis locais de
thread para coordenar melhor os resultados. Se quisermos somar os resultados de todas as
iterações de um loop paralelo, como faríamos 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.

Os loops Parallel.For e Parallel.ForEach têm sobrecargas que implementam uma


variável local de thread. O que queremos dizer com uma variável thread-local?
Esta é uma variável cujo escopo dura a duração do loop paralelo, desde o início da primeira
iteração até a conclusão da última iteração. Cada iteração do loop obtém sua própria cópia da
variável thread-local.

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 examinar uma das sobrecargas de ForEach :

ForEach<TSource, TLocal>(IEnumerable<TSource>, Func<TLocal>,


Func<TSource, ParallelLoopState, TLocal, TLocal>, Action<TLocal>)

Vamos dissecar isso por um minuto. Vamos pegar cada parte da definição do método e explicar
seu papel:

• ForEach<TSource, TLocal>: TSource é o tipo de dados da coleção


de origem e TLocal é o tipo de dados da variável thread-local. •
IEnumerable<TSource>: Esta é a coleção de origem. Como estamos usando
um exemplo ForEach , a coleção de origem deve ser IEnumerable.
• Func<TLocal>: Esta é a primeira função; ele inicializa o thread-local
variável.

• Func<TSource, ParallelLoopState, TLocal, TLocal>: Este é o


segunda função; é o Action delegado que é executado por cada iteração do loop.

• Action<TLocal>: Esta é a terceira função; é o Action delegado que é


executado no estado local de cada iteração.

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.

Como fazer isso


Para usar uma variável local de thread para resumir nossas caixas de texto depois de executarmos
nosso loop paralelo nelas, vamos abrir nosso projeto ParallelMath1 e fazer algumas alterações:

1. No arquivo MainWindow.xaml na exibição do designer, vamos adicionar um controle de rótulo


e defina a propriedade Content como Sum:.

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

3. No arquivo MainWindow.xaml.cs , adicione a seguinte instrução using para que


podemos usar o método Interlocked.Add :

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;

5. Em seguida, comente nosso comando Parallel.For atual porque estamos


vai usar a nova versão sobrecarregada necessária para variáveis locais de thread:
//Parallel.For(0, 10, CalculateNumbers3);

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 :

Parallel.For<long>(0, 10, () => 0, (i,


loop, subtotal) => {

int j = numeros[i]; for (int k =


1; k <= 10; k++) {

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.

Agora, vamos examinar o que acabou de acontecer

Como funciona Assim como

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:

Parallel.For<long>(0, 10, () => 0, (i, loop,


subtotal) => {

int j = numeros[i]; for (int k = 1;


k <= 10; k++) {

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 :

(i, loop, subtotal) =>


{
int j = numeros[i]; for (int k = 1;
k <= 10; k++)

[ 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:

(finalResult) => Interlocked.Add(ref total, finalResult)

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.

Aprendemos como executar um loop paralelo em um conjunto de dados usando


Parallel.For e uma coleção de dados usando Parallel.ForEach. Em seguida, vimos como
parar ou sair de um loop quando uma determinada condição foi atingida; para isso usamos
o objeto ParallelLoopState que o .NET pode gerar.

Em seguida, exploramos o tratamento de erros com loops paralelos e o objeto


AggregateException . Aprendemos como processar todas as exceções que podem ocorrer
durante as diferentes iterações do loop sem afetar as outras iterações.

[ 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

Depurando aplicativos multithread com o Visual Studio

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.

Neste capítulo, você aprenderá o seguinte:

• Usando a janela Threads no depurador • Usando a janela

Tasks no depurador • Acessando as janelas Parallel Stacks

e Parallel Watch • Percorrendo um aplicativo com mais de um thread e mais de

uma tarefa

• Detectando uma condição de impasse

Considerações para depurar


aplicativos multithread
Há várias coisas a serem consideradas ao depurar um aplicativo paralelo. A primeira coisa é provavelmente
a mais óbvia – há várias coisas acontecendo ao mesmo tempo. Se você tiver um aplicativo com cinco
threads, todos os cinco threads poderão ser executados ao mesmo tempo, dependendo de quantos núcleos
sua máquina possui. Se você passar por um thread específico, os outros threads não estarão apenas
esperando por você, a menos que haja pontos de interrupção ou eles estejam esperando por um recurso
bloqueado pelo thread que você está percorrendo. Você precisa entender como seu aplicativo paralelo é
projetado e como ele é executado para que você saiba o que está acontecendo em paralelo - especialmente
o que está acontecendo em threads diferentes daquele que você pode percorrer.

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

Usando a janela Threads


A janela Threads no depurador do Visual Studio permite exibir detalhes de todos os threads em
um aplicativo e trabalhar com eles. Você pode visualizar a pilha de cada thread. Você pode congelar
ou descongelar um tópico e visualizar todos os detalhes relativos a um tópico.

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

Depurando aplicativos multithread com o Visual Studio

• 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 ID mostra o número de ID de um thread no Windows, que é diferente do ID gerenciado do .NET.

A coluna Managed ID mostra o número do ID gerenciado de um encadeamento. Um thread gerenciado é diferente


de um thread do Windows. Um thread gerenciado, neste contexto, é um thread gerenciado no .NET pelo CLR e pelo
threadpool. Um thread gerenciado pode ser executado em um ou mais threads do Windows. Um thread
gerenciado .NET é criado usando a classe .NET Threads . Além disso, lembre-se ao usar Tarefas que uma tarefa
não é um encadeamento.

A coluna Categoria exibe a classificação do encadeamento como o encadeamento principal, um encadeamento da


interface do usuário, um encadeamento do manipulador de procedimento remoto ou um encadeamento de trabalho.

A coluna Name exibe a propriedade Name do thread, se estiver definida ou, caso contrário, exibe <No Name>.

A coluna Local mostra qual método o encadeamento está executando no momento.


Ao clicar duas vezes em uma linha nesta coluna, você pode ver a pilha completa do encadeamento.

A coluna Prioridade mostra a prioridade do Windows para o thread.

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

A coluna Suspended Count é uma contagem manipulada pelo congelamento e descongelamento


de threads. Se você congelar um encadeamento no código gerenciado, o encadeamento não será
executado, mesmo que seja capaz, e a contagem suspensa será 1.

A coluna Nome do processo exibe o processo do Windows ao qual o encadeamento está


associado. Como estamos executando nosso aplicativo por meio do depurador do Visual Studio,
você verá que nossos threads têm OldStarsFinder.vshost.exe como processo. Você também pode
executar o Gerenciador de Tarefas no Windows e ver esse processo.

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.

Em seguida, vamos examinar a janela Tarefas .

Usando a janela Tarefas


A janela Tarefas foi adicionada ao depurador do Visual Studio na versão 2010 quando o TPL foi
adicionado ao .NET na versão 4.0. É semelhante à janela Threads e mostra informações sobre cada
tarefa em seu aplicativo. As tarefas são criadas usando a classe System.Threading.Tasks.Task ou usando
as palavras-chave async e await . As palavras-chave async e await foram introduzidas na versão 4.5
do .NET e serão abordadas no Capítulo 11, O modelo de programação assíncrona. Assim como a janela
Threads , ela pode ser acessada a partir do depurador no Visual Studio navegando até Debug | Janelas:

[ 217 ]

www.it-ebooks.info
Machine Translated by Google

Depurando aplicativos multithread com o Visual Studio

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

aguardando um recurso ° Waiting: A tarefa está atualmente aguardando Alocação de CPU °

Scheduled: A tarefa é agendada para iniciar a execução quando um thread no threadpool

estiver disponível

° Concluído: A tarefa concluiu sua execução

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. •

Atribuição do encadeamento: esta coluna exibe o ID do encadeamento e o nome do encadeamento

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:

Parallel.ForEach(BitmapList, item =>


ThreadOldStarsFinder(item,ran.Next(1000).ToString()));

[ 218 ]

www.it-ebooks.info
Machine Translated by Google

Capítulo 8

Passamos um número aleatório entre 1 e 1.000 como o segundo parâmetro do método


ThreadOldStarsFinder e, em seguida, usamos a seguinte linha para definir o nome do thread para
a tarefa correspondente:

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

executar uma tarefa


de uma chamada de método, mostra o estado assíncrono da tarefa.

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.

A janela Tarefas só é realmente útil na depuração de aplicativos multiencadeados se você estiver


usando o TPL. Se você estiver usando threads ou o componente BackgroundWorker , esta janela não
será útil.

Usando a janela Pilhas Paralelas


A janela Parallel Stacks é muito útil na depuração de aplicativos paralelos porque mostra
informações sobre tarefas e encadeamentos em uma exibição gráfica quase como um fluxograma. Ele
mostra muitas das mesmas informações das janelas Threads e Tasks , mas em um formato diferente e
mostra as relações entre os threads e as tarefas, o que é muito útil.

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

exibições também pode ser alternada para uma exibição de método .

[ 219 ]

www.it-ebooks.info
Machine Translated by Google

Depurando aplicativos multithread com o Visual Studio

A seguir está a visualização Threads de nosso aplicativo OldStarsFinder após a


execução do comando Parallel.ForEach e o bitmap é dividido em oito seções:

A seguir está a exibição de Tarefas de nosso aplicativo OldStarsFinder após


a execução do comando Parallel.ForEach e o bitmap é dividido em oito seções:

A seguir está a visualização Threads alternada para a visualização


Method de nosso aplicativo OldStarsFinder após a execução do comando
Parallel.ForEach e o bitmap é dividido em oito seções:

[ 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 seta amarela em um nó indica o quadro ativo do encadeamento atualmente ativo.


Além disso, o nome do método em todos os nós do quadro de pilha atual do encadeamento ativo é
estilizado em negrito. Se você clicar com o botão direito do mouse em uma linha de método em um quadro
de pilha, obterá um menu que permite ir para essa tarefa ou thread ou para o código-fonte.

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

Depurando aplicativos multithread com o Visual Studio

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.

Usando a janela de Observação Paralela


A janela Parallel Watch permite que você exiba os valores de uma expressão mantida
em vários threads. Você pode vê-lo executando um aplicativo paralelo e selecionar
Parallel Watch navegando para Janela | Depurar. Como você pode ver na captura de
tela a seguir, a janela Parallel Watch fica vazia até que você adicione uma instrução de observação.
Cada linha mostra um thread que possui uma chamada de método que corresponde ao método no
quadro de pilha atual.

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

No exemplo a seguir, pegamos o exemplo usado anteriormente e adicionamos um watch


em uma variável. Estamos observando o valor Thread.Name do thread a cada tarefa atribuída.
Aqui está a mesma janela depois de concluirmos o Parallel.
Comando ForEach . Você pode ver que apenas uma das tarefas foi executada, mas as outras
estão agendadas. Aquele que iniciou executou a primeira linha de seu delegado, que é para definir
o nome do thread:

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.

Depurando um aplicativo inteiro


Agora que examinamos todas as janelas que o depurador do Visual Studio fornece para
depurar aplicativos multithread, vamos ver um exemplo real.
Usaremos o aplicativo OldStarsFinder que usa tarefas e o TPL que finalizamos no Capítulo
7, Paralelismo de dados. Lembre-se de que esse aplicativo pega um número inserido pelo
usuário e divide um arquivo de bitmap grande por esse número em um número igual de
arquivos de bitmap menores. Em seguida, usando um loop ForEach paralelo , ele processa
cada bitmap para tentar encontrar estrelas antigas na imagem. Ele mantém um registro de
quantas estrelas antigas são encontradas. Ao terminar, ele remonta as peças e exibe o grande
bitmap com cada estrela antiga colorida de azul. Ele também exibe o número final de estrelas
antigas encontradas. Vamos começar.

[ 223 ]

www.it-ebooks.info
Machine Translated by Google

Depurando aplicativos multithread com o Visual Studio

Como fazer Primeiro,


vamos abrir nosso aplicativo OldStarsFinder no Visual Studio e definir alguns pontos de
interrupção para nos ajudar a começar. Em seguida, execute as seguintes etapas:

1. Coloque um ponto de interrupção na linha de código que contém o Parallel.ForEach


comando como mostrado aqui:

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

3. Agora, vamos iniciar o aplicativo no depurador e inserir 6 na caixa de texto


para quantos bitmaps dividir a imagem grande:

[ 225 ]

www.it-ebooks.info
Machine Translated by Google

Depurando aplicativos multithread com o Visual Studio

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:

E olhando para a janela Parallel Stacks você vê apenas o thread principal


do aplicativo:

[ 227 ]

www.it-ebooks.info
Machine Translated by Google

Depurando aplicativos multithread com o Visual Studio

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

7. Agora, vamos examinar a mesma coisa usando a visualização Tasks da janela


Parallel Stacks :

8. Além disso, ao mesmo tempo, vamos examinar nossa janela Parallel Watch :

[ 229 ]

www.it-ebooks.info
Machine Translated by Google

Depurando aplicativos multithread com o Visual Studio

9. Em seguida, vamos passar pela linha atual no método delegado ThreadOldStarsFinder


e dar uma olhada em nossas janelas:

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

Depurando aplicativos multithread com o Visual Studio

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

Vamos mudar nossa janela Parallel Stacks para a visualização Threads :

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

Depurando aplicativos multithread com o Visual Studio

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

Depurando aplicativos multithread com o Visual Studio

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

Depurando aplicativos multithread com o Visual Studio

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.

Observe também que quando você vê [Código externo] no quadro de


pilha, isso se refere ao código .NET e não ao seu próprio código-fonte.
Você pode configurar seu depurador para mostrar a pilha de código
externo, mas geralmente isso fornece muitas informações quando você
está mais preocupado com o fluxo e a depuração de seu próprio código-fonte.

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.

Você aprendeu como:

• Examine todos os encadeamentos de seu aplicativo enquanto


ele é executado • Examine todas as tarefas que seu aplicativo cria
usando o TPL • Percorra um aplicativo com várias tarefas e encadeamentos em
execução • Visualize a pilha de qualquer encadeamento ou tarefa em seu aplicativo •
Defina expressões de observação e visualize-as em todos os tópicos

No próximo capítulo, discutiremos alguns padrões de projeto populares para


aplicativos paralelos.

[ 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.

O padrão de projeto produtor-consumidor é um conceito semelhante, mas diferente. Nesse


projeto, temos um conjunto de funcionalidades que produz dados que são consumidos por outro
conjunto de funcionalidades. Cada conjunto de funcionalidade é uma tarefa TPL. Assim, temos uma
tarefa de produtor e uma tarefa de consumidor, com um buffer entre elas. Cada uma dessas tarefas
pode ser executada independentemente uma da outra. Também podemos ter várias tarefas de
produtor e várias tarefas de consumidor. Os produtores são executados de forma independente e
produzem resultados de fila para o buffer. Os consumidores são executados de forma independente e
saem da fila do buffer e executam o trabalho no item. O produtor pode bloquear se o buffer estiver
cheio e aguardar a disponibilidade de espaço antes de produzir mais resultados. Além disso, o
consumidor pode bloquear se o buffer estiver vazio, esperando que mais resultados estejam disponíveis para consumo.

www.it-ebooks.info
Machine Translated by Google

Padrões de design de pipeline e produtor-consumidor

Neste capítulo, você aprenderá o seguinte:

• Projetando um aplicativo com um design Pipeline •


Projetando um aplicativo com um design produtor-consumidor •
Aprendendo a usar BlockingCollection • Aprendendo a usar BufferedBlocks
• Compreendendo as classes do System.Threading.Tasks.Dataflow

biblioteca

Padrão de design de pipeline


O design Pipeline é muito útil no design paralelo quando você pode dividir um aplicativo
em séries de tarefas a serem executadas de forma que cada tarefa possa ser executada
simultaneamente com outras tarefas. É importante que a saída de cada tarefa esteja na mesma
ordem que a entrada. Se a ordem não importa, um loop paralelo pode ser executado. Quando
a ordem é importante e não queremos esperar até que todos os itens tenham concluído a tarefa
A antes que os itens comecem a executar a tarefa B, uma implementação de Pipeline é perfeita.

Alguns aplicativos que se prestam ao pipelining são streaming de vídeo, compactação


e criptografia. Em cada um desses exemplos, precisamos executar um conjunto de tarefas nos
dados e preservar a ordem dos dados, mas não queremos esperar que cada item de dados
execute uma tarefa antes que qualquer um dos dados possa executar a próxima tarefa.

A classe-chave que o .NET forneceu para implementar esse padrão de design é


BlockingCollection do namespace System.Collections.Concurrent .
A classe BlockingCollection foi introduzida com o .NET 4.5. É uma coleção thread-safe
projetada especificamente para padrões de design produtor-consumidor e Pipeline.
Ele suporta simultaneamente adicionar e remover itens por vários segmentos de e para
a coleção. Ele também possui métodos para adicionar e remover esse bloco quando a coleção
está cheio ou vazio. Você pode especificar um tamanho máximo de coleta para garantir que
uma tarefa de produção que ultrapasse uma tarefa de consumo não torne a fila muito grande.
Ele suporta tokens de cancelamento. Por fim, ele oferece suporte a enumerações para que
você possa usar o loop foreach ao processar itens da coleção.

[ 242 ]

www.it-ebooks.info
Machine Translated by Google

Capítulo 9

Um produtor de itens para a coleção pode chamar o método CompleteAdding quando o


último item de dados for adicionado à coleção. Até que esse método seja chamado se um
consumidor estiver consumindo itens da coleção com um loop foreach e a coleção estiver
vazia, ele será bloqueado até que um item seja colocado na coleção em vez de encerrar o
loop.

A seguir, veremos um exemplo simples de implementação de design de Pipeline


usando um programa de criptografia. Este programa implementará três etapas em nosso pipeline.
O primeiro estágio lerá um arquivo de texto caractere por caractere e colocará cada
caractere em um buffer (BlockingCollection). O próximo estágio lerá cada caractere do buffer
e o criptografará adicionando 1 ao seu número ASCII. Em seguida, ele colocará o novo
caractere em nosso segundo buffer e o gravará em um arquivo de criptografia. Nosso estágio
final irá ler o caractere do segundo buffer, descriptografá-lo para seu caractere original e gravá-
lo em um novo arquivo e na tela. Como você verá, os estágios 2 e 3 começarão a processar os
caracteres antes que o estágio 1 termine de ler todos os caracteres do arquivo de entrada. E
tudo isso será feito mantendo a ordem dos caracteres para que o arquivo de saída final seja
idêntico ao arquivo de entrada:

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

Padrões de design de pipeline e produtor-consumidor

Como fazer isso


Primeiro, vamos abrir o Visual Studio e criar uma nova apresentação do Windows
Foundation (WPF) chamado PipeLineApplication e execute as seguintes etapas:

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;

2. No arquivo MainWindow.xaml.cs , certifique-se de usar as seguintes instruções


estão presentes:
using System;
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) {

String DisplayData = ""; tentar {

foreach (char C em GetData(PipelineInputFile)) {

//Caracteres exibidos lidos do arquivo.

[ 244 ]

www.it-ebooks.info
Machine Translated by Google

Capítulo 9

DisplayData = DisplayData + C.ToString();

// Adiciona cada caractere ao buffer para o próximo estágio.


output.Add(C);

} finalmente
{
output.CompleteAdding();
}
}

4. Em seguida, adicionaremos um método para o segundo estágio chamado StageWorker.


Este método não retornará nenhum valor e receberá três parâmetros.
Um será um valor BlockingCollection que será seu buffer de entrada, o segundo será
o buffer de saída do palco e o último será um caminho de arquivo para armazenar o
texto criptografado em um arquivo de dados. O código desse método ficará assim:

public void StageWorker(BlockingCollection<char> input,


Saída BlockingCollection<char>, String PipelineEncryptFile)
{
String DisplayData = "";

tentar

foreach (char C em input.GetConsumingEnumerable()) {

//Criptografa cada caractere. char


criptografado = Criptografar(C);

DisplayData = DisplayData + criptografado.


Para sequenciar();

//Adiciona caracteres ao buffer para o próximo estágio.


output.Add(criptografado);

//escreve a string criptografada no arquivo de saída.


usando (StreamWriter outfile =
novo StreamWriter(PipelineEncryptFile))
{
outfile.Write(DisplayData);

[ 245 ]

www.it-ebooks.info
Machine Translated by Google

Padrões de design de pipeline e produtor-consumidor

} finalmente
{
output.CompleteAdding();
}
}

5. Agora, adicionaremos um método para a terceira e última etapa do Pipeline


projeto. Este método será nomeado FinalStage. Ele não retornará nenhum valor
e receberá dois parâmetros. Um será um objeto BlockingCollection que é o
buffer de entrada e o outro será uma string apontando para um arquivo de dados
de saída. Ele terá o seguinte código: public void FinalStage(BlockingCollection<char>
input, String PipelineResultsFile)

{
String OutputString = "";
String DisplayData = "";

//Lê os caracteres criptografados do buffer,


descriptografá-los e exibi-los.
foreach (char C em input.GetConsumingEnumerable()) {

//Descriptografar os dados.
char descriptografado = Descriptografar(C);

//Exibe os dados descriptografados.


DisplayData = DisplayData + decrypted.ToString();

//Adiciona à string de saída.


OutputString += descriptografado.ToString();

//escreve a string descriptografada no arquivo de saída. usando


(StreamWriter outfile =
novo StreamWriter(PipelineResultsFile))
{
outfile.Write(OutputString);
}
}

[ 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>();

//Obtém os dados de origem.


usando (StreamReader inputfile = new StreamReader(Pipel
ineInputFile)) {

while (inputfile.Peek() >= 0) {

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:

public char Encrypt(char C)


{
//Pega o caractere, converte para um int, adiciona 1, então
converter de volta para um caractere.

int i = (int)C; i = i + 1; C
= Convert.ToChar(i);

retornar C;
}

[ 247 ]

www.it-ebooks.info
Machine Translated by Google

Padrões de design de pipeline e produtor-consumidor

8. Agora adicionaremos um método final à classe Stages para descriptografar um valor de


caractere. Ele simplesmente fará o inverso do método criptografar . Ele pegará o valor
numérico ASCII e subtrairá 1. O código para este método ficará assim:

public char Descriptografar (char C)


{
int i = (int)C; i = i - 1; C
= Convert.ToChar(i);

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";

private static String PipelineEncryptFile = @"c:\projetos\


EncryptData.txt";
private static String PipelineInputFile = @"c:\projects\ InputData.txt"; Estágio Privado Palco;

11. Em seguida, no método construtor MainWindow , logo após o


Chamada InitializeComponent , adicione uma linha para instanciar nosso objeto
Stages : //Crie o objeto Stage e registre os event listeners para atualizar a interface
do usuário conforme os estágios funcionam.
Fase = new Fases();

12. Em seguida, adicione um botão ao arquivo MainWindow.xaml que iniciará o pipeline e a


criptografia. Nomeie esse controle de botão como butEncrypt e defina sua propriedade
Content como Encrypt File. Em seguida, adicione um manipulador de eventos de clique
para este botão no arquivo MainWindow.xaml.cs . Seu método manipulador de eventos
será butEncrypt_ Click e conterá o código principal para este aplicativo. Ele irá instanciar

[ 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

//Criar filas de entrada e saída para os estágios. int tamanho = 20;


BlockingCollection<char> Buffer1 = new BlockingCollection<char>(tamanho);
BlockingCollection<char> Buffer2 = new BlockingCollection<char>(tamanho);

Tarefas TaskFactory = novo


TaskFactory(TaskCreationOptions.LongRunning,
TaskContinuationOptions.None);

Tarefa Stage1 = tasks.StartNew(() => Stage.


FirstStage(Buffer1, PipelineInputFile));
Tarefa Stage2 = tasks.StartNew(() => Stage.
StageWorker(Buffer1, Buffer2, PipelineEncryptFile)); Tarefa Stage3 =
tasks.StartNew(() => Stage.
FinalStage(Buffer2, PipelineResultsFile));

Task.WaitAll(Estágio1, Estágio2, Estágio3);

//Mostra os 3 arquivos. usando


(StreamReader inputfile = new StreamReader(Pipel
ineInputFile)) {

while (inputfile.Peek() >= 0) {

tbStage1.Text = tbStage1.Text + (char)


arquivo de entrada.Read(); }

} using (StreamReader inputfile = new StreamReader(Pipel ineEncryptFile))

[ 249 ]

www.it-ebooks.info
Machine Translated by Google

Padrões de design de pipeline e produtor-consumidor

while (inputfile.Peek() >= 0) {

tbStage2.Text = tbStage2.Text + (char)


arquivo de entrada.Read(); }

} using (StreamReader inputfile = new StreamReader(Pipel


ineResultsFile))
{
while (inputfile.Peek() >= 0) {

tbStage3.Text = tbStage3.Text + (char)


arquivo de entrada.Read(); }

}
}

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:

15. Todos terminamos e estamos prontos para experimentá-lo. Compile e execute o


aplicativo e você deve ter uma janela parecida com a seguinte captura de tela:

[ 251 ]

www.it-ebooks.info
Machine Translated by Google

Padrões de design de pipeline e produtor-consumidor

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.

Agora, vamos dar uma olhada em como isso funciona.

Como funciona Vejamos o

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

//Criar filas de entrada e saída para os estágios. int tamanho = 20;


BlockingCollection<char> Buffer1 = new BlockingCollection<char>(tamanho);
BlockingCollection<char> Buffer2 = new BlockingCollection<char>(tamanho);

TaskFactory tasks = new TaskFactory(TaskCreationOptions.


LongRunning, TaskContinuationOptions.None);

Tarefa Stage1 = tasks.StartNew(() => Stage.


FirstStage(Buffer1, PipelineInputFile));
Tarefa Stage2 = tasks.StartNew(() => Stage.
StageWorker(Buffer1, Buffer2, PipelineEncryptFile)); Tarefa Stage3 =
tasks.StartNew(() => Stage.
FinalStage(Buffer2, PipelineResultsFile));

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.

Depois que o Stage1 leu todos os caracteres do arquivo de entrada e os colocou no


Buffer1, ele faz a seguinte chamada:

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.

Quando criamos a TaskFactory, usamos o seguinte parâmetro:

TaskCreationOptions.LongRunning

[ 253 ]

www.it-ebooks.info
Machine Translated by Google

Padrões de design de pipeline e produtor-consumidor

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 :

foreach (char C em input.GetConsumingEnumerable())

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.

Então, finalmente temos esta linha de código:

Task.WaitAll(Estágio1, Estágio2, Estágio3);

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:

//Mostra os 3 arquivos. usando


(StreamReader inputfile = new StreamReader(Pipeline
Arquivo de entrada))

{
while (inputfile.Peek() >= 0) {

tbStage1.Text = tbStage1.Text + (char)arquivo de entrada.


Ler();
}

} using (StreamReader inputfile = new StreamReader(Pipeline


EncryptFile))

[ 254 ]

www.it-ebooks.info
Machine Translated by Google

Capítulo 9

{
while (inputfile.Peek() >= 0) {

tbStage2.Text = tbStage2.Text + (char)arquivo de entrada.


Ler();
}

} using (StreamReader inputfile = new StreamReader(Pipeline


Arquivo de Resultados))

{
while (inputfile.Peek() >= 0) {

tbStage3.Text = tbStage3.Text + (char)arquivo de entrada.


Ler();
}

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.

Explicando os blocos de mensagens


Vamos falar um pouco sobre os blocos de mensagens e o TPL. Há uma nova biblioteca
que a Microsoft desenvolveu como parte do TPL, mas ela não vem diretamente com
o .NET 4.5. Essa biblioteca é chamada de biblioteca TPL Dataflow . Ele está localizado no
namespace System.Threading.Tasks.Dataflow . Ele vem com vários componentes de fluxo
de dados que auxiliam em aplicativos simultâneos assíncronos em que as mensagens
precisam ser passadas entre várias tarefas ou os dados precisam ser passados quando
estiverem disponíveis, como no caso de um streaming de vídeo de câmera da web.

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

Padrões de design de pipeline e produtor-consumidor

A biblioteca TPL Dataflow contém classes para auxiliar na passagem de mensagens e na


paralelização de aplicativos pesados de E/S que têm muita taxa de transferência. Ele fornece
controle explícito sobre como os dados são armazenados em buffer e transmitidos. Considere um
aplicativo que carrega de forma assíncrona grandes arquivos binários do armazenamento e manipula esses dados.
A programação tradicional exige que você use callbacks e classes de sincronização, como
bloqueios, para coordenar tarefas e ter acesso aos dados que são compartilhados.
Usando os objetos TPL Dataflow , você pode criar objetos que processam arquivos de imagem à
medida que são lidos de um local de disco. Você pode definir como os dados serão tratados quando
estiverem disponíveis. Como o mecanismo de tempo de execução CLR gerencia as dependências entre
os dados, você não precisa se preocupar com a sincronização do acesso aos dados compartilhados. Além
disso, como o mecanismo CLR agenda o trabalho dependendo da chegada assíncrona de dados, os
objetos TPL Dataflow podem melhorar o desempenho gerenciando os threads nos quais as tarefas são
executadas.

Nesta seção, abordaremos duas dessas classes, BufferBlock e ActionBlock.

A biblioteca TPL Dataflow (System.Threading.Tasks.


Dataflow) não acompanha o .NET 4.5. Para instalar Sistema.
Threading.Tasks.Dataflow, abra seu projeto no Visual Studio,
selecione Manage NuGet Packages no menu Project e pesquise
online por Microsoft.Tpl.Dataflow.

BufferBlock O objeto

BufferBlock na biblioteca Dataflow fornece um buffer para armazenar dados.


A sintaxe é BufferBlock<T>. O T indica que o tipo de dados é genérico e pode ser de qualquer tipo.
Todas as variáveis estáticas desse tipo de objeto têm garantia de thread-safe. BufferBlock é uma
estrutura de mensagem assíncrona que armazena mensagens em uma fila primeiro a entrar,
primeiro a sair. As mensagens podem ser "postadas" na fila por vários produtores e "recebidas" da
fila por vários consumidores.

[ 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.

O seguinte é um conjunto de métodos para esta classe:

Referenciado em http://msdn.microsoft.com/en-us/library/hh160414(v=vs.110).aspx

[ 257 ]

www.it-ebooks.info
Machine Translated by Google

Padrões de design de pipeline e produtor-consumidor

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

Finalmente, aqui estão as referências de interface para esta classe:

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 de código, o objeto ActionBlock é criado com um parâmetro inteiro e


executa uma expressão lambda simples que faz um Console.WriteLine quando uma mensagem
de dados é postada no buffer. Então, quando o comando action.Post(10) é executado, o
inteiro, 10, é postado no buffer ActionBlock e então o delegado ActionBlock , implementado
como uma expressão lambda neste caso,
É executado.

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

Padrões de design de pipeline e produtor-consumidor

Os seguintes métodos de extensão são implementados a partir de suas interfaces:

Referenciado em http://msdn.microsoft.com/en-us/library/hh194684(v=vs.110).aspx

Além disso, implementa as seguintes interfaces:

Referenciado em http://msdn.microsoft.com/en-us/library/hh194684(v=vs.110).aspx

Agora que examinamos um pouco da biblioteca Dataflow desenvolvida pela Microsoft,


vamos utilizá-la em um aplicativo produtor-consumidor.

Padrão de projeto produtor-consumidor


Agora que abordamos a biblioteca Dataflow da TPL e o conjunto de objetos que ela fornece
para auxiliar na transmissão assíncrona de mensagens entre tarefas simultâneas, vamos dar
uma olhada no padrão de design produtor-consumidor. Em um projeto produtor-consumidor típico,
temos um ou mais produtores colocando dados em uma fila ou bloco de dados de mensagem.
Então, temos um ou mais consumidores pegando dados da fila e processando-os. Isso permite o
processamento assíncrono de dados. Usando os objetos da biblioteca Dataflow , podemos criar
uma tarefa de consumidor que monitora um BufferBlock e extrai itens dos dados dele quando eles
chegam. Se nenhum item estiver disponível, o método do consumidor será bloqueado até que os
itens estejam disponíveis ou o BufferBlock tenha sido definido como Complete. Por causa disso,
podemos iniciar nosso consumidor a qualquer momento, mesmo antes de o produtor começar a
colocar itens na fila.

Em seguida, criamos uma ou mais tarefas que produzem itens e os colocamos no


BufferBlock. Depois que os produtores terminarem de processar todos os itens de dados para
o BufferBlock, eles poderão marcar o bloco como Completo. Até então, o objeto BufferBlock
ainda está disponível para adicionar itens. Isso é perfeito para tarefas e aplicativos de longa
duração, quando não sabemos quando os dados chegarão.

[ 260 ]

www.it-ebooks.info
Machine Translated by Google

Capítulo 9

Como a tarefa do produtor está implementando um parâmetro de entrada de um BufferBlock como


um objeto ITargetBlock e a tarefa do consumidor está implementando um parâmetro de entrada de
um BufferBlock como um ISourceBlock, ambos podem usar o mesmo objeto BufferBlock , mas têm
métodos diferentes disponíveis para eles. Um tem métodos para produzir itens para o bloco e marcá-
lo como completo. O outro tem métodos para receber itens e aguardar mais itens até que o bloco
seja marcado como concluído. Dessa forma, a biblioteca Dataflow implementa o objeto perfeito para
atuar como uma fila entre nossos produtores e consumidores.

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.

Como fazer A primeira

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:

1. Primeiro, precisamos instalar e adicionar uma referência à biblioteca TPL Dataflow . A


biblioteca TPL Dataflow (System.Threading.Tasks.Dataflow) não é fornecida com o .NET
4.5. Selecione Gerenciar pacotes NuGet no menu Projeto e pesquise online por
Microsoft.Tpl.Dataflow.

2. Agora, precisaremos adicionar duas instruções using ao nosso programa. Um para


StreamReader e StreamWriter e outro para o objeto BufferBlock : using
System.Threading.Tasks.Dataflow; usando System.IO;

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";

private static String PipelineInputFile = @"c:\projects\ InputData.txt";

[ 261 ]

www.it-ebooks.info
Machine Translated by Google

Padrões de design de pipeline e produtor-consumidor

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) {

String DisplayData = "";

tentar

foreach (char C em GetData(PipelineInputFile)) {

//Caracteres exibidos lidos do arquivo.


DisplayData = DisplayData + C.ToString();

// Adiciona cada caractere ao buffer para o


Próximo estágio.
Target.Post(C);

}
}

finalmente
{
Target.Complete();
}

5. Em seguida, adicionaremos um método estático para executar nossa funcionalidade de consumidor.


Terá o seguinte código:
// Este é o nosso método de consumo. A TI é executada de forma assíncrona. static
async Task<int> Consumer(ISourceBlock<char> Source) {

String DisplayData = "";

// Lê do buffer de origem até o buffer de origem


não tem
// dados de saída disponíveis. while
(aguarde Source.OutputAvailableAsync()) {

char C = Source.Receive();

//Criptografa cada caractere.

[ 262 ]

www.it-ebooks.info
Machine Translated by Google

Capítulo 9

char criptografado = Criptografar(C);

DisplayData = DisplayData + criptografado.


Para sequenciar();

//escreve a string descriptografada no arquivo de saída. usando (StreamWriter


outfile = new StreamWriter(PipelineEncryptFile))

{
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) {

List<char> Dados = new List<char>();

//Obtém os dados de origem.


usando (StreamReader inputfile = new StreamReader(Pipel
ineInputFile))
{
while (inputfile.Peek() >= 0) {

Data.Add((char)inputfile.Read());
}

dados de retorno;
}

7. Em seguida, adicionaremos um método estático para criptografar nossos personagens. Esse


método funcionará como o que usamos em nosso aplicativo de pipelining. Ele adicionará
um ao valor numérico ASCII do caractere:

public static char Encrypt(char C) {

[ 263 ]

www.it-ebooks.info
Machine Translated by Google

Padrões de design de pipeline e produtor-consumidor

//Pega o caractere, converte para um int, adiciona 1, então


converter de volta para um caractere.

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:

static void Main(string[] args) {

// Cria o objeto de bloco de buffer para usar entre o


produtor e consumidor.
BufferBlock<char> buffer = new BufferBlock<char>();

// O método do consumidor é executado de forma assíncrona. Comece


agora.

Task<int> consumidor = Consumer(buffer);

// Poste os dados de origem no bloco de fluxo de dados.


Produtor(buffer);

// Espera o consumidor processar todos os dados. consumidor.Wait();

// Imprime a contagem de caracteres do arquivo de entrada.


Console.WriteLine("Processado {0} bytes do arquivo de entrada.", consumer.Result);

//Imprime o arquivo de entrada para o console.


Console.WriteLine("\r\n\r\n"); Console.WriteLine("Este
é o arquivo de dados de entrada.
\r\n");
usando (StreamReader inputfile = new StreamReader(Pipel
ineInputFile)) {

while (inputfile.Peek() >= 0) {

Console.Write((char)inputfile.Read());
}

[ 264 ]

www.it-ebooks.info
Machine Translated by Google

Capítulo 9

//Imprima o arquivo criptografado para o console.


Console.WriteLine("\r\n\r\n");
Console.WriteLine("Este é o arquivo de dados criptografado.
\r\n");
usando (StreamReader encryptfile = new StreamReader(Pip
com EncryptFile)) {

while (encryptfile.Peek() >= 0) {

Console.Write((char)encryptfile.Read());
}

//Aguarde antes de fechar o aplicativo para que possamos ver


os resultados.
Console.ReadLine();
}

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

Padrões de design de pipeline e produtor-consumidor

10. Depois de executado e concluído, sua saída deve se parecer com o


captura de tela a seguir:

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.

Em seguida, iniciamos nossa tarefa de consumidor usando este comando:

Task<int> consumidor = Consumer(buffer);

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:

static async Task<int> Consumer(ISourceBlock<char> Source)

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 :

while (aguarde Source.OutputAvailableAsync())


{
char C = Source.Receive();

//Criptografa cada caractere. char


criptografado = Criptografar(C);

DisplayData = DisplayData + criptografado.ToString();

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

Padrões de design de pipeline e produtor-consumidor

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 :

static void Producer(ITargetBlock<char> Target)

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:

// Imprime a contagem de caracteres do arquivo de entrada.


Console.WriteLine("Processado {0} bytes do arquivo de entrada.", consumer.Result);

//Imprime o arquivo de entrada para o console.


Console.WriteLine("\r\n\r\n");
Console.WriteLine("Este é o arquivo de dados de entrada. \r\n"); usando
(StreamReader inputfile = new StreamReader(Pipeline
Arquivo de entrada))

{
while (inputfile.Peek() >= 0) {

Console.Write((char)inputfile.Read());
}

//Imprima o arquivo criptografado para o console.


Console.WriteLine("\r\n\r\n");
Console.WriteLine("Este é o arquivo de dados criptografado.
\r\n");
usando (StreamReader encryptfile = new StreamReader(Pipelin
eEncryptFile)) {

while (encryptfile.Peek() >= 0) {

[ 268 ]

www.it-ebooks.info
Machine Translated by Google

Capítulo 9

Console.Write((char)encryptfile.Read());
}

Agora que você está confortável implementando um design básico de produtor-consumidor


usando objetos da biblioteca TPL Dataflow , tente experimentar essa ideia básica, mas use vários
produtores e vários consumidores, todos com o mesmo objeto BufferBlock como a fila entre todos
eles.

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.

No exemplo Pipeline, examinamos os benefícios da classe BlockingCollection em atuar como uma


fila entre os estágios em nosso pipeline.

Em seguida, exploramos a nova biblioteca TPL Dataflow e algumas de suas classes


de blocos de mensagens. Essas classes implementam várias interfaces definidas na
biblioteca — ISourceBlock, ITargetBlock e IPropogatorBlock. A implementação dessas interfaces
nos permite escrever funcionalidades genéricas de tarefas de produtor e consumidor que podem
ser reutilizadas em vários aplicativos.

[ 269 ]

www.it-ebooks.info
Machine Translated by Google

Padrões de design de pipeline e produtor-consumidor

Ambos os padrões de design e a biblioteca Dataflow permitem implementações fáceis de


funcionalidade comum de maneira simultânea. Você usará essas técnicas em muitos aplicativos
e isso se tornará um padrão de design obrigatório quando você avaliar os requisitos de um
sistema e determinar como implementar a simultaneidade para ajudar a melhorar o desempenho.
Como toda programação, a programação paralela fica mais fácil quando você tem uma caixa de
ferramentas com técnicas fáceis de usar com as quais se sente confortável.

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.

Agora, voltaremos ao paralelismo de dados no próximo capítulo e, desta vez,


exploraremos o paralelismo de dados explícito usando PLINQ.

[ 270 ]

www.it-ebooks.info
Machine Translated by Google

LINQ Paralelo – PLINQ


Neste capítulo, aprenderemos sobre paralelismo de dados declarativo usando Parallel
Language Integrated Query (PLINQ). No Capítulo 7, Paralelismo de dados, discutimos o
paralelismo de dados usando a biblioteca Parallel , Parallel.For e Parallel.
ForEach loops. PLINQ é uma versão paralela do LINQ to Objects. LINQ to Objects
permite consultas LINQ em coleções de dados na memória, como List e DataTable , que
implementam a interface IEnumerable ou IEnumerable<T> . Ao contrário de um LINQ
sequencial, o PLINQ tenta usar todos os processadores no computador em que está sendo
executado, dividindo as coletas de dados em segmentos e criando uma tarefa para processar
cada segmento da coleta de dados. O PLINQ e o Common Language Runtime (CLR) são
inteligentes o suficiente para avaliar a consulta e determinar se ela se beneficiará de uma
execução multithread. Caso contrário, ele será executado sequencialmente como um LINQ normal.
É o melhor dos dois mundos. O .NET descobre se pode obter melhorias de desempenho
operando simultaneamente ou não e toma a decisão por você.

Os métodos de extensão paralela foram adicionados ao namespace System.Linq no .NET


4.5. Inclui métodos de extensão para todos os operadores LINQ padrão, bem como alguns
extras para operadores paralelos. Quase todos os métodos PLINQ são implementados no
namespace System.Linq.ParallelEnumerable .

Os tópicos que serão abordados neste capítulo são os seguintes:

• 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

LINQ Paralelo – PLINQ

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.

Como fazer Vamos

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:

1. No arquivo MainWindow.xaml no modo de design, adicione três controles de botão e


nomeie-os como btnMethod1, btnMethod2 e btnMethod3. Defina a propriedade
Content de cada um como Execute o método 1, Execute o método 2 e Execute o
método 3, respectivamente. Em seguida, crie um manipulador de eventos de clique
para cada um no arquivo MainWindow.xaml.cs e nomeie esses três métodos como
btnMethod1_ Click, btnMethod2_Click e btnMethod3_Click, respectivamente.
2. No método do manipulador de eventos btnMethod1_Click , coloque o seguinte código:
private void btnMethod1_Click(remetente do objeto, RoutedEventArgs e)
{
IEnumerable<int> coleção1 = Enumerable.Range(10,
500000);

// Inicia o cronômetro.
Cronômetro sw1 = new Cronômetro(); sw1.Start();

//Método 1 - Isso usa um método ForAll e um método //delegate vazio.

ParallelQuery<int> PQ1 = de num na coleção1.


AsParallel()
onde num % 5 == 0
selecione num;

PQ1.ForAll((i) => DoWork(i));

// Use um loop foreach padrão e mescle os resultados. foreach (int i em PQ1) {

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();

3. No método manipulador de eventos btnMethod2_Click , coloque o seguinte código:


private void btnMethod2_Click(object sender, RoutedEventArgs e)
{
IEnumerable<int> coleção2 = Enumerable.Range(10,
500000);

// Inicia o cronômetro.
Cronômetro sw2 = new Cronômetro(); sw2.Start();

// Método 2 - Use um método ToArray padrão para retornar


//os resultados.
int[] PQ2 = (de num na coleção2.AsParallel()
onde num % 10 == 0
selecione num).ToArray();

// Use um loop foreach padrão e mescle os resultados. foreach (int i em PQ2) {

lb2.Items.Add(i);
}

//Para o cronômetro.
sw2.Stop();

tbTime2.Text = sw2.ElapsedMilliseconds.ToString();
}

4. No método do manipulador de eventos btnMethod3_Click , coloque o código a seguir.


Além disso, vamos adicionar três caixas de listagem e nomeá-las lb1, lb2 e lb3.
private void btnMethod3_Click(remetente do objeto, RoutedEventArgs e)
{
IEnumerable<int> coleção3 = Enumerable.Range(10,
10000);

// Inicia o cronômetro.
Cronômetro sw3 = new Cronômetro(); sw3.Start();

[ 273 ]

www.it-ebooks.info
Machine Translated by Google

LINQ Paralelo – PLINQ

// Método 3 - Use o formato de método padrão LINQ.


ParallelQuery<int> PQ3 = coleção3.AsParallel().
Onde(n => n % 10 == 0).Select(n => n);

// Use um loop foreach padrão e mescle os resultados. foreach (int i em PQ3) {

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)

{}

8. Esse é todo o código que precisamos adicionar ao nosso arquivo MainWindow.xaml.cs .


Nosso arquivo MainWindow.xaml agora deve conter o seguinte código: <Window
x:Class="WpfPLINQQuery.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/

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

<ListBox x:Name="lb3" HorizontalAlignment="Esquerda"


Height="557" Margin="526,78,0,0" VerticalAlignment="Top"
Largura="150"/>
<Button x:Name="btnMethod1" Content="Método de execução

1" HorizontalAlignment="Left" Height="43" Margin="38,26,0,0"


VerticalAlignment="Top" Width="150" Click="btnMethod1_Click"/>
<Button x:Name="btnMethod2" Content="Método de execução

2" HorizontalAlignment="Left" Height="43" Margin="285,26,0,0"


VerticalAlignment="Top" Width="150" Click="btnMethod2_Click"/>
<Button x:Name="btnMethod3" Content="Método de execução

3" HorizontalAlignment="Left" Height="43" Margin="526,26,0,0"


VerticalAlignment="Top" Width="150" Click="btnMethod3_Click"/>
<TextBox x:Name="tbTime1" HorizontalAlignment="Esquerda"
Height="28" Margin="38,684,0,0" TextWrapping="Wrap"
VerticalAlignment="Top" Width="150"/>
<TextBox x:Name="tbTime2" HorizontalAlignment="Esquerda"
Height="28" Margin="285,684,0,0" TextWrapping="Wrap"
VerticalAlignment="Top" Width="150"/>
<TextBox x:Name="tbTime3" HorizontalAlignment="Left"
Height="28" Margin="526,684,0,0" TextWrapping="Wrap"
VerticalAlignment="Top" Width="150"/>

</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

LINQ Paralelo – PLINQ

Agora, vamos clicar no botão Execute Method 1 . Devemos ver resultados


como os seguintes:

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 .

Agora vamos examinar como isso funciona.

[ 277 ]

www.it-ebooks.info
Machine Translated by Google

LINQ Paralelo – PLINQ

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:

ParallelQuery<int> PQ1 = de num na coleção1.AsParallel()


onde num % 5 == 0
selecione num;

Adicionamos o método AsParallel à coleção IEnumerable com o seguinte


comando:
coleção1.AsParallel();

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).

Na verdade, executamos a consulta usando o método ForAll da classe ParallelQuery .


Fizemos isso usando o seguinte comando:
PQ1.ForAll((i) => DoWork(i));

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();

Observe que aqui adicionamos o método ToArray() aos resultados da consulta.


Ao implementar a consulta paralela dessa maneira, garantimos que a consulta seja
executada durante esse comando. No primeiro método, criamos a consulta, mas não a
executamos até que o comando ForAll seja executado.

[ 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:

ParallelQuery<int> PQ3 = coleção3.AsParallel().Where(n => n % 10 == 0).Select(n => n);

Aqui, retornamos um objeto ParallelQuery em vez de um array.

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.

Como fazer isso


Vamos abrir nosso projeto PLINQQuery e fazer as seguintes alterações:

1. Altere a instrução de consulta paralela no método btnMethod1_Click para se parecer com o


seguinte, adicionando o método AsOrdered à consulta: ParallelQuery<int> PQ1 = from num
in collection1.AsParallel().
Conforme Pedido()
onde num % 5 == 0
selecione num;

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

LINQ Paralelo – PLINQ

3. Por fim, vamos fazer uma alteração semelhante no método btn3Method_Click e em sua instrução
de consulta:

ParallelQuery<int> PQ3 = coleção3.AsParallel().AsOrdered().


Onde(n => n % 10 == 0).Selecione(n => n);

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

LINQ Paralelo – PLINQ

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.

O método WithMergeOptions<TSource> pode informar ao PLINQ como executar o processo de


mesclagem de uma consulta simultânea.

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

LINQ Paralelo – PLINQ

A seguir estão os operadores de consulta paralela que oferecem suporte à mesclagem:

Referenciado em http://msdn.microsoft.com/en-us/library/dd997424(v=vs.110).aspx

Como fazer isso


Agora, vamos voltar ao nosso projeto e tentar alterar nosso programa para usar as opções de
mesclagem NotBuffered, AutoBuffered e FullyBuffered .

Para fazer isso, vamos abrir nosso projeto PLINQQuery e alterar a instrução de consulta paralela
no método btnMethod1_Click para ficar assim:

ParallelQuery<int> PQ1 = de num na coleção1.AsParallel().


AsOrdered().WithMergeOptions(ParallelMergeOptions.NotBuffered)
onde num % 5 == 0
selecione num;

[ 284 ]

www.it-ebooks.info
Machine Translated by Google

Capítulo 10

Agora, vamos construir e executar o programa e clicar no botão Execute Method 1 .


Você deve ver os seguintes resultados:

[ 285 ]

www.it-ebooks.info
Machine Translated by Google

LINQ Paralelo – PLINQ

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;

Agora, crie e execute o programa novamente. Clique no botão Execute Method


1 e você deve obter os seguintes resultados:

[ 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:

ParallelQuery<int> PQ1 = de num na coleção1.AsParallel().


AsOrdered().WithMergeOptions(ParallelMergeOptions.FullyBuffered)
onde num % 5 == 0
selecione num;

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

LINQ Paralelo – PLINQ

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 .

Como fazer isso


Agora, vamos abrir nosso projeto PLINQQuery novamente e desta vez vamos adicionar um botão
Cancelar e trabalhar com o método manipulador de eventos btnMethod2_Click :

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:

private void btnCancel_Click(objeto remetente,


RoutedEventArgs e)
{
cs.Cancelar();
}

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);

6. Por fim, vamos alterar o manipulador de eventos btnMethod2_Click para ter o


seguinte código para a consulta paralela:

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();

} catch (OperationCanceledException ex) {

lb2.Items.Clear();
lb2.Items.Add(ex.Message);
retornar;
}

[ 289 ]

www.it-ebooks.info
Machine Translated by Google

LINQ Paralelo – PLINQ

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:

CancellationTokenSource cs = new CancellationTokenSource();

Em seguida, criamos um botão Cancelar . e quando é pressionado, executamos o método Cancel() do


objeto token de cancelamento. Isso é feito aqui:

private void btnCancel_Click(objeto remetente, RoutedEventArgs e)


{
cs.Cancelar();
}

[ 290 ]

www.it-ebooks.info
Machine Translated by Google

Capítulo 10

Por fim, em nossa consulta paralela, adicionamos o método WithCancellation() e


passamos a ele o token de cancelamento. Em seguida, adicionamos um bloco catch e
capturamos quaisquer exceções OperationCanceledException :

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();

} catch (OperationCanceledException ex) {

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 Paralelo – PLINQ

O diagrama a seguir mostra o processo do LINQ:

LINQ

Cláusula Onde Resultados

Dados
Coleção

O diagrama a seguir mostra o processo de PLINQ. Observe como os dados são


particionados e a cláusula Where é executada simultaneamente em cada partiçã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

Primeiro, veremos o processamento ForAll :

Processamento Simultâneo

ForAll - Partição de processo 1

ForAll - Partição de processo 2

ForAll - Partição de processo 3

Dados
Coleção

ForAll - Partição de processo 4

Em segundo lugar, vamos ver o processo de processamento foreach . Observe no diagrama a


seguir que a mesclagem ocorre antes do processamento do loop foreach , portanto, o processamento
do foreach é feito sequencialmente:

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

LINQ Paralelo – PLINQ

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

Os detalhes dos outros dois algoritmos são os seguintes:

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

LINQ Paralelo – PLINQ

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.

No próximo capítulo, examinaremos as novas palavras-chave async e await e como implementar


facilmente métodos assíncronos.

[ 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.

O modelo de programação assíncrona usa a interface IAsyncResult para realizar o


mesmo tipo de design. Nesse padrão de design, você cria um delegado e, em seguida,
usa os métodos BeginInvoke e EndInvoke do delegado para iniciar o método e aguardar sua
conclusão. Você também pode usar a interface IAsyncResult que é retornada pelo método
BeginInvoke para pesquisar para ver se o método assíncrono foi concluído ou definir um
identificador de espera para que o método assíncrono seja concluído.

Esses dois métodos nos fornecem maneiras fáceis de implementar funcionalidades


semelhantes ao componente BackgroundWorker , que é muito usado em aplicativos
Windows Forms e versões anteriores do .NET. Essa funcionalidade é especialmente útil
em aplicativos de desktop quando não queremos que o thread principal do aplicativo seja
bloqueado. Existem poucas coisas piores no design de aplicativos de desktop do que ter
um usuário clicando em um botão e toda a interface do usuário congelar enquanto alguma
função está sendo executada.

www.it-ebooks.info
Machine Translated by Google

O modelo de programação assíncrona

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.

Neste capítulo, abordaremos os seguintes tópicos:

• O modelo de programação assíncrona • Usando


um método delegado AsyncCallback • As palavras-
chave async e await

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.

Agora, vamos ver um exemplo usando os métodos BeginRead e EndRead da classe


FileStream . Essa mesma técnica pode ser implementada usando suas próprias classes e
implementando a interface IAsyncResult . Você não precisa prefixar seus nomes de método com
Begin e End, mas é uma boa prática de programação seguir a convenção de nomenclatura para
que futuros desenvolvedores usando suas classes entendam intuitivamente como elas operam.

[ 298 ]

www.it-ebooks.info
Machine Translated by Google

Capítulo 11

Como fazer isso


Primeiro, vamos abrir o Visual Studio e criar um novo projeto de console chamado
FileReadAsync. Então, vamos realizar os seguintes passos:

1. Adicione duas instruções using para as classes IO e Threading , conforme mostrado aqui:
usando System.Threading; usando
System.IO;

2. Agora, vamos adicionar o seguinte código ao método Main :


public static void Main()
{
byte[] ArquivoData = new byte[1000];

FileStream FS = new FileStream("c:\\projetos\\


InputData.txt", FileMode.Open, FileAccess.Read, FileShare.Read, 1024, FileOptions.Asynchronous);

Console.WriteLine("Para iniciar a leitura assíncrona, pressione


retornar.");
Console.ReadLine();

Resultado IAsyncResult = FS.BeginRead(FileData, 0,


FileData.Length, nulo, nulo);

// Trabalho sendo feito enquanto esperamos a //leitura assíncrona.


Console.WriteLine("\r\n");
Console.WriteLine("Fazendo algum outro trabalho aqui. \r\n"); Console.WriteLine("\r\n");

//Chamar EndRead irá bloquear o thread principal //até


o trabalho assíncrono terminou.
int num = FS.EndRead(resultado);

FS.Close();

[ 299 ]

www.it-ebooks.info
Machine Translated by Google

O modelo de programação assíncrona

Console.WriteLine("Ler {0} bytes do arquivo. \r\n", num); Console.WriteLine("A


leitura assíncrona foi concluída - {0}. \r\n", result.IsCompleted.ToString());
Console.WriteLine(BitConverter.ToString(FileData));

Console.ReadLine();
}

3. Então, finalmente, precisamos adicionar um arquivo chamado InputData.txt a C:\projects.


Este será o arquivo que lemos de forma assíncrona. Pode conter qualquer texto que
você desejar; para nosso exemplo, o arquivo InputData.txt se parece com a captura de
tela a seguir:

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.

Como funciona Este é um

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 :

substituição pública IAsyncResult BeginRead( byte[] array,


int offset, int numBytes,

AsyncCallback userCallback,
Estado do objetoObjeto
)

[ 301 ]

www.it-ebooks.info
Machine Translated by Google

O modelo de programação assíncrona

Você também pode visualizar a definição do método para o método EndRead da seguinte maneira:

substituição pública int EndRead(


IAsyncResult asyncResult
)

Agora, vamos ver seu projeto de exemplo. Nas linhas de código a seguir, configuramos o objeto FileStream
chamado FS:

FileStream FS = new FileStream("c:\\projects\\InputData.txt", FileMode.Open, FileAccess.Read,

FileShare.Read, 1024, FileOptions.Asynchronous);

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, iniciamos a operação de leitura de forma assíncrona com o seguinte código:

Resultado IAsyncResult = FS.BeginRead(FileData, 0, FileData.


comprimento, nulo, nulo);

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 :

// Trabalho sendo feito enquanto esperamos a leitura assíncrona.


Console.WriteLine("\r\n");
Console.WriteLine("Fazendo algum outro trabalho aqui. \r\n"); Console.WriteLine("\r\n");

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:

Console.WriteLine("Ler {0} bytes do arquivo. \r\n",


num);
Console.WriteLine("A leitura assíncrona foi concluída - {0}. \r\n",
result.IsCompleted.ToString());

[ 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.

Uma terceira maneira de implementar o modelo de programação assíncrona envolve o uso de


um método delegado para processamento quando a operação de leitura for concluída.

Usando um método delegado AsyncCallback


Vimos como podemos usar o padrão de design APM implementado na classe FileStream para
executar uma leitura assíncrona e aguardar os resultados.
Também mencionamos como podemos pesquisar para ver se a leitura assíncrona foi concluída
em vez de bloquear o thread principal. Agora, veremos como podemos executar um método
delegado quando a leitura assíncrona for concluída.

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.

Vejamos o cabeçalho do método BeginRead da classe FileStream .


A seguir está a definição do método:

substituição pública IAsyncResult BeginRead( byte[] array,


int offset, int numBytes,

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

O modelo de programação assíncrona

Como fazer isso


Vamos abrir o Visual Studio e criar um novo aplicativo de console chamado
FileReadAsyncWithDelegate. Uma vez criado, seguiremos os seguintes passos:

1. Adicione duas instruções using para as classes IO e Threading , conforme mostrado:


usando System.Threading; usando
System.IO;

2. Em seguida, vamos adicionar o seguinte código ao método Main da nossa aplicação:


public static void Main()
{

FileStream FS = new FileStream("c:\\projects\\ InputData.txt",


FileMode.Open, FileAccess.Read, FileShare.Read, 1024, FileOptions.Asynchronous);

Console.WriteLine("Para iniciar a leitura assíncrona, pressione


retornar.");
Console.ReadLine();

Resultado IAsyncResult = FS.BeginRead(FileData, 0, FileData.Length,


ReadComplete, FS);

// Trabalho sendo feito enquanto esperamos a //leitura assíncrona.


Console.WriteLine("\r\n");
Console.WriteLine("Fazendo algum outro trabalho aqui. \r\n"); Console.WriteLine("\r\n");

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);

// Obtém o FileStream do objeto IAsyncResult.


FileStream FS = (FileStream)AResult.AsyncState;

// Obtenha os resultados da operação de leitura. int num =


FS.EndRead(AResult);

// Certifique-se de fechar o FileStream.


FS.Close();

//Agora, escreva os resultados.


Console.WriteLine("Ler {0} bytes do arquivo. \r\n", num); Console.WriteLine("A
leitura assíncrona foi concluída - {0}. \r\n", AResult.IsCompleted.ToString());

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.

Agora, crie e execute o aplicativo e você verá o seguinte:

[ 305 ]

www.it-ebooks.info
Machine Translated by Google

O modelo de programação assíncrona

Em seguida, pressione Enter e o aplicativo deve exibir os seguintes resultados:

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:

public static void Main() {

FileStream FS = new FileStream("c:\\projetos\\InputData.


txt", FileMode.Open, FileAccess.Read, FileShare.Read,
1024, FileOptions.Asynchronous);

Console.WriteLine("Para iniciar a leitura assíncrona, pressione Enter."); Console.ReadLine();

Resultado IAsyncResult = FS.BeginRead(FileData, 0, FileData.


Comprimento, ReadComplete, FS);

// Trabalho sendo feito enquanto esperamos a leitura assíncrona.


Console.WriteLine("\r\n");
Console.WriteLine("Fazendo algum outro trabalho aqui. \r\n"); Console.WriteLine("\r\n");

Console.ReadLine();
}

A chave aqui é a seguinte declaração:

Resultado IAsyncResult = FS.BeginRead(FileData, 0, FileData.Length, ReadComplete, FS);

Agora, em vez de o quarto e quinto parâmetros serem nulos, passamos um método


delegado e o objeto FileStream para esse método. Isso permite que o método Main
continue com seus negócios.

Agora, vamos ver onde o trabalho está sendo feito - o método delegado,
Ler Completo:

private static void ReadComplete(IAsyncResult AResult) {

// Escreva o id do thread que está executando o


ler.

[ 307 ]

www.it-ebooks.info
Machine Translated by Google

O modelo de programação assíncrona

Console.WriteLine("A operação de leitura está sendo executada no ID do encadeamento:


{0}.",
Thread.CurrentThread.ManagedThreadId);

// Obtém o FileStream do objeto IAsyncResult.


FileStream FS = (FileStream)AResult.AsyncState;

// Obtenha os resultados da operação de leitura. int num =


FS.EndRead(AResult);

// Certifique-se de fechar o FileStream.


FS.Close();

//Agora, escreva os resultados.


Console.WriteLine("Ler {0} bytes do arquivo. \r\n",
num);
Console.WriteLine("A leitura assíncrona foi concluída - {0}. \r\n",
AResult.IsCompleted.ToString());
Console.WriteLine(BitConverter.ToString(FileData));
}

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.

Em seguida, obtemos o estado do objeto FileStream usando a seguinte instrução:

FileStream FS = (FileStream)AResult.AsyncState;

Em seguida, esperamos a conclusão da leitura e obtemos os resultados usando esta instrução:

int num = FS.EndRead(AResult);

E, finalmente, processamos os resultados usando estas declarações:

Console.WriteLine("Ler {0} bytes do arquivo. \r\n",


num);
Console.WriteLine("A leitura assíncrona foi concluída - {0}. \r\n",
AResult.IsCompleted.ToString());
Console.WriteLine(BitConverter.ToString(FileData));

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

• Chame o nome do método Begin , continue o processamento e verifique periodicamente


a propriedade IsCompleted do objeto IAsyncResult para saber quando a operação
assíncrona é concluída
• Chame o nome do método Begin para iniciar a operação assíncrona e, em seguida,
implemente um método delegado em um thread separado para aguardar a
operação assíncrona e processar os resultados

No restante do capítulo, exploraremos as novas palavras-chave async e await que foram


introduzidas no .NET versão 4.5.1.

As palavras-chave async e await


No .NET 4.5, a Microsoft introduziu as palavras-chave async e await que tornaram
muito fácil para os desenvolvedores implementar a funcionalidade assíncrona em seus
métodos. Adicionar a palavra-chave async ao cabeçalho do método informa ao CLR
do .NET para executar esse método em um thread separado no pool de threads se
determinar que as duas condições a seguir são atendidas: primeiro, que executá-lo em um
thread separado aumentará o desempenho e, segundo, a palavra-chave await é usada no
método. Um método assíncrono retornará void, Task ou Task<TResult>. Além disso, a
convenção de nomenclatura é postfixar qualquer método que use a palavra-chave async com Async.
Portanto, o nome do método deve ser MyMethodAsync. Isso é tudo. A implementação
é semelhante ao APM, mas não usa a interface IAsyncResult nem a implementa.

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

O modelo de programação assíncrona

Como fazer isso


Primeiro, vamos abrir o Visual Studio e criar um novo aplicativo de console
chamado FileReadUsingAsync. Depois de criar este projeto, execute as seguintes etapas:

1. Adicione duas instruções using para as classes IO e Threading , como a seguir:


usando System.Threading; usando
System.IO;

2. Em seguida, vamos adicionar as seguintes instruções ao método Main :


static void Main() {

Console.WriteLine("O ID do método Main: {0}.


\r\n",
Thread.CurrentThread.ManagedThreadId);

//Aguarda o usuário iniciar a leitura do //arquivo.


Console.ReadLine();

// Cria a tarefa, inicia-a e espera //concluir.


Tarefa tarefa = new Task(ProcessFileAsync); tarefa.Iniciar();

tarefa.Wait();

//Espera retorno antes de sair.


Console.ReadLine();
}

3. Em seguida, criaremos o método ProcessFileAsync que será executado dentro da


tarefa que criamos no método Main . Esse método implementará a palavra-chave
async e será executado de forma assíncrona. Adicione o seguinte código para criar
este método:
static async void ProcessFileAsync() {

// Escreva o id do thread da tarefa //que


chamará o método assíncrono para ler o arquivo.
Console.WriteLine("O ID do encadeamento do
Método ProcessFileAsync: {0}. \r\n",
Thread.CurrentThread.ManagedThreadId);

// Inicia o método HandleFile.


Task<String> task = ReadFileAsync("C:\\projects\\ InputData.txt");

// Execute algum outro trabalho.

[ 310 ]

www.it-ebooks.info
Machine Translated by Google

Capítulo 11

Console.WriteLine("Faça algum outro trabalho. \r\n");

Console.WriteLine("Aguarde a conclusão da leitura. \r\n"); Console.ReadLine();

// Espera que a tarefa termine de ler o //arquivo.


String resultados = aguarda tarefa;
Console.WriteLine("Número de caracteres lidos são: {0}. \r\n", results.Length);

Console.WriteLine("O conteúdo do arquivo é: {0}. \r\n",


resultados); }

4. Agora, esse método chamará outro método assíncrono que realmente


abrirá o arquivo e lerá o conteúdo. Este método, ReadFileAsync, terá o
seguinte código:
Tarefa assíncrona estática<String> ReadFileAsync(arquivo de string)
{
// Escreva o id da thread que está // executando
a lida.
Console.WriteLine("O ID do encadeamento do ReadFileAsync
método: {0}. \r\n",
Thread.CurrentThread.ManagedThreadId);

Console.WriteLine("Iniciar a leitura do arquivo de forma assíncrona.


\r\n");

// Lê o arquivo especificado.
String DadosLeitura = ""; usando
(leitor StreamReader = novo StreamReader(arquivo)) {

string caractere = await reader.ReadToEndAsync();

//Construir string de leitura de dados.


DataRead = DataRead + caractere;

//Desacelera o processo.
System.Threading.Thread.Sleep(10000);

Console.WriteLine("Concluída a leitura do arquivo de forma assíncrona.


\r\n");
retornar DataRead;
}

[ 311 ]

www.it-ebooks.info
Machine Translated by Google

O modelo de programação assíncrona

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:

Agora, iniciamos a leitura assíncrona do arquivo, realizamos algum outro trabalho


e estamos aguardando a conclusão da leitura do arquivo. Depois de concluído, devemos
ver a seguinte saída:

[ 312 ]

www.it-ebooks.info
Machine Translated by Google

Capítulo 11

Agora, se pressionarmos return novamente, veremos os resultados da operação de leitura:

Finalmente, se pressionarmos o retorno uma última vez, o aplicativo será finalizado e encerrado.
Então, como isso tudo funcionou?

Como funciona Agora, vamos

examinar como esse programa funciona. Primeiro, dê uma olhada no método Main :

static void Main()


{
Console.WriteLine("O ID do método Main: {0}. \r\n", Thread.CurrentThread.ManagedThreadId);

//Aguarda o usuário iniciar a leitura do arquivo.


Console.ReadLine();

// Criar tarefa, iniciá-la e esperar que ela termine.


Tarefa tarefa = new Task(ProcessFileAsync); tarefa.Iniciar();

[ 313 ]

www.it-ebooks.info
Machine Translated by Google

O modelo de programação assíncrona

tarefa.Wait();

//Espera retorno antes de sair.


Console.ReadLine();
}

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.

Agora, o método assíncrono ProcessFileAsync é executado em um thread separado do método


Main . Isso é mostrado exibindo o número de ID do encadeamento. Você notará na saída do console
que a ID do thread principal é diferente da ID do thread da tarefa que executa o método
ProcessFileAsync . Vamos examinar este método. O seguinte é o código para dissecarmos:

static async void ProcessFileAsync() {

// Escreva o id do thread da tarefa que chamará o método assíncrono para ler o arquivo.

Console.WriteLine("O ID do encadeamento do ProcessFileAsync


método: {0}. \r\n",
Thread.CurrentThread.ManagedThreadId);

// Inicia o método HandleFile.


Task<String> task = ReadFileAsync("C:\\projects\\ InputData.txt");

// Execute algum outro trabalho.


Console.WriteLine("Faça algum outro trabalho. \r\n");

Console.WriteLine("Aguarde a conclusão da leitura. \r\n"); Console.ReadLine();

// Espera a tarefa terminar de ler o arquivo.


String resultados = aguarda tarefa;
Console.WriteLine("Número de caracteres lidos são: {0}. \r\n", results.Length);

Console.WriteLine("O conteúdo do arquivo é: {0}. \r\n",


resultados);
}

[ 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:

Task<String> task = ReadFileAsync("C:\\projects\\InputData.txt");

Aguardamos então esta tarefa com a seguinte declaração:

String resultados = aguarda tarefa;

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.

O coração do trabalho é feito no método ReadFileAsync . Vejamos este método:

static async Task<String> ReadFileAsync(arquivo de string) {

// Escreva o id do thread que está executando o


ler.
Console.WriteLine("O ID do encadeamento do ReadFileAsync
método: {0}. \r\n",
Thread.CurrentThread.ManagedThreadId);

Console.WriteLine("Iniciar a leitura do arquivo de forma assíncrona.


\r\n");

// Lê o arquivo especificado.
String DadosLeitura = ""; usando
(leitor StreamReader = novo StreamReader(arquivo)) {

string caractere = await reader.ReadToEndAsync();

//Construir string de leitura de dados.


DataRead = DataRead + caractere;

//Desacelera o processo.
System.Threading.Thread.Sleep(10000);

Console.WriteLine("Concluída a leitura do arquivo de forma assíncrona.


\r\n");
retornar DataRead;
}

[ 315 ]

www.it-ebooks.info
Machine Translated by Google

O modelo de programação assíncrona

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:

string caractere = await reader.ReadToEndAsync();

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:

Console.WriteLine("Aguarde a conclusão da leitura. \r\n"); Console.ReadLine();

Console.WriteLine("Concluída a leitura do arquivo de forma assíncrona. \r\n");

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.

As palavras-chave async e await são uma maneira simples de designar na assinatura de


um método que ele deve ser executado como uma operação assíncrona. A palavra-chave async
designa o método como um método assíncrono e a palavra-chave await é usada dentro do método
para aguardar a conclusão de uma instrução antes que seu método seja concluído. Essa técnica
pode ser implementada em uma classe ou como métodos estáticos em um aplicativo ou classe
auxiliar. Essas duas técnicas fornecem ao desenvolvedor .NET mais duas ferramentas em sua caixa
de ferramentas para implementar a funcionalidade simultânea.

Ao longo deste livro, exploramos muitas maneiras de implementar funcionalidades


simultâneas e assíncronas no .NET. Isso inclui técnicas que existem desde o .NET 1.0 até as
técnicas introduzidas no .NET 4.5 mais recente. Eles variam de métodos assíncronos simples
que não permitem o bloqueio de um thread principal da interface do usuário a objetos para
projetar aplicativos simultâneos completos, a objetos para processar conjuntos de dados
simultaneamente. Há muitas maneiras e técnicas de executar a funcionalidade multithread
no .NET, dependendo dos requisitos de seu aplicativo ou tarefa específica.

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

Unidade Lógica e Aritmética (ALU) 10


multiprocessamento assimétrico (AMP ou
saturação 112
ASMP) 11
Método delegado AsyncCallback design de
usando 303-309 simultaneidade para
23, 24 coleções simultâneas 146-149
execução assíncrona 47
feedback de interface do usuário
Modelo de programação assíncrona (APM) sobre
297 implementando 298-303 palavra-chave simultâneo 103 cancelamento
cooperativo 179 fatores de custos de
assíncrona demonstrando 309-316
coordenação 25 núcleo 8

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

tarefa com falha 180 expressões lambda 157, 158


Função de filtro de imagem 24 simultaneidade leve 71
Função Localizar Todas as Referências 221 LINQ para objetos 271
Coluna sinalizadora, janela Tópicos 216 Função Carregar Imagem 24
Coluna Flags, janela Tarefas 218 Coluna de localização, janela Tarefas 218
Unidade de Ponto Flutuante (FPU) 10 Coluna de localização, janela de threads 216
Sobrecargas ForEach condição de bloqueio 214
Função Action<TLocal> 207
Tipo de dados ForEach<TSource, TLocal> 207 M
Func<TLocal> função 207
Func<TSource, ParallelLoopState, TLocal, ciclo da máquina 21
TLocal> função 207 Coluna ID gerenciada, janela Threads 216 thread
Coleção IEnumerable<TSource> 207 gerenciado 216 fusão, em PLINQ 282-288 blocos de
Barramento frontal (FSB) 17 mensagens examinando 255, 256 sistemas
monoprocessadores cerca de 8 núcleos de
G processamento único 9 multicore 16 componentes
BackgroundWorker múltiplos trabalhando com 58-66
Método GetBrightness() 112 processadores de vários núcleos 13
Método GetHue() 112
Método GetSaturation() 112
Lei de Gustafson 31, 32

[ 320 ]

www.it-ebooks.info
Machine Translated by Google

múltiplos núcleos de execução Janela Parallel Watch sobre


vantagem, tendo de 15-18 213 usando 222, 223
processadores múltiplos parâmetros passando,
analisando, fatores 29 para threads 95-102
conclusão de tarefas múltiplas
aguardando 165-173 Coluna pai, Janela de tarefas 218
multiplexação 9 sistemas melhorias de desempenho estimando
multiprocessadores 11, 12 25-28 melhorias de desempenho,
aplicativos multithreaded cerca de PLINQ 291-295 fatores de incremento de desempenho
213 considerações, para depuração 25
214 sistemas multiusuários 9
CPUs físicas, núcleos lógicos 14
Padrão de design de
N pipeline sobre 241
aplicativos, projetando com 242-255
Coluna Nome, janela Threads 216 pipelines sobre 105, 106 aplicativos de
Telescópio Espacial Spitzer da NASA processamento de imagem, usando
URL 114
composições de cores de 107-112 pixels
multiprocessamento simétrico n-way 11 112, 113

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

exceções 197-200, manipulando em URL, para baixar 67 núcleos


201-206 processando variáveis de processamento 16
Coluna Nome do processo, janela Threads 217
locais de thread 184-189, usando em
206-211 representação esquemática padrão de design produtor-consumidor sobre 241
aplicativo, projetando com 260-269 progresso
de design de pipeline paralelo 24 Janela
Pilhas Paralelas sobre 213 usando 219-222 exibindo 50-55

[ 321 ]

www.it-ebooks.info
Machine Translated by Google

R manipulação de exceção de tarefa 180, 181


Classe TaskFactory
condição de corrida explorando a Biblioteca
213 refatoração 66 Paralela de Tarefas 149-152. Veja
recursos tarefas TPL explorando 139-142 com
bloqueio, para garantir dados thread-safe valores de retorno 143-146
128-130 abordagens de tempo de
resposta , para evitar 13 agendador de tarefas 153
Janela de tarefas
tarefas de valores sobre 213
de retorno com 143-146 Coluna APPDomain 219
Coluna AsyncState 219
S Coluna de bandeiras 218
Coluna de ícones 218
Escala Função de imagem 24 coluna ID 218
Agendador 21 Operação
Coluna de localização 218
seqüencial Representação Coluna pai 218
esquemática 24 Serviço 21 Sinais, Coluna de status 218
verificação entre threads , com WaitHandle
Coluna de atribuição de rosca 218 usando
classe 126 Manipulação 121-125
217
Manipulação, com AutoResetEvent classe 125,
Coluna de atribuição de thread, janela de
126 tarefas 218
Classe de discussão

threads, criando com 72-78 variáveis


trabalhando 125 locais de thread usando, em loops
núcleo único 9
paralelos 206-211
instrução única, dados únicos (SISD) 8 aplicativo Grupo de discussão
de software 21 processo de software 21 URL 154
threads
Coluna de status, janela de tarefas sobre 21
Ativo 218
aplicações, criando com 78-85 criando,
Concluído 218
com Thread class 72-78 dados,
Impasse 218
compartilhando entre 86-95 juntando 127
Agendado 218
parâmetros, passando para 97-102
Esperando 218
pausando 113-121 reiniciando 113-121
Coluna Suspended Count, janela Threads
usado, para tratamento de erros 131-136
217 multiprocessamento simétrico
tipos de segurança de rosca 84
(SMP) 11 mecanismos de sincronização 111

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

obrigado por comprar


C# Programação Multithread e Paralela

Sobre a Editora Packt


Packt, pronuncia-se 'packed', publicou seu primeiro livro, Mastering phpMyAdmin for Effective MySQL Management, em abril
de 2004, e subsequentemente continuou a se especializar na publicação de livros altamente focados em tecnologias e
soluções específicas.

Nossos livros e publicações compartilham as experiências de seus colegas profissionais de TI na adaptação e


personalização dos sistemas, aplicativos e estruturas atuais. Nossos livros baseados em soluções fornecem o conhecimento
e o poder para personalizar o software e as tecnologias que você está usando para realizar o trabalho. Os livros Packt são
mais específicos e menos gerais do que os livros de TI que você viu no passado. Nosso modelo de negócios exclusivo nos
permite trazer informações mais focadas, dando a você mais do que você precisa saber e menos do que você não precisa.

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.

Sobre a Packt Enterprise


Em 2010, a Packt lançou duas novas marcas, Packt Enterprise e Packt Open Source, para continuar seu foco na
especialização. Este livro faz parte da marca Packt Enterprise, lar de livros publicados sobre software empresarial –
software criado por grandes fornecedores, incluindo (mas não limitado a) IBM, Microsoft e Oracle, muitas vezes para uso
em outras corporações. Seus títulos oferecerão informações relevantes para uma variedade de usuários deste software,
incluindo administradores, desenvolvedores, arquitetos e usuários finais.

Escrevendo para Packt


Congratulamo-nos com todas as perguntas de pessoas interessadas em autoria. As propostas de livros devem ser
enviadas para author@packtpub.com. Se a ideia do seu livro ainda está em um estágio inicial e você gostaria de discuti-la
primeiro antes de escrever uma proposta formal de livro, entre em contato conosco; um de nossos editores de
comissionamento entrará em contato com você.

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

Programação paralela com


Pitão
ISBN: 978-1-78328-839-7 Brochura: 128 páginas

Desenvolva sistemas paralelos eficientes usando o robusto


Ambiente Python

1. Demonstra os conceitos de programação


paralela Python.

2. Aumenta seus recursos de computação Python.

3. Contém explicações fáceis de entender e muitos exemplos.

Programação Paralela OpenCL


Livro de receitas de desenvolvimento
ISBN: 978-1-84969-452-0 Brochura: 302 páginas

Acelere seus aplicativos e entenda a computação


de alto desempenho com mais de 50
receitas OpenCL

1. Aprenda sobre programação paralela


desenvolvimento em OpenCL e também as
várias técnicas envolvidas na escrita de código
de alto desempenho.

2. Saiba mais sobre desenvolvimento paralelo


de dados ou paralelo de tarefas e também
sobre a combinação de ambos.

3. Entenda e explore o subjacente


recursos de hardware como registradores de
processador e caches que executam potencialmente
dezenas de milhares de threads nos processadores.

Verifique www.PacktPub.com para obter informações sobre nossos títulos

www.it-ebooks.info
Machine Translated by Google

C++ Multithreading Cookbook


ISBN: 978-1-78328-979-0 Brochura: 422 páginas

Mais de 60 receitas para ajudá-lo a criar


aplicativos multithread ultrarrápidos usando C++ com
regras, diretrizes e práticas recomendadas

1. Crie aplicativos multithread usando o poder do C++.

2. Atualize seus aplicativos com execução paralela em


etapas fáceis de entender.

3. Mantenha-se atualizado com as novas tarefas


simultâneas do Windows 8.

4. Evite problemas clássicos de sincronização.

Multithreading em C#
5.0 Cookbook
ISBN: 978-1-84969-764-4 Brochura: 268 páginas

Mais de 70 receitas para ajudá-lo a aprender


programação assíncrona e paralela com C# 5.0 de
forma rápida e eficiente

1. Aprofunde-se na infra-estrutura de
encadeamento .NET e use a Biblioteca Paralela
de Tarefas para programação assíncrona.

2. Dimensione seus aplicativos de servidor com eficiência.

3. Suporte a linguagem de operações assíncronas Master


C# 5.0.

Verifique www.PacktPub.com para obter informações sobre nossos títulos

www.it-ebooks.info
Machine Translated by Google

www.it-ebooks.info

Você também pode gostar