C#
- Threads : Conceitos Básicos (revisão)
Uma thread (linha de execução) é uma seqüência de instruções que podem ser executadas simultaneamente com outras sequências de instruções.
Um programa que permite mais de uma thread em execução simultânea é chamado de multithreaded.
Por exemplo, afim de importar um arquivo grande, e, ao mesmo tempo permitir que o usuário clique em Cancelar para abortar o processo, o desenvolvedor cria uma nova thread (segmento adicional) para executar a importação. Ao realizar a importação em uma thread diferente (segmento diferente), o usuário pode solicitar o cancelamento, em vez de congelar a interface do usuário até que a importação termine.
Um sistema operacional simula múltiplas threads em execução simultânea através de um mecanismo conhecido como tempo de corte. Mesmo com múltiplos processadores, há geralmente uma demanda por mais threads do que os processadores existentes, e como resultado, o tempo de corte ocorre.
O Tempo de Corte é um mecanismo através do qual o sistema operacional alterna a execução a partir de uma thread(sequencia de instruções) para a próxima thread tão rapidamente que parece que as threads estão em execução simultânea. O período de tempo no qual o processador executa uma thread específica antes de mudar para outra é o tempo de corte ou quantum.
Em geral uma thread esta à espera de vários eventos, tais como uma operação de I/O, um processamento na memória, etc., a mudança para uma thread diferente resulta em uma execução mais eficiente, do que o processador ficar esperando, "de braços cruzados", para a concluir a operação.
No entanto, a passagem de uma thread para uma outra thread cria alguma sobrecarga no sistema.
Se existem muitas threads, a sobrecarga de comutação começa a afetar o desempenho notavelmente, e a inclusão de threads adicionais irá diminuir o desempenho; o processador gasta tempo alternando de uma thread para outra , em vez de concluir o trabalho de cada thread.
Atomicidade
Considere o código que transfere dinheiro de uma conta bancária. Em primeiro lugar, o código verifica se existem fundos suficientes, se há, a transferência ocorre.
Se após a verificação dos fundos, uma thread diferente remove os fundos, uma transferência inválida pode ocorrer quando a execução retorna para o segmento inicial.
Controlar o acesso à conta, de modo que apenas uma thread possa acessar a conta em um momento, resolve o problema, e, faz com que uma transferência seja atómica.
Um conjunto de operações é atômica se uma das seguintes condições for verdade:
Infelizmente, a complexidade aumenta porque a maioria das declarações C# não são atômicas. Um contador onde temos o código Contar++, por exemplo, é uma instrução simples em C#, mas que se traduz em múltiplas instruções para o processador:
1. O
processador lê os dados no Contador;
2. O processador calcula o novo valor;
3. O contador tem um novo valor atribuído;
Depois disso os dados são acessados, mas antes do novo valor ser atribuído, uma thread diferente pode modificar o valor original (talvez também verificar o valor antes de modificá-lo), criando uma condição de corrida porque o valor no Contador, pelo menos na perspectiva do contador da thread, mudou inesperadamente.
DeadLock
Para evitar condições de corrida as linguagens fornecem a capacidade de restringir blocos de código a um número especificado de threads.(geralmente uma).
No entanto, se a ordem de aquisição de bloqueio entre as threads variar, um deadlock(impasse) poderia ocorrer de forma a congelar as threads, onde teríamos cada uma esperando pela outra para liberar o bloqueio.
Assim, um Deadlock (interbloqueio,blocagem, impasse), no contexto do sistemas operacionais (SO), caracteriza uma situação em que ocorre um impasse onde dois ou mais processos ficam impedidos de continuar suas execuções, ou seja, ficam bloqueados. Trata-se de um problema bastante estudado no contexto dos Sistemas Operacionais, assim como em outras disciplinas, como banco de dados, pois é inerente à própria natureza desses sistemas.
O deadlock ocorre com um conjunto de processos e recursos não preemptíveis, onde um ou mais processos desse conjunto está aguardando a liberação de um recurso por um outro processo que, por sua vez, aguarda a liberação de outro recurso alocado ou dependente do primeiro processo.
Preempetivo - Esquema de processamento computacional onde o kernel tem o controle do tempo que será usado por cada processo, e tem o poder de tomar de volta este tempo e dá-lo para outro processo segundo seu esquema de prioridades (http://www.dicionarioinformal.com.br/preemptivo/) |
A definição textual de deadlock normalmente, por ser muito abstrata, é mais difícil de se compreender do que a representação por grafos, que será resumida mais adiante. No entanto, algumas observações são pertinentes:
Exemplo: DeadLock - Uma thread fica aguardando pela outra;
![]() |
O problema com o código que não é atômico ou que provoca deadlocks(impasses) é que ele depende da ordem na qual as instruções do processador entre as várias threads ocorram.
Esta dependência introduz uma incerteza relacionada com a execução do programa; A ordem na qual uma instrução será executada relativa a uma instrução em uma thread diferente é desconhecida.
Muitas vezes, o código irá parecer se comportar de maneira uniforme, mas, ocasionalmente, não vai, e este é o cerne da programação multithreaded.
Como tais condições de corrida são difíceis de se replicar em laboratório, muito da qualidade de garantia de código multithreaded depende de testes de longa duração de estresse.
O sistema operacional implementa threads e fornece várias APIs não gerenciadas para criar e gerenciar essas threads.
Tratando Tarefas (Tasks)
O CLR envolve estas threads não gerenciadas e as expõe em código gerenciado através da classe System.Threading.Tasks.Task, que representa uma operação assíncrona.
O namespace System.Threading.Tasks fornece tipos que simplificam o trabalho de escrever código simultâneo e assíncrono.
Os principais tipos são System.Threading.Tasks.Task que representa uma operação assíncrona que pode ser aguardada e cancelada, e System.Threading.Tasks.Task(Of TResult), que é uma tarefa que pode retornar um valor.
A classe Factory fornece métodos estáticos para criar e iniciar tarefas e a classe System.Threading.Tasks.TaskScheduler fornece a thread padrão de planejamento da infraestrutura.
No entanto, uma tarefa não é mapeada diretamente para uma thread não gerenciada. Em vez disso, a tarefa fornece um grau de abstração para a thread subjacente não gerenciada.
A criação de uma thread é uma operação relativamente custosa.
Portanto, sempre que você pode reutilizar uma thread entre dois ou mais conjuntos de instruções (Ao invés de recriar o segmento para cada conjunto) a execução global é potencialmente mais eficiente.
Na plataforma .NET , em vez de criar uma thread do sistema cada vez que uma tarefa é criada, a tarefa solicita uma thread do pool de threads. O pool de threads avalia se vai criar uma thread nova ou se aloca uma thread existente para a tarefa requisitada.
Ao abstrair o conceito de uma thread em tarefas, a API multithreading da plataforma .NET reduz as complexidades do gerenciamento eficiente da thread, ou seja, quando cria uma nova thread do sistema operacional e quando realiza a reutilização de um já existente.
Da mesma forma, o comportamento interno da Tarefa(Task)(via System.Threading.ThreadPool) gerencia quando retornar uma thread para o pool de threads para reutilização posterior e quando desalocar uma thread e liberar quaisquer recursos que estejam sendo consumidos.
O trabalho de programação da tarefa envolve atribuir o conjunto de instruções que a tarefa irá executar e então iniciar a tarefa.
A atribuição destas instruções é fortemente dependente de delegados. A seguir temos o código de um exemplo simples sobre este assunto:
1- Iniciando um método em uma nova thread
using System; using System.Threading.Tasks; namespace Threads { class Program { static void Main(string[] args) { const int repeticoes = 10000; Task task = new Task(() => { for (int contador = 0; contador < repeticoes; contador++) { Console.Write('x'); } }); task.Start(); for (int contador = 0; contador < repeticoes; contador++) { Console.Write('#'); } // aguarda a tarefa terminar task.Wait(); } } } |
![]() |
O código que é executado
em uma nova thread é definido no delegado (do tipo Action
neste caso) e passado para o construtor Task().
Esse delegado (na forma de uma expressão lambda)
imprime um xis (x) no console repetidamente durante cada
iteração dentro de um laço.
O próximo laço for, após a declaração Task().Start, é praticamente idêntico, exceto que ele exibe no console uma cerquilha (#).
A saída resultante do programa é uma série de x até a próxima troca da thread no contexto e assim por diante.
1Ts 2:8
Assim nós, sendo-vos tão afeiçoados, de boa vontade desejávamos comunicar-vos não somente o evangelho de Deus, mas ainda as nossas próprias almas; porquanto vos tornastes muito amados de nós.1Ts 2:9
Porque vos lembrais, irmãos, do nosso labor e fadiga; pois, trabalhando noite e dia, para não sermos pesados a nenhum de vós, vos pregamos o evangelho de Deus.1Ts 2:10
Vós e Deus sois testemunhas de quão santa e irrepreensivelmente nos portamos para convosco que credes;1Ts 2:11
assim como sabeis de que modo vos tratávamos a cada um de vós, como um pai a seus filhos,1Ts 2:12
exortando-vos e consolando-vos, e instando que andásseis de um modo digno de Deus, o qual vos chama ao seu reino e glória.Referências: