10. Os threads de execução
10.1. A classe Thread
Quando se inicia um aplicativo, ele é executado em um fluxo de execução chamado thread. A classe .NET que modela um thread é a classe System.Threading.Thread e possui a seguinte definição:
Construtores
![]() |
Nos exemplos a seguir, utilizaremos apenas os construtores [1,3]. O construtor [1] aceita como parâmetro um método com a assinatura [2], c.a.d, que possui um parâmetro do tipo object e não retorna nenhum resultado. O construtor [3] aceita como parâmetro um método com a assinatura [4], c.a.d, que não possui parâmetros e não retorna nenhum resultado.
Propriedades
Algumas propriedades úteis:
- Thread CurrentThread: propriedade estática que fornece uma referência à thread na qual se encontra o código que solicitou essa propriedade
- string Name: o nome da thread
- bool IsAlive: indica se o thread está em execução ou não.
Métodos
Os métodos mais utilizados são os seguintes:
- Start(), Start(object obj): inicia a execução assíncrona do thread, eventualmente passando informações a ele em um tipo object.
- Abort(), Abort(object obj): para encerrar forçosamente um thread
- Join(): o thread T1 que executa T2.Join fica bloqueado até que o thread T2 seja concluído. Existem variantes para encerrar a espera após um tempo determinado.
- Sleep(int n): método estático — o thread que executa o método é suspenso por n milissegundos. Ele perde então o processador, que é cedido a outro thread.
Vejamos uma primeira aplicação que destaca a existência de um thread principal de execução, aquele no qual é executada a função Main de uma classe:
using System;
using System.Threading;
namespace Chap8 {
class Program {
static void Main(string[] args) {
// inicialização da thread atual
Thread main = Thread.CurrentThread;
// exibição
Console.WriteLine("Thread courant : {0}", main.Name);
// alteração do nome
main.Name = "main";
// verificação
Console.WriteLine("Thread courant : {0}", main.Name);
// loop infinito
while (true) {
// exibição
Console.WriteLine("{0} : {1:hh:mm:ss}", main.Name, DateTime.Now);
// parada temporária
Thread.Sleep(1000);
}//while
}
}
}
- linha 8: obtém-se uma referência à thread na qual o método [main] é executado
- linhas 10-14: exibe-se e altera-se o nome do thread
- linhas 17-22: um loop que exibe a informação a cada segundo
- linha 21: o thread no qual o método [main] está sendo executado será suspenso por 1 segundo
Os resultados na tela são os seguintes:
- linha 1: o thread atual não tinha nome
- linha 2: agora ele tem um
- linhas 3-7: a exibição que ocorre a cada segundo
- linha 8: o programa é interrompido com Ctrl-C.
10.2. Criação de threads de execução
É possível ter aplicativos em que trechos de código são executados de forma “simultânea” em diferentes threads de execução. Quando se diz que os threads são executados simultaneamente, muitas vezes trata-se de um uso incorreto do termo. Se a máquina tiver apenas um processador, como ainda é comum, os threads compartilham esse processador: cada um deles tem acesso a ele, por sua vez, por um breve instante (alguns milissegundos). É isso que dá a ilusão de paralelismo de execução. O tempo concedido a um thread depende de vários fatores, entre os quais sua prioridade, que tem um valor padrão, mas que também pode ser definida por programação. Quando um thread dispõe do processador, ele normalmente o utiliza durante todo o tempo que lhe foi concedido. No entanto, ele pode liberá-lo antes do tempo:
- entrando em espera por um evento (Wait, Join)
- entrando em modo de suspensão por um período determinado (Sleep)
- Um thread T é criado inicialmente por um dos construtores apresentados acima, por exemplo:
onde Start é um método com uma das duas assinaturas a seguir:
A criação de um thread não o inicia.
- A execução da thread T é iniciada por T.Start(): o método Start passado ao construtor de T será então executado pela thread T. O programa que executa a instrução T.Start() não aguarda o término da tarefa T: ele passa imediatamente para a instrução seguinte. Temos, então, duas tarefas sendo executadas em paralelo. Frequentemente, elas precisam se comunicar entre si para saber em que ponto está o trabalho comum a ser realizado. Esse é o problema da sincronização de threads.
- Uma vez iniciado, o thread T é executado de forma autônoma. Ele será interrompido quando o método Start que ele está executando tiver concluído seu trabalho.
- É possível forçar o thread T a encerrar:
- T.Abort() solicita que o thread T seja encerrado.
- Também é possível aguardar o término de sua execução por meio de T.Join(). Trata-se de uma instrução bloqueante: o programa que a executa fica bloqueado até que a tarefa T tenha concluído seu trabalho. Essa é uma forma de sincronização.
Vamos examinar o seguinte programa:
using System;
using System.Threading;
namespace Chap8 {
class Program {
public static void Main() {
// inicialização do thread atual
Thread main = Thread.CurrentThread;
// atribuição de nome à thread
main.Name = "Main";
// criação de threads de execução
Thread[] tâches = new Thread[5];
for (int i = 0; i < tâches.Length; i++) {
// criação da thread i
tâches[i] = new Thread(Affiche);
// definindo o nome do thread
tâches[i].Name = i.ToString();
// inicia-se a execução do thread i
tâches[i].Start();
}
// fim da rotina
Console.WriteLine("Fin du thread {0} à {1:hh:mm:ss}",main.Name,DateTime.Now);
}
public static void Affiche() {
// exibição do início da execução
Console.WriteLine("Début d'exécution de la méthode Affiche dans le Thread {0} : {1:hh:mm:ss}",Thread.CurrentThread.Name,DateTime.Now);
// suspensão por 1 s
Thread.Sleep(1000);
// exibição do fim da execução
Console.WriteLine("Fin d'exécution de la méthode Affiche dans le Thread {0} : {1:hh:mm:ss}", Thread.CurrentThread.Name, DateTime.Now);
}
}
}
- linhas 8-10: atribui-se um nome à thread que executa o método [Main]
- linhas 13-21: criam-se 5 threads e elas são executadas. As referências das threads são armazenadas em um array para que possam ser recuperadas posteriormente. Cada thread executa o método Affiche das linhas 27-35.
- linha 20: o thread nº i é iniciado. Essa operação é não bloqueante. O thread nº i será executado em paralelo ao thread do método [Main] que o iniciou.
- linha 24: o thread que executa o método [Main] é encerrado.
- linhas 27-35: o método [Affiche] realiza exibições. Ele exibe o nome do thread que o executa, bem como as horas de início e término da execução.
- linha 31: qualquer thread que esteja executando o método [Affiche] será interrompido por 1 segundo. O processador será então cedido a outro thread que esteja aguardando o processador. Ao final do segundo de pausa, o thread que estava parado voltará a ser candidato ao processador. Ele o receberá quando chegar sua vez. Isso depende de vários fatores, incluindo a prioridade dos outros threads que aguardam o processador.
Os resultados são os seguintes:
Esses resultados são muito esclarecedores:
- vemos, em primeiro lugar, que o início da execução de um thread não é bloqueante. O método Main iniciou a execução de 5 threads em paralelo e concluiu sua execução antes deles. A operação
inicia a execução do thread tarefas[i], mas, feito isso, a execução prossegue imediatamente com a instrução seguinte, sem aguardar o término da execução do thread.
- Todos os threads criados devem executar o método Affiche. A ordem de execução é imprevisível. Mesmo que, no exemplo, a ordem de execução pareça seguir a ordem das solicitações de execução, não se pode tirar conclusões gerais a partir disso. O sistema operacional possui, neste caso, 6 threads e um processador. Ele distribuirá o processador entre essas 6 threads de acordo com regras próprias.
- Nos resultados, observa-se uma consequência do método Sleep. No exemplo, é o thread 0 que executa primeiro o método Affiche. A mensagem de início de execução é exibida e, em seguida, ele executa o método Sleep, que o suspende por 1 segundo. Ele então perde o processador, que fica disponível para outro thread. O exemplo mostra que é o thread 1 que o obterá. A thread 1 seguirá o mesmo caminho, assim como as outras threads. Quando o segundo de espera da thread 0 terminar, sua execução poderá ser retomada. O sistema lhe concede o processador e ela pode concluir a execução do método Affiche.
Vamos modificar nosso programa para encerrar o método Main com as instruções:
// fim da rotina
Console.WriteLine("Fin du thread " + main.Name);
// paramos todas as threads
Environment.Exit(0);
A execução do novo programa produz os seguintes resultados:
- linhas 1-5: os threads criados pela função Main iniciam sua execução e são interrompidos por 1 segundo
- linha 6: o thread [Main] recupera o processador e executa a instrução:
Essa instrução interrompe todas as threads do aplicativo e não apenas a thread Main.
Se o método Main quiser aguardar o término da execução dos threads que criou, ele pode utilizar o método Join da classe Thread:
public static void Main() {
...
// aguarda todos os threads
for (int i = 0; i < tâches.Length; i++) {
// aguardando o término da execução do thread i
tâches[i].Join();
}
// fim da rotina
Console.WriteLine("Fin du thread {0} à {1:hh:mm:ss}", main.Name, DateTime.Now);
}
- linha 6: o thread [Main] aguarda cada um dos threads. Ele fica inicialmente bloqueado aguardando o thread nº 1, depois o thread nº 2, e assim por diante. Por fim, quando sai do loop das linhas 2 a 5, significa que os 5 threads que ele iniciou já foram concluídos.
Obteve-se, então, os seguintes resultados:
- linha 11: o thread [Main] foi concluído após os threads que ele havia iniciado.
10.3. Importância dos threads
Agora que destacamos a existência de um thread padrão — aquele que executa o método Main — e que sabemos como criar outros, vamos nos deter na utilidade dos threads para nós e nas razões pelas quais os apresentamos aqui. Existe um tipo de aplicativo que se presta bem ao uso de threads: os aplicativos cliente-servidor da Internet. Vamos apresentá-los no capítulo a seguir. Em um aplicativo cliente-servidor da Internet, um servidor localizado em uma máquina S1 responde às solicitações de clientes localizados em máquinas remotas C1, C2, ..., Cn.
![]() |
Utilizamos diariamente aplicativos da Internet que se enquadram nesse esquema: serviços da Web, e-mail, consulta de fóruns, transferência de arquivos... No esquema acima, o servidor S1 deve atender os clientes Ci simultaneamente. Se tomarmos o exemplo de um servidor FTP (Protocolo de Transferência de Arquivos) que fornece arquivos aos seus clientes, sabemos que uma transferência de arquivo pode, às vezes, levar vários minutos. É claro que está fora de questão que um único cliente monopolize o servidor por todo esse tempo. O que se faz normalmente é que o servidor crie tantos threads de execução quantos forem os clientes. Cada thread fica então encarregado de atender a um cliente específico. Como o processador é compartilhado ciclicamente entre todos os threads ativos da máquina, o servidor dedica um pouco de tempo a cada cliente, garantindo assim a simultaneidade do serviço.
![]() |
Na prática, o servidor utiliza um pool de threads com um número limitado de threads, 50, por exemplo. O 51º cliente é, então, solicitado a aguardar.
10.4. Troca de informações entre threads
Nos exemplos anteriores, uma thread era inicializada da seguinte maneira:
onde Run era um método com a seguinte assinatura:
Também é possível utilizar a seguinte assinatura:
Isso permite transmitir informações para a thread iniciada. Assim,
irá iniciar o thread t, que, por sua vez, executará o método Run, ao qual foi associado por definição, passando-lhe o parâmetro efetivo obj1. Veja um exemplo:
using System;
using System.Threading;
namespace Chap8 {
class Program4 {
public static void Main() {
// inicialização do thread atual
Thread main = Thread.CurrentThread;
// atribuímos um nome ao thread
main.Name = "Main";
// Criação de threads de execução
Thread[] tâches = new Thread[5];
Data[] data = new Data[5];
for (int i = 0; i < tâches.Length; i++) {
// criação da thread i
tâches[i] = new Thread(Sleep);
// definindo o nome do thread
tâches[i].Name = i.ToString();
// inicia-se a execução do thread i
tâches[i].Start(data[i] = new Data { Début = DateTime.Now, Durée = i+1 });
}
// aguarda-se a conclusão de todos os threads
for (int i = 0; i < tâches.Length; i++) {
// aguarda o término da execução do thread i
tâches[i].Join();
// exibição do resultado
Console.WriteLine("Thread {0} terminé : début {1:hh:mm:ss}, durée programmée {2} s, fin {3:hh:mm:ss}, durée effective {4}",
tâches[i].Name,data[i].Début,data[i].Durée,data[i].Fin,(data[i].Fin-data[i].Début));
}
// fim do programa
Console.WriteLine("Fin du thread {0} à {1:hh:mm:ss}", main.Name, DateTime.Now);
}
public static void Sleep(object infos) {
// recuperação do parâmetro
Data data = (Data)infos;
// suspensão por Duration segundos
Thread.Sleep(data.Durée*1000);
// fim da execução
data.Fin = DateTime.Now;
}
}
internal class Data {
// informações diversas
public DateTime Début { get; set; }
public int Durée { get; set; }
public DateTime Fin { get; set; }
}
}
- linhas 45-50: a informação do tipo [Data] passada aos threads:
- Début: hora do início da execução do thread — definida pelo thread iniciador
- Durée: duração, em segundos, do Sleep executado pela thread iniciada — definida pela thread iniciadora
- Fin: hora do início da execução da thread — definida pela thread iniciadora
- linhas 35-43: o método Sleep executado pelas threads tem a assinatura void Sleep(object obj). O parâmetro efetivo obj será do tipo [Data], definido na linha 45.
- linhas 15-22: criação de 5 threads
- linha 17: cada thread é associado ao método Sleep da linha 35
- linha 21: um objeto do tipo [Data] é passado para o método Start, que inicia o thread. Nesse objeto, foram registradas a hora de início da execução do thread, bem como a duração, em segundos, durante a qual ele deve permanecer inativo. Esse objeto é armazenado na matriz da linha 14.
- linhas 24-30: o thread [Main] aguarda o término de todos os threads que ele iniciou.
- linhas 28-29: o thread [Main] recupera o objeto data[i] do thread nº i e exibe seu conteúdo.
- linhas 35-42: o método Sleep executado pelos threads
- linha 37: recupera-se o parâmetro do tipo [Data]
- linha 39: o campo Durée do parâmetro é usado para definir a duração do Sleep
- linha 41: o campo Fin do parâmetro é inicializado
Os resultados da execução são os seguintes:
Este exemplo mostra que dois threads podem trocar informações:
- o thread iniciador pode controlar a execução do thread iniciado, fornecendo-lhe informações
- o thread lançado pode retornar resultados ao thread lançador.
Para que a thread lançada saiba quando os resultados que ela aguarda estão disponíveis, é necessário que ela seja notificada sobre o término da thread lançada. Neste caso, ela aguardou o término utilizando o método Join. Existem outras maneiras de fazer o mesmo. Veremos isso posteriormente.
10.5. Acesso simultâneo a recursos compartilhados
10.5.1. Acesso simultâneo não sincronizado
No parágrafo sobre a troca de informações entre threads, as informações eram trocadas apenas entre duas threads e em momentos bem específicos. Trata-se de uma clássica passagem de parâmetros. Existem outros casos em que uma informação é compartilhada por várias threads que podem querer lê-la ou atualizá-la ao mesmo tempo. Surge, então, o problema da integridade dessa informação. Suponhamos que a informação compartilhada seja uma estrutura S com diversas informações I1, I2, ... In.
- Um thread T1 começa a atualizar a estrutura S: ele modifica o campo I1 e é interrompido antes de concluir a atualização completa da estrutura S
- um thread T2, que recupera o processador, lê então a estrutura S para tomar decisões. Ele lê uma estrutura em um estado instável: alguns campos estão atualizados, outros não.
Essa situação é chamada de acesso a um recurso compartilhado — neste caso, a estrutura S — e costuma ser bastante delicada de se gerenciar. Vejamos o exemplo a seguir para ilustrar os problemas que podem surgir:
- um aplicativo irá gerar n threads, sendo que n é passado como parâmetro
- o recurso compartilhado é um contador que deverá ser incrementado por cada thread gerada
- no final do aplicativo, o valor do contador é exibido. Portanto, deveríamos encontrar n.
O programa é o seguinte:
using System;
using System.Threading;
namespace Chap8 {
class Program {
// variáveis de classe
static int cptrThreads = 0; // contador de threads
//main
public static void Main(string[] args) {
// manual de instruções
const string syntaxe = "pg nbThreads";
const int nbMaxThreads = 100;
// verificação do número de argumentos
if (args.Length != 1) {
// erro
Console.WriteLine(syntaxe);
// parada
Environment.Exit(1);
}
// verificação da qualidade do argumento
int nbThreads = 0;
bool erreur = false;
try {
nbThreads = int.Parse(args[0]);
if (nbThreads < 1 || nbThreads > nbMaxThreads)
erreur = true;
} catch {
// erro
erreur = true;
}
// erro?
if (erreur) {
// erro
Console.Error.WriteLine("Nombre de threads incorrect (entre 1 et 100)");
// fim
Environment.Exit(2);
}
// criação e geração de threads
Thread[] threads = new Thread[nbThreads];
for (int i = 0; i < nbThreads; i++) {
// criação
threads[i] = new Thread(Incrémente);
// nomeação
threads[i].Name = "" + i;
// inicialização
threads[i].Start();
}//for
// aguardando o término dos threads
for (int i = 0; i < nbThreads; i++) {
threads[i].Join();
}
// exibição do contador
Console.WriteLine("Nombre de threads générés : " + cptrThreads);
}
public static void Incrémente() {
// incrementa o contador de threads
// leitura do contador
int valeur = cptrThreads;
// acompanhamento
Console.WriteLine("A {0:hh:mm:ss}, le thread {1} a lu la valeur du compteur : {2}", DateTime.Now, Thread.CurrentThread.Name, cptrThreads);
// espera
Thread.Sleep(1000);
// incremento do contador
cptrThreads = valeur + 1;
// rastreamento
Console.WriteLine("A {0:hh:mm:ss}, le thread {1} a écrit la valeur du compteur : {2}", DateTime.Now, Thread.CurrentThread.Name, cptrThreads);
}
}
}
Não nos deteremos na parte relativa à geração de threads, já estudada. Vamos nos concentrar, em vez disso, no método Incrémente, da linha 59, utilizado por cada thread para incrementar o contador estático cptrThreads da linha 8.
- linha 62: o contador é lido
- linha 66: o thread fica parado por 1 s. Assim, ele perde o processador
- linha 68: o contador é incrementado
A etapa 2 existe apenas para forçar o thread a perder o processador. Este será cedido a outro thread. Na prática, nada garante que um thread não seja interrompido entre o momento em que ele vai ler o contador e o momento em que vai incrementá-lo. Mesmo que se escreva cptrThreads++, dando assim a ilusão de uma única instrução, existe o risco de perder o processador entre o momento em que se lê o valor do contador e aquele em que se grava seu valor incrementado em 1. De fato, a operação de alto nível cptrThreads++ será dividida em várias instruções elementares no nível do processador. A etapa 2 de espera de um segundo existe, portanto, apenas para minimizar esse risco.
Os resultados obtidos com 5 threads são os seguintes:
Ao analisar esses resultados, fica claro o que está acontecendo:
- linha 1: um primeiro thread lê o contador. Ele encontra 0. Ele para por 1 s, perdendo assim o processador
- linha 2: um segundo thread assume então o processador e também lê o valor do contador. Ele ainda está em 0, já que o thread anterior ainda não o incrementou. Ele também fica parado por 1 s e, por sua vez, perde o processador.
- linhas 1-5: em 1 s, os 5 threads têm tempo de passar todos e ler o valor 0.
- linhas 6-10: quando forem reativados, um após o outro, eles irão incrementar o valor 0 que leram e gravar o valor 1 no contador, o que é confirmado pelo programa principal (Main) na linha 11.
De onde vem o problema? O segundo thread leu um valor incorreto porque o primeiro havia sido interrompido antes de concluir seu trabalho, que era atualizar o contador na janela. Isso nos leva ao conceito de recurso crítico e de seção crítica de um programa:
- um recurso crítico é um recurso que só pode ser mantido por um thread por vez. Aqui, o recurso crítico é o contador.
- uma seção crítica de um programa é uma sequência de instruções no fluxo de execução de um thread durante a qual ele acessa um recurso crítico. É preciso garantir que, durante essa seção crítica, ele seja o único a ter acesso ao recurso.
Em nosso exemplo, a seção crítica é o código localizado entre a leitura do contador e a gravação de seu novo valor:
// leitura do medidor
int valeur = cptrThreads;
// em espera
Thread.Sleep(1000);
// incremento do contador
cptrThreads = valeur + 1;
Para executar esse código, é preciso garantir que a thread esteja sozinha. Ela pode ser interrompida, mas, durante essa interrupção, nenhuma outra thread deve poder executar esse mesmo código. A plataforma .NET oferece diversas ferramentas para garantir o acesso exclusivo às seções críticas do código. Veremos algumas delas a seguir.
10.5.2. A cláusula lock
A cláusula lock permite delimitar uma seção crítica da seguinte maneira:
obj deve ser uma referência de objeto visível por todas as threads que executam a seção crítica. A cláusula lock garante que apenas uma thread por vez execute a seção crítica. O exemplo anterior é reescrito da seguinte forma:
using System;
using System.Threading;
namespace Chap8 {
class Program2 {
// variáveis de classe
static int cptrThreads = 0; // contador de threads
static object synchro = new object(); // objeto de sincronização
//main
public static void Main(string[] args) {
...
// aguardando o término dos threads
Thread.CurrentThread.Name = "Main";
for (int i = nbThreads - 1; i >= 0; i--) {
Console.WriteLine("A {0:hh:mm:ss}, le thread {1} attend la fin du thread {2}", DateTime.Now, Thread.CurrentThread.Name, threads[i].Name);
threads[i].Join();
Console.WriteLine("A {0:hh:mm:ss}, le thread {1} a été prévenu de la fin du thread {2}", DateTime.Now, Thread.CurrentThread.Name, threads[i].Name);
}
// exibição do contador
Console.WriteLine("Nombre de threads générés : " + cptrThreads);
}
public static void Incrémente() {
// incrementa o contador de threads
// é solicitado acesso exclusivo ao contador
Console.WriteLine("A {0:hh:mm:ss}, le thread {1} attend l'autorisation d'entrer dans la section critique", DateTime.Now, Thread.CurrentThread.Name);
lock (synchro) {
// leitura do contador
int valeur = cptrThreads;
// acompanhamento
Console.WriteLine("A {0:hh:mm:ss}, le thread {1} a lu la valeur du compteur : {2}", DateTime.Now, Thread.CurrentThread.Name, cptrThreads);
// espera
Thread.Sleep(1000);
// incremento do contador
cptrThreads = valeur + 1;
// acompanhamento
Console.WriteLine("A {0:hh:mm:ss}, le thread {1} a écrit la valeur du compteur : {2}", DateTime.Now, Thread.CurrentThread.Name, cptrThreads);
}
Console.WriteLine("A {0:hh:mm:ss}, le thread {1} a quitté la section critique", DateTime.Now, Thread.CurrentThread.Name);
}
}
}
- linha 9: synchro é o objeto que permitirá a sincronização de todas as threads.
- linhas 16-23: o método [Main] aguarda as threads na ordem inversa à de sua criação.
- linhas 29-40: a seção crítica do método Incrémente foi delimitada pela cláusula lock.
Os resultados obtidos com 3 threads são os seguintes:
- o thread 0 é o primeiro a entrar na seção crítica: linhas 1, 2, 6, 8
- os outros dois threads ficarão bloqueados enquanto o thread 0 não sair da seção crítica: linhas 3 e 4
- em seguida, o thread 1 é executado: linhas 7, 9, 10
- em seguida, o thread 2 é executado: linhas 11, 12, 13
- linha 14: o thread Main, que aguardava o término do thread 2, é notificado
- linha 15: o thread Main agora aguarda o término do thread 1. Este já foi concluído. O thread Main é notificado imediatamente, linha 16.
- linhas 17-18: o mesmo processo ocorre com o thread 0
- linha 19: o número de threads está correto
10.5.3. A classe Mutex
A classe System.Threading.Mutex também permite delimitar seções críticas. Ela difere da cláusula lock em termos de visibilidade:
- a cláusula lock permite sincronizar threads de uma mesma aplicação
- a classe Mutex permite sincronizar threads de diferentes aplicações.
Utilizaremos o construtor e os seguintes métodos:
cria um Mutex M | |
A thread T1, que executa a operação M.WaitOne(), solicita a posse do objeto de sincronização M. Se o Mutex M não estiver na posse de nenhuma thread (o que ocorre inicialmente), ele é “concedido” ao thread T1 que o solicitou. Se, pouco depois, um thread T2 realizar a mesma operação, ele ficará bloqueado. De fato, um Mutex só pode pertencer a um único thread. Ele será desbloqueado quando o thread T1 liberar o Mutex M que ele detém. Assim, vários threads podem ficar bloqueados aguardando o Mutex M. | |
A thread T1, que executa a operação M.ReleaseMutex(), abre mão da posse do Mutex Mutex. Quando a thread T1 perder o processador, o sistema poderá atribuí-lo a uma das threads em espera pelo mutex M. Apenas uma delas o obterá por sua vez, enquanto as demais em espera por M permanecerão bloqueadas |
Um Mutex M gerencia o acesso a um recurso compartilhado R. Uma thread solicita o recurso R por meio de M.WaitOne() e o libera por meio de M.ReleaseMutex(). Uma seção crítica de código que deve ser executada por apenas um thread por vez é um recurso compartilhado. A sincronização da execução da seção crítica pode ser feita da seguinte forma:
onde M é um objeto Mutex. Não se deve esquecer de liberar um Mutex que se tornou desnecessário, para que outra thread possa entrar na seção crítica; caso contrário, as threads que aguardam o Mutex que nunca foi liberado nunca terão acesso ao processador.
Se colocarmos em prática no exemplo anterior o que acabamos de ver, nossa aplicação fica da seguinte forma:
using System;
using System.Threading;
namespace Chap8 {
class Program3 {
// variáveis de classe
static int cptrThreads = 0; // contador de threads
static Mutex synchro = new Mutex(); // objeto de sincronização
//main
public static void Main(string[] args) {
...
}
public static void Incrémente() {
....
synchro.WaitOne();
try {
...
} finally {
...
synchro.ReleaseMutex();
}
}
}
}
- linha 9: o objeto de sincronização das threads agora é um Mutex.
- linha 18: início da seção crítica — apenas um thread deve entrar nela. Ficamos bloqueados até que o Mutex synchro esteja livre.
- linha 33: como um Mutex deve sempre ser liberado, haja ou não exceção, a seção crítica é gerenciada com um try/finally para liberar o Mutex no finally.
- linha 23: o Mutex é liberado assim que a seção crítica é concluída.
Os resultados obtidos são os mesmos de antes.
10.5.4. A classe AutoResetEvent
Um objeto AutoResetEvent é uma barreira que permite a passagem de apenas um thread por vez, assim como as duas ferramentas anteriores, lock e Mutex. Constrói-se um objeto AutoResetEvent da seguinte maneira:
A variável booleana état indica se a barreira está fechada (false) ou aberta (true). Um thread que deseje passar pela barreira indicará isso da seguinte maneira:
- se a barreira estiver aberta, o thread passa e a barreira é fechada novamente atrás dele. Se houver vários threads aguardando, tem-se a garantia de que apenas um passará.
- Se a barreira estiver fechada, o thread fica bloqueado. Outro thread a abrirá quando chegar a hora. Esse momento depende inteiramente do problema em questão. A barreira será aberta pela operação:
Pode acontecer que um thread queira fechar uma barreira. Ele poderá fazer isso por meio de:
Se, no exemplo anterior, substituirmos o objeto Mutex por um objeto do tipo AutoResetEvent, o código fica da seguinte forma:
using System;
using System.Threading;
namespace Chap8 {
class Program4 {
// variáveis de classe
static int cptrThreads = 0; // contador de threads
static EventWaitHandle synchro = new AutoResetEvent(false); // objeto de sincronização
//main
public static void Main(string[] args) {
....
// abrindo a barreira da seção crítica
Console.WriteLine("A {0:hh:mm:ss}, le thread {1} ouvre la barrière de la section critique", DateTime.Now, Thread.CurrentThread.Name);
synchro.Set();
// aguardando o término das threads
...
// exibição do contador
Console.WriteLine("Nombre de threads générés : " + cptrThreads);
}
public static void Incrémente() {
// incrementa o contador de threads
// é solicitado acesso exclusivo ao contador
...
synchro.WaitOne();
try {
...
} finally {
// o recurso é liberado
...
synchro.Set();
}
}
}
}
- linha 9: a barreira é criada fechada. Ela será aberta pela thread Main na linha 16.
- linha 27: o thread encarregado de incrementar o contador de threads solicita autorização para entrar na seção crítica. Os diferentes threads vão se acumular diante da barreira fechada. Quando o thread Main a abrir, um dos threads em espera passará.
- linha 33: ao concluir seu trabalho, ele reabre a barreira, permitindo que outro thread entre.
Obtemos resultados semelhantes aos anteriores.
10.5.5. A classe Interlocked
A classe Interlocked permite tornar atômico um grupo de operações. Em um grupo de operações atomique, ou todas as operações são executadas pela thread que executa o grupo, ou nenhuma. Não ficamos em um estado em que algumas foram executadas e outras não. Os objetos de sincronização lock, Mutex e AutoResetEvent têm, todos, o objetivo de tornar atomique um grupo de operações. Esse resultado é obtido à custa do bloqueio de threads. A classe Interlocked permite, para operações simples, porém bastante frequentes, evitar o bloqueio de threads. A classe **Interlocked** oferece os seguintes métodos estáticos:

O método Increment tem a seguinte assinatura:
Ele permite incrementar em 1 o parâmetro location. A operação é garantida por atomique.
Nosso programa de contagem de threads pode, então, ser o seguinte:
using System;
using System.Threading;
namespace Chap8 {
class Program5 {
// variáveis de classe
static int cptrThreads = 0; // contador de threads
//main
public static void Main(string[] args) {
...
}
public static void Incrémente() {
// incrementa o contador de threads
Interlocked.Increment(ref cptrThreads);
}
}
}
- linha 17: o contador de threads é incrementado de forma atômica.
10.6. Acessos simultâneos a múltiplos recursos compartilhados
10.6.1. Um exemplo
Em nossos exemplos anteriores, um único recurso era compartilhado pelas diferentes threads. A situação pode se complicar se houver várias recursos e elas forem dependentes umas das outras. Uma situação de interbloqueio pode ocorrer, por exemplo. Essa situação, também chamada de deadlock, é aquela em que duas threads aguardam uma à outra. Consideremos as seguintes ações que ocorrem sequencialmente no tempo:
- um thread T1 obtém a posse de um mutex M1 para ter acesso a um recurso compartilhado R1
- um thread T2 obtém a posse de um mutex M2 para acessar um recurso compartilhado R2
- o thread T1 solicita o mutex M2. Ele fica bloqueado.
- A thread T2 solicita o mutex M1. Ela fica bloqueada.
Aqui, os threads T1 e T2 estão aguardando um ao outro. Esse caso ocorre quando os threads precisam de dois recursos compartilhados: o recurso R1, controlado pelo mutex M1, e o recurso R2, controlado pelo mutex M2. Uma solução possível é solicitar os dois recursos ao mesmo tempo por meio de um único mutex M. Mas isso nem sempre é possível se, por exemplo, isso implicar em uma ocupação prolongada de um recurso caro. Outra solução é que uma thread que possua M1 e não consiga obter M2 libere M1 para evitar o interbloqueio.
- Temos uma tabela na qual algumas threads vêm gravar dados (os gravadores) e outras vêm lê-los (os leitores).
- Os gravadores são iguais entre si, mas exclusivos: apenas um gravador por vez pode gravar seus dados na matriz.
- Os leitores são iguais entre si, mas exclusivos: apenas um leitor por vez pode ler os dados gravados na tabela.
- Um leitor só pode ler os dados da matriz quando um gravador os tiver inserido nela, e um gravador só pode inserir novos dados na matriz quando os que já estão nela tiverem sido lidos por um leitor.
É possível distinguir dois tipos de recursos compartilhados:
- a tabela em gravação: apenas um gravador por vez deve ter acesso a ela.
- a tabela em leitura: apenas um leitor por vez deve ter acesso a ela.
e uma ordem de utilização desses recursos:
- um leitor deve sempre vir depois de um gravador.
- um gravador deve sempre vir depois de um leitor, exceto na primeira vez.
É possível controlar o acesso a esses dois recursos com duas barreiras do tipo AutoResetEvent:
- a barreira peutEcrire controlará o acesso dos gravadores à tabela.
- A barreira peutLire controlará o acesso dos leitores ao quadro.
- A barreira peutEcrire será criada inicialmente aberta, permitindo a passagem de um primeiro usuário e bloqueando todos os demais.
- A barreira peutLire será criada inicialmente fechada, bloqueando todos os leitores.
- Quando um gravador terminar seu trabalho, ele abrirá a barreira peutLire para permitir a entrada de um leitor.
- Quando um leitor terminar seu trabalho, ele abrirá a barreira peutEcrire para permitir a entrada de um gravador.
O programa que ilustra essa sincronização por eventos é o seguinte:
using System;
using System.Threading;
namespace Chap8 {
class Program {
// uso de threads de leitura e gravação
// ilustra o uso de eventos de sincronização
// variáveis de classe
static int[] data = new int[3]; // recurso compartilhado entre threads de leitura e threads de gravação
static Random objRandom = new Random(DateTime.Now.Second); // um gerador de números aleatórios
static AutoResetEvent peutLire; // indica que é possível ler o conteúdo de data
static AutoResetEvent peutEcrire; // indica que é possível gravar o conteúdo de data
//main
public static void Main(string[] args) {
// o número de threads a serem geradas
const int nbThreads = 2;
// inicialização dos sinalizadores
peutLire = new AutoResetEvent(false); // ainda não é possível ler
peutEcrire = new AutoResetEvent(true); // já é possível gravar
// criação dos threads de leitura
Thread[] lecteurs = new Thread[nbThreads];
for (int i = 0; i < nbThreads; i++) {
// criação
lecteurs[i] = new Thread(Lire);
lecteurs[i].Name = "L" + i.ToString();
// inicialização
lecteurs[i].Start();
}
// criação dos threads de gravação
Thread[] écrivains = new Thread[nbThreads];
for (int i = 0; i < nbThreads; i++) {
// criação
écrivains[i] = new Thread(Ecrire);
écrivains[i].Name = "E" + i.ToString();
// inicialização
écrivains[i].Start();
}
//fim da mão
Console.WriteLine("Fin de Main...");
}
// ler o conteúdo da tabela
public static void Lire() {
...
}
// gravar na tabela
public static void Ecrire() {
....
}
}
}
- linha 11: o array data é o recurso compartilhado entre as threads de leitura e de gravação. Ele é compartilhado em leitura pelas threads de leitura e em gravação pelas threads de gravação.
- linha 13: o objeto peutLire serve para avisar as threads de leitura de que podem ler a matriz data. Ele é definido como verdadeiro pela thread de gravação que preencheu a matriz data. Ele é inicializado como false, na linha 23. É necessário que um thread de gravação preencha primeiro a matriz antes de passar o evento peutLire para vrai.
- linha 14: o objeto peutEcrire serve para avisar as threads de gravação de que podem gravar na matriz data. Ele é definido como verdadeiro pela thread de leitura que utilizou toda a matriz data. Ele é inicializado em true, linha 24. De fato, o array data está livre para gravação.
- linhas 27-34: criação e inicialização dos threads de leitura
- linhas 37-44: criação e inicialização dos threads de gravação
O método Lire executado pelos threads de leitura é o seguinte:
public static void Lire() {
// acompanhamento
Console.WriteLine("Méthode [Lire] démarrée par le thread n° {0}", Thread.CurrentThread.Name);
// é preciso aguardar a autorização de leitura
peutLire.WaitOne();
// leitura da tabela
for (int i = 0; i < data.Length; i++) {
//espera de 1 s
Thread.Sleep(1000);
// exibição
Console.WriteLine("{0:hh:mm:ss} : Le lecteur {1} a lu le nombre {2}", DateTime.Now, Thread.CurrentThread.Name, data[i]);
}
// é possível gravar
peutEcrire.Set();
// acompanhamento
Console.WriteLine("Méthode [Lire] terminée par le thread n° {0}", Thread.CurrentThread.Name);
}
- linha 5: aguarda-se que um thread de gravação sinalize que a matriz foi preenchida. Quando esse sinal for recebido, apenas um dos threads de leitura que estiverem aguardando esse sinal poderá prosseguir.
- linhas 7-12: utilização da matriz data com um Sleep no meio para forçar o thread a liberar o processador.
- linha 14: indica aos threads de gravação que a matriz foi lida e que pode ser preenchida novamente.
O método Ecrire executado pelas threads de gravação é o seguinte:
public static void Ecrire() {
// acompanhamento
Console.WriteLine("Méthode [Ecrire] démarrée par le thread n° {0}", Thread.CurrentThread.Name);
// é preciso aguardar a autorização para gravar
peutEcrire.WaitOne();
// gravação na tabela
for (int i = 0; i < data.Length; i++) {
//espera de 1 s
Thread.Sleep(1000);
// exibição
data[i] = objRandom.Next(0, 1000);
Console.WriteLine("{0:hh:mm:ss} : L'écrivain {1} a écrit le nombre {2}", DateTime.Now, Thread.CurrentThread.Name, data[i]);
}
// é possível ler
peutLire.Set();
// acompanhamento
Console.WriteLine("Méthode [Ecrire] terminée par le thread n° {0}", Thread.CurrentThread.Name);
}
- linha 5: aguarda que uma thread de leitura sinalize que a matriz foi lida. Quando esse sinal for recebido, apenas uma das threads de gravação que estiverem aguardando esse sinal poderá prosseguir.
- linhas 7-13: processamento da matriz data com um Sleep no meio para forçar a thread a perder o processador.
- linha 15: indica aos threads de leitura que a matriz foi preenchida e que pode ser lida novamente.
A execução produz os seguintes resultados:
É possível observar os seguintes pontos:
- há de fato apenas um leitor por vez, embora este perca o processador na seção crítica Lire
- há de fato apenas um gravador por vez, embora este perca o processador na seção crítica Ecrire
- um leitor só lê quando há algo para ler na tabela
- um gravador só grava quando a tabela tiver sido lida por completo
10.6.2. A classe Monitor
No exemplo anterior:
- há dois recursos compartilhados a serem gerenciados
- para um determinado recurso, os threads são iguais.
Quando os threads de gravação ficam bloqueados na instrução peutEcrire.WaitOne, um deles, qualquer um, é desbloqueado pela operação peutEcrire.Set. Se a operação anterior precisar abrir a barreira para um thread de gravação específico, as coisas ficam mais complicadas.
Pode-se considerar a analogia com um estabelecimento que atende o público em guichês, onde cada guichê é especializado. Quando o cliente chega, ele pega um número na máquina de distribuição de números para o guichê X e, em seguida, vai sentar-se. Cada número é numerado e os clientes são chamados por seu número por meio de um alto-falante. Enquanto espera, o cliente faz o que quiser. Ele pode ler ou cochilar. É acordado sempre que o alto-falante anuncia que o nº Y é chamado no guichê X. Se for ele, o cliente se levanta e se dirige ao guichê X; caso contrário, continua o que estava fazendo.
Aqui, podemos proceder de maneira semelhante. Tomemos o exemplo dos escritores:
seus threads estão bloqueados | |
O thread que estava lendo o array indica aos threads de gravação que o array está disponível. Esse mesmo thread ou outro thread bloqueou o thread de gravação, que precisa passar pelo bloqueio. | |
cada thread verifica se é o escolhido. Se for, ele passa pela barreira. Caso contrário, volta a ficar em espera. |
A classe Monitor permite implementar esse cenário.

Descreveremos agora uma construção padrão (pattern), proposta no capítulo Threading do livro C# 3.0 mencionado na introdução deste documento, capaz de resolver os problemas de barreira com condição de entrada.
- Em primeiro lugar, as threads que compartilham um recurso (o guichê, etc.) acessam-no por meio de um objeto que chamaremos de token. Para abrir a barreira que leva ao guichê, é necessário ter o token, e há apenas um token. As threads devem, portanto, passar o token entre si.
- Para chegar ao guichê, os threads primeiro solicitam o token:
Se o token estiver disponível, ele é concedido ao thread que executou a operação anterior; caso contrário, o thread fica em espera pelo token.
- Se o acesso ao balcão for feito de forma não ordenada, c.a.d. No caso em que a ordem de entrada das pessoas não importa, a operação anterior é suficiente. O thread que possui o token vai ao balcão. Se o acesso for feito de forma ordenada, o thread que possui o token verifica se cumpre a condição para ir ao balcão:
Se o thread não for aquele esperado no balcão, ele cede sua vez, devolvendo o token. Ele passa para um estado bloqueado. Será reativado assim que o token voltar a estar disponível para ele. Nesse momento, verificará novamente se atende à condição para ir ao balcão. A operação Monitor.Wait(ficha), que libera a ficha, só pode ser realizada se o thread for o proprietário da ficha. Caso contrário, é lançada uma exceção.
- A thread que verifica a condição para acessar o guichê o faz:
- // trabalho no guichê
- ....
Antes de sair do guichê, o thread deve devolver seu token; caso contrário, os threads bloqueados à espera dele permanecerão assim indefinidamente. Há duas situações diferentes:
- a primeira situação é aquela em que a thread que possui o token é também a que sinaliza às threads que aguardam o token que este está livre. Ela fará isso da seguinte maneira:
Na linha 6, ele desperta os threads que aguardam o token. Esse despertar significa que eles se tornam elegíveis para receber o token. Isso não significa que eles o recebam imediatamente. Na linha 8, o token é liberado. Todas as threads elegíveis receberão o token, uma por vez, de forma indeterminista. Isso lhes dará a oportunidade de verificar novamente se atendem à condição de acesso. A thread que liberou o token alterou essa condição na linha 4 para permitir que uma nova thread entre. A primeira que atender à condição fica com o token e vai ao balcão na sua vez.
- A segunda situação é aquela em que o thread que possui o token não é o responsável por sinalizar aos threads que aguardam o token que este está livre. Ele deve, no entanto, liberá-lo, pois o thread encarregado de enviar esse sinal deve ser o detentor do token. Ele fará isso por meio da operação:
O token agora está disponível, mas os threads que o aguardam (eles executaram uma operação Wait(token)) não são notificados. Essa tarefa é confiada a outro thread que, em determinado momento, executará um código semelhante ao seguinte:
No final, a construção padrão proposta no capítulo Threading do livro C# 3.0 é a seguinte:
- definir o token de acesso ao balcão:
- solicitar acesso ao guichê:
lock(jeton){
while (! jeNeSuisPasCeluiQuiEstAttendu)
Monitor.Wait(jeton);
}
// passagem para o guichê
...
é equivalente a
Observe-se que, nesse esquema, o token é liberado imediatamente, assim que a barreira é ultrapassada. Outro thread pode, então, verificar a condição de acesso. A construção anterior permite, portanto, a entrada de todos os threads que verificam a condição de acesso. Se isso não for o desejado, pode-se escrever:
lock(jeton){
while (! jeNeSuisPasCeluiQuiEstAttendu)
Monitor.Wait(jeton);
// passagem para o guichê
...
}
onde o token só é liberado após a passagem pelo guichê.
- modificar a condição de acesso ao guichê e notificar os outros threads
lock(jeton){
// alteração da condição de acesso ao guichê
...
// notificar os threads que aguardam o token
Monitor.PulseAll(jeton);
}
No código acima, a condição de acesso só pode ser alterada pela thread que possui o token. Também seria possível escrever:
// alterar a condição de acesso ao guichê
...
// notificar os threads que aguardam o token
Monitor.PulseAll(jeton);
// liberar o token
Monitor.Exit(jeton);
se o thread já tiver o token.
Com essas informações, podemos reescrever o aplicativo de leitura/gravação, definindo uma ordem para os leitores e gravadores no acesso às suas respectivas janelas. O código é o seguinte:
using System;
using System.Threading;
namespace Chap8 {
class Program2 {
// uso de threads de leitura e gravação
// ilustra o uso de eventos de sincronização
// variáveis de classe
static int[] data = new int[3]; // recurso compartilhado entre threads de leitura e threads de gravação
static Random objRandom = new Random(DateTime.Now.Second); // um gerador de números aleatórios
static object peutLire = new object(); // indica que é possível ler o conteúdo de data
static object peutEcrire = new object(); // indica que é possível gravar o conteúdo de data
static bool lectureAutorisée = false; // para autorizar a leitura da matriz
static bool écritureAutorisée = false; // para autorizar a gravação na matriz
static string[] ordreLecture; // define a ordem dos leitores
static string[] ordreEcriture; // define a ordem dos gravadores
static int lecteurSuivant = 0; // indica o número do próximo leitor
static int écrivainSuivant = 0; // indica o número do próximo gravador
//principal
public static void Main(string[] args) {
// o número de threads a serem geradas
const int nbThreads = 5;
// criação dos threads de leitura
Thread[] lecteurs = new Thread[nbThreads];
for (int i = 0; i < nbThreads; i++) {
// criação
lecteurs[i] = new Thread(Lire);
lecteurs[i].Name = "L" + i.ToString();
// inicialização
lecteurs[i].Start();
}
// criação da ordem de leitura
ordreLecture = new string[nbThreads];
for (int i = 0; i < nbThreads; i++) {
ordreLecture[i] = lecteurs[nbThreads - i - 1].Name;
Console.WriteLine("Le lecteur {0} est en position {1}", ordreLecture[i], i);
}
// criação dos threads de gravação
Thread[] écrivains = new Thread[nbThreads];
for (int i = 0; i < nbThreads; i++) {
// criação
écrivains[i] = new Thread(Ecrire);
écrivains[i].Name = "E" + i.ToString();
// inicialização
écrivains[i].Start();
}
// criação da ordem de gravação
ordreEcriture = new string[nbThreads];
for (int i = 0; i < nbThreads; i++) {
ordreEcriture[i] = écrivains[i].Name;
Console.WriteLine("L'écrivain {0} est en position {1}", ordreEcriture[i], i);
}
// autorização de gravação
lock (peutEcrire) {
écritureAutorisée = true;
Monitor.Pulse(peutEcrire);
}
//fim da transação
Console.WriteLine("Fin de Main...");
}
// ler o conteúdo da tabela
public static void Lire() {
...
}
// gravar na tabela
public static void Ecrire() {
...
}
}
}
O acesso ao canal de leitura é condicionado pelos seguintes elementos:
- linha 13: o token peutLire
- linha 15: o booleano lectureAutorisée
- linha 17: a matriz ordenada dos leitores. Os leitores se dirigem ao guichê de leitura na ordem dessa matriz, que contém seus nomes.
- linha 19: lecteurSuivant indica o número do próximo leitor autorizado a se dirigir ao balcão.
O acesso ao balcão de gravação depende dos seguintes elementos:
- linha 14: o token peutEcrire
- linha 16: o valor booleano écritureAutorisée
- linha 18: a matriz ordenada dos escritores. Os escritores se dirigem ao balcão de gravação na ordem dessa matriz, que contém seus nomes.
- linha 20: écrivainSuivant indica o número do próximo autor autorizado a se dirigir ao balcão.
Os demais elementos do código são os seguintes:
- linhas 29-36: criação e inicialização dos threads de leitura. Todos eles ficarão bloqueados, pois a leitura não é permitida (linha 15).
- linhas 39-43: a ordem de atendimento no guichê será na ordem inversa à de sua criação.
- linhas 46-53: criação e inicialização dos threads de gravação. Todos eles ficarão bloqueados, pois a gravação não é permitida (linha 16).
- linhas 56-60: a ordem de atendimento no guichê será na ordem de criação.
- linha 64: a gravação é autorizada
- linha 65: avisa-se aos escritores que algo mudou.
O método Lire é o seguinte:
public static void Lire() {
// acompanhamento
Console.WriteLine("Méthode [Lire] démarrée par le thread n° {0}", Thread.CurrentThread.Name);
// é preciso aguardar a autorização de leitura
lock (peutLire) {
while (!lectureAutorisée || ordreLecture[lecteurSuivant] != Thread.CurrentThread.Name) {
Monitor.Wait(peutLire);
}
// leitura da tabela
for (int i = 0; i < data.Length; i++) {
//espera de 1 s
Thread.Sleep(1000);
// exibição
Console.WriteLine("{0:hh:mm:ss} : Le lecteur {1} a lu le nombre {2}", DateTime.Now, Thread.CurrentThread.Name, data[i]);
}
// próximo leitor
lectureAutorisée = false;
lecteurSuivant++;
// avisa-se aos gravadores que eles podem gravar
lock (peutEcrire) {
écritureAutorisée = true;
Monitor.PulseAll(peutEcrire);
}
// acompanhamento
Console.WriteLine("Méthode [Lire] terminée par le thread n° {0}", Thread.CurrentThread.Name);
}
}
- Todo o acesso ao balcão é controlado pelo lock das linhas 5 a 27. O leitor que recebe o token o mantém durante toda a sua passagem pelo balcão
- linhas 6 a 8: um leitor que tenha adquirido o token na linha 5 o libera se a leitura não for autorizada ou se não for a sua vez de passar.
- linhas 10-15: passagem pelo guichê (processamento da tabela)
- linhas 17-18: o thread altera as condições de acesso ao balcão de leitura. Observe-se que ele ainda possui o token de leitura e que essas modificações ainda não permitem que um leitor passe.
- linhas 20-23: o thread altera as condições de acesso ao guichê de gravação e avisa todos os gravadores em espera de que algo mudou.
- linha 27: o lock é encerrado, o token peutLire é liberado. Um thread de leitura poderia então adquiri-lo na linha 5, mas não passaria pela condição de acesso, uma vez que o booleano lectureAutorisée está falso. Além disso, todas as threads que estão aguardando o token peutLire permanecem nessa condição, pois a operação PulseAll(peutLire) ainda não ocorreu.
O método Ecrire é o seguinte:
public static void Ecrire() {
// acompanhamento
Console.WriteLine("Méthode [Ecrire] démarrée par le thread n° {0}", Thread.CurrentThread.Name);
// é preciso aguardar a autorização para gravar
lock (peutEcrire) {
while (!écritureAutorisée || ordreEcriture[écrivainSuivant] != Thread.CurrentThread.Name) {
Monitor.Wait(peutEcrire);
}
// gravação na tabela
for (int i = 0; i < data.Length; i++) {
//espera de 1 s
Thread.Sleep(1000);
// exibição
data[i] = objRandom.Next(0, 1000);
Console.WriteLine("{0:hh:mm:ss} : L'écrivain {1} a écrit le nombre {2}", DateTime.Now, Thread.CurrentThread.Name, data[i]);
}
// próximo gravador
écritureAutorisée = false;
écrivainSuivant++;
// despertam-se os leitores em espera pelo token peutLire
lock (peutLire) {
lectureAutorisée = true;
Monitor.PulseAll(peutLire);
}
// acompanhamento
Console.WriteLine("Méthode [Ecrire] terminée par le thread n° {0}", Thread.CurrentThread.Name);
}
}
- Todo o acesso ao balcão de gravação é controlado pelo lock das linhas 5 a 27. O gravador que recebe o token o mantém durante toda a sua permanência no balcão
- linhas 6 a 8: um usuário que tenha obtido o token na linha 5 o libera se a gravação não for autorizada ou se não for sua vez de passar.
- linhas 10-16: passagem pelo balcão (processamento da tabela)
- linhas 18-19: o thread altera as condições de acesso ao guichê de gravação. Observe-se que ele ainda possui o token de gravação e que essas modificações ainda não permitem que um gravador tenha acesso.
- linhas 21-24: o thread altera as condições de acesso ao canal de leitura e avisa todos os leitores em espera de que algo mudou.
- linha 27: o lock é encerrado, o token peutEcrire é liberado. Um thread de gravação poderia então adquiri-lo na linha 5, mas não passaria pela condição de acesso, uma vez que o booleano écritureAutorisée está falso. Além disso, todas as threads que estão aguardando o token peutEcrire permanecem nessa espera por uma nova operação PulseAll(peutEcrire).
Um exemplo de execução é o seguinte:
10.7. Os pools de threads
Até agora, para gerenciar threads:
- nós os criávamos com Thread T = new Thread(...)
- e, em seguida, os executávamos por meio de T.Start()
Vimos no capítulo “Bancos de dados” que, com certos SGBD, era possível ter pools de conexões abertas:
- n conexões são abertas ao iniciar o pool
- quando um thread solicita uma conexão, recebe uma das conexões abertas do pool
- quando o thread fecha a conexão, ela não é encerrada, mas devolvida ao pool
O uso de um pool de conexões é transparente no código. A vantagem está na melhoria do desempenho: abrir uma conexão consome muitos recursos. Nesse caso, 10 conexões abertas podem atender centenas de solicitações.
Existe um sistema semelhante para os threads:
- min threads são criados ao iniciar o pool. O valor de min é definido com o método ThreadPool.SetMinThreads(min1,min2). Um pool de threads pode ser usado para executar tarefas bloqueantes ou não bloqueantes, conhecidas como assíncronas. O primeiro parâmetro, min1, define o número de threads bloqueantes; o segundo, min2, define o número de threads assíncronas. Os valores atuais desses dois parâmetros podem ser obtidos por meio de ThreadPool.GetMinThreads(out min1,out min2).
- Se esse número não for suficiente, o pool criará outras threads para atender às solicitações até o limite de max threads. O valor de max é definido com o método ThreadPool.SetMaxThreads(max1,max2). Os dois parâmetros têm o mesmo significado que no método SetMinThreads. Os valores atuais desses dois parâmetros podem ser obtidos por meio do método ThreadPool.GetMaxThreads(out max1,out max2). Quando o limite de threads definido pelo método max1 for atingido, as solicitações de threads para tarefas bloqueantes ficarão em espera até que um thread fique livre no pool.
Um pool de threads oferece diversas vantagens:
- assim como no pool de conexões, economiza-se tempo na criação de threads: 10 threads podem atender centenas de solicitações.
- a aplicação fica mais segura: ao definir um número máximo de threads, evita-se a sobrecarga da aplicação por um número excessivo de solicitações. Estas serão colocadas em fila de espera.
Para atribuir uma tarefa a um thread do pool, utiliza-se um dos dois métodos:
- ThreadPool.QueueWorkItem(WaitCallBack)
- ThreadPool.QueueWorkItem(WaitCallBack, object)
onde WaitCallBack é qualquer método com a assinatura void WaitCallBack(object). O método 1 solicita que uma thread execute o método WaitCallBack sem passar nenhum parâmetro a ele. O método 2 faz o mesmo, mas passando um parâmetro do tipo object ao método WaitCallBack.
Aqui está um programa que ilustra esses conceitos:
using System;
using System.Threading;
namespace Chap8 {
class Program {
public static void Main() {
// inicialização do thread atual
Thread main = Thread.CurrentThread;
// atribui-se um nome à thread
main.Name = "Main";
// utiliza-se um pool de threads
int min1, min2;
// definimos o número mínimo de threads bloqueantes
ThreadPool.GetMinThreads(out min1, out min2);
Console.WriteLine("Nombre minimum de tâches bloquantes dans le pool : {0}", min1);
Console.WriteLine("Nombre minimum de tâches asynchrones dans le pool : {0}", min2);
ThreadPool.SetMinThreads(3, min2);
ThreadPool.GetMinThreads(out min1, out min2);
Console.WriteLine("Nombre minimum de tâches bloquantes dans le pool après changement : {0}", min1);
// define-se o número máximo de threads bloqueantes
int max1, max2;
ThreadPool.GetMaxThreads(out max1, out max2);
Console.WriteLine("Nombre maximum de tâches bloquantes dans le pool : {0}", max1);
Console.WriteLine("Nombre maximum de tâches asynchrones dans le pool : {0}", max2);
ThreadPool.SetMaxThreads(5, max2);
ThreadPool.GetMaxThreads(out max1, out max2);
Console.WriteLine("Nombre maximum de tâches bloquantes dans le pool après changement : {0}", max1);
// executam-se 7 threads
for (int i = 0; i < 7; i++) {
// inicia-se a execução do thread i em um pool
ThreadPool.QueueUserWorkItem(Sleep, new Data2 { Numéro = i.ToString(), Début = DateTime.Now, Durée = i + 10 });
}
// fim do programa
Console.Write("Tapez [entrée] pour terminer le thread {0} à {1:hh:mm:ss:FF}", main.Name, DateTime.Now);
// espera
Console.ReadLine();
}
public static void Sleep(object infos) {
// recuperando o parâmetro
Data2 data = infos as Data2;
Console.WriteLine("A {2:hh:mm:ss:FF}, le thread n° {0} va dormir pendant {1} seconde(s)", data.Numéro, data.Durée,DateTime.Now);
// estado do pool
int cpt1, cpt2;
ThreadPool.GetAvailableThreads(out cpt1, out cpt2);
Console.WriteLine("Nombre de threads pour tâches bloquantes disponibles dans le pool : {0}", cpt1);
// colocado em espera por Duration segundos
Thread.Sleep(data.Durée * 1000);
// fim da execução
data.Fin = DateTime.Now;
Console.WriteLine("A {3:hh:mm:ss:FF}, le thread n° {0} se termine. Il était programmé pour durer {1} seconde(s). Il a duré {2} seconde(s)", data.Numéro, data.Durée, data.Fin - data.Début,DateTime.Now);
}
}
internal class Data2 {
// informações diversas
public string Numéro { get; set; }
public DateTime Début { get; set; }
public int Durée { get; set; }
public DateTime Fin { get; set; }
}
}
- linhas 15-17: solicita-se e exibe-se o número mínimo atual dos dois tipos de threads do pool de threads
- linha 18: altera-se o número mínimo de threads para tarefas bloqueantes: 2
- linhas 19-21: exibimos os novos valores mínimos
- linhas 22-28: faz-se o mesmo para definir o número máximo de threads para tarefas bloqueantes: 5
- linhas 30-33: executam-se 7 tarefas em um pool de 5 threads. 5 tarefas devem obter 1 thread cada; as 2 primeiras rapidamente, já que 2 threads ainda estão disponíveis, e as 3 demais com um tempo de espera de 0,5 segundo. 2 tarefas devem aguardar até que um thread seja liberado.
- linha 32: as tarefas executam o método Sleep das linhas 40-54, passando-lhe um parâmetro do tipo Data2 definido nas linhas 56-62.
- linha 40: o método Sleep executado pelas tarefas
- linha 42: recupera-se o parâmetro passado ao método Sleep.
- linha 43: a tarefa se identifica no console
- linhas 45-47: exibe-se o número de threads atualmente disponíveis. Queremos observar como ele evolui.
- linha 49: a tarefa é interrompida por alguns segundos (tarefa bloqueante).
- linha 52: quando ela retoma a execução, exibimos algumas informações sobre sua conta.
Os resultados obtidos são os seguintes.
Para os números min e max de threads no pool:
Para a execução das 7 threads:
- linhas 1-6: as três primeiras tarefas são executadas uma após a outra. Elas encontram imediatamente um thread disponível (MinThreads=3) e, em seguida, entram em espera.
- linhas 7-9: para as tarefas 3 e 4, o processo é um pouco mais demorado. Para cada uma delas, não havia nenhum thread livre. Foi necessário criar um. Esse mecanismo é possível até 5 (MaxThreads=5).
- linha 10: não há mais threads disponíveis: as tarefas 5 e 6 terão que aguardar.
- linhas 11-12: a tarefa 0 é concluída. A tarefa 5 ocupa seu thread.
- linhas 13-14: a tarefa 1 é concluída. A tarefa 6 ocupa seu thread.
- linhas 17-21: as tarefas são concluídas uma após a outra.
10.8. A classe BackgroundWorker
10.8.1. Exemplo 1
A classe BackgroundWorker pertence ao espaço de nomes [System.ComponentModel]. Ela é utilizada como um thread, mas apresenta particularidades que podem torná-la, em certos casos, mais interessante do que a classe [Thread]:
- ela emite os seguintes eventos:
- DoWork: um thread solicitou a execução do BackgroundWorker
- ProgressChanged: o objeto BackgroundWorker executou o método ReportProgress. Esse método serve para fornecer uma porcentagem de execução.
- RunWorkerCompleted: o objeto BackgroundWorker concluiu seu trabalho. Ele pode ter sido concluído normalmente, por cancelamento ou por exceção.
Esses eventos tornam o BackgroundWorker útil em interfaces gráficas: uma tarefa demorada será atribuída a um BackgroundWorker, que poderá informar seu andamento com o evento ProgressChanged e sua conclusão com o evento RunWorkerCompleted. O trabalho a ser realizado pelo BackgroundWorker será executado por um método que tenha sido associado ao evento DoWork.
- É possível solicitar seu cancelamento. Em uma interface gráfica, uma tarefa demorada poderá, assim, ser cancelada pelo usuário.
- Os objetos BackgroundWorker pertencem a um pool e são reciclados conforme a necessidade. Um aplicativo que precise de um objeto BackgroundWorker o obterá do pool, que lhe fornecerá um thread já existente, porém não utilizado. O fato de reciclar os threads dessa forma, em vez de criar um novo a cada vez, melhora o desempenho.
Utilizamos essa ferramenta na aplicação anterior, no caso em que o acesso ao balcão não é controlado:
using System;
using System.Threading;
using System.ComponentModel;
namespace Chap8 {
class Program2 {
// uso de threads de leitura e gravação
// ilustra o uso simultâneo de recursos compartilhados e de sincronização
// variáveis de classe
const int nbThreads = 2; // número total de threads
static int nbLecteursTerminés = 0; // número de threads concluídos
static int[] data = new int[5]; // matriz compartilhada entre threads de leitura e threads de gravação
static object appli; // sincroniza o acesso ao número de threads concluídas
static Random objRandom = new Random(DateTime.Now.Second); // um gerador de números aleatórios
static AutoResetEvent peutLire; // indica que é possível ler o conteúdo da matriz
static AutoResetEvent peutEcrire; // indica que é possível gravar na matriz
static AutoResetEvent finLecteurs; // indica o fim dos leitores
//main
public static void Main(string[] args) {
// atribui-se um nome à thread
Thread.CurrentThread.Name = "Main";
// inicialização dos sinalizadores
peutLire = new AutoResetEvent(false); // ainda não é possível ler
peutEcrire = new AutoResetEvent(true); // já é possível gravar
finLecteurs = new AutoResetEvent(false); // aplicativo não finalizado
// sincroniza o acesso ao contador de threads concluídas
appli = new object();
// criação das threads de leitura
MyBackgroundWorker[] lecteurs = new MyBackgroundWorker[nbThreads];
for (int i = 0; i < nbThreads; i++) {
// criação
lecteurs[i] = new MyBackgroundWorker();
lecteurs[i].Numéro = "L" + i;
lecteurs[i].DoWork += Lire;
lecteurs[i].RunWorkerCompleted += EndLecteur;
// inicialização
lecteurs[i].RunWorkerAsync();
}
// criação das threads de gravação
MyBackgroundWorker[] écrivains = new MyBackgroundWorker[nbThreads];
for (int i = 0; i < nbThreads; i++) {
// criação
écrivains[i] = new MyBackgroundWorker();
écrivains[i].Numéro = "E" + i;
écrivains[i].DoWork += Ecrire;
// inicialização
écrivains[i].RunWorkerAsync();
}
// aguardando o término de todas as threads
finLecteurs.WaitOne();
//fim da operação
Console.WriteLine("Fin de Main...");
}
public static void EndLecteur(object sender, RunWorkerCompletedEventArgs infos) {
...
}
// ler o conteúdo da matriz
public static void Lire(object sender, DoWorkEventArgs infos) {
...
}
// gravar na tabela
public static void Ecrire(object sender, DoWorkEventArgs infos) {
...
}
}
// tópico
internal class MyBackgroundWorker : BackgroundWorker {
// informações diversas
public string Numéro { get; set; }
}
}
Detalhamos apenas as alterações:
- a classe Thread foi substituída pela classe MyBackgroundWorker nas linhas 79 a 82. A classe BackgroundWorker foi derivada para atribuir um número ao thread. Poderíamos ter procedido de maneira diferente, passando um objeto para o método RunWorkerAsync nas linhas 43 e 54, objeto que contém o número do thread.
- linha 58: o método Main é encerrado depois que todas as threads de leitura concluíram seu trabalho. Para isso, na linha 12, o contador nbLecteursTerminés conta o número de threads de leitura que concluíram seu trabalho. Esse contador é incrementado pelo método EndLecteur das linhas 63 a 65, que é executado sempre que um thread de leitura é encerrado. É esse procedimento que controla o evento AutoResetEvent finLecteurs da linha 18, com o qual se sincroniza, na linha 59, o método Main.
- linha 16: como várias threads de leitura podem querer incrementar simultaneamente o contador nbLecteursTerminés, o acesso exclusivo a ele é garantido pelo objeto de sincronização appli. Esse caso é improvável, mas teoricamente possível.
- linhas 35-44: criação das threads de leitura
- linha 38: criação do thread do tipo MyBackgroundWorker
- linha 39: atribui-se um número a ele
- linha 40: atribui-se a ele o método Lire a ser executado
- linha 41: o método EndLecteur será executado após o término do thread
- linha 43: o thread é iniciado
- linhas 47-55: criação dos threads de gravação
- linha 50: criação do thread do tipo MyBackgroundWorker
- linha 51: é atribuído um número a ele
- linha 52: atribui-se a ele o método Ecrire a ser executado
- linha 54: o thread é iniciado
Os métodos Lire e Ecrire permanecem inalterados. O método EndLecteur é executado ao final de cada thread de leitura. Seu código é o seguinte:
public static void EndLecteur(object sender, RunWorkerCompletedEventArgs infos) {
// incremento no número de leitores concluídos
lock (appli) {
nbLecteursTerminés++;
if (nbLecteursTerminés == nbThreads)
finLecteurs.Set();
}
}
A função do método EndLecteur é notificar o método Main de que todos os leitores concluíram seu trabalho.
- linha 4: o contador nbLecteursTerminés é incrementado.
- linhas 5-6: se todos os leitores tiverem concluído seu trabalho, então o evento finLecteurs é definido como verdadeiro para notificar o método Main, que aguarda esse evento.
- Como o procedimento EndLecteur é executado por várias threads, a seção crítica anterior é protegida pela cláusula lock da linha 3.
A execução produz resultados semelhantes aos da versão que utiliza threads.
10.8.2. Exemplo 2
O código a seguir ilustra outros aspectos da classe BackgroundWorker:
- a possibilidade de cancelar a tarefa
- o repasse de uma exceção lançada na tarefa
- a passagem de um parâmetro de E/S para a tarefa
using System;
using System.Threading;
using System.ComponentModel;
namespace Chap8 {
class Program3 {
// threads
static BackgroundWorker[] tâches = new BackgroundWorker[5];
public static void Main() {
// inicialização do thread atual
Thread main = Thread.CurrentThread;
// atribuição de nome ao thread
main.Name = "Main";
// criação de threads
for (int i = 0; i < tâches.Length; i++) {
// criação do thread nº i
tâches[i] = new BackgroundWorker();
// inicializando-o
tâches[i].DoWork += Sleep;
tâches[i].RunWorkerCompleted += End;
tâches[i].WorkerSupportsCancellation = true;
// inicia-se o thread
tâches[i].RunWorkerAsync(new Data { Numéro = i, Début = DateTime.Now, Durée = i + 1 });
}
// cancela-se o último thread
tâches[4].CancelAsync();
// fim da rotina
Console.WriteLine("Fin du thread {0}, tapez [entrée] pour terminer...", main.Name);
Console.ReadLine();
return;
}
public static void Sleep(object sender, DoWorkEventArgs infos) {
...
}
public static void End(object sender, RunWorkerCompletedEventArgs infos) {
...
}
internal class Data {
// informações diversas
public int Numéro { get; set; }
public DateTime Début { get; set; }
public int Durée { get; set; }
public DateTime Fin { get; set; }
}
}
}
- linha 9: a matriz de BackgroundWorker
- linhas 18-27: criação dos threads
- linha 20: criação do thread
- linha 22: o thread executará o método Sleep das linhas 39-41
- linha 23: o método End das linhas 43-45 será executado ao final do thread
- linha 24: o thread poderá ser cancelado
- linha 26: o thread é iniciado com um parâmetro do tipo [Data], definido nas linhas 49-52. Esse objeto possui os seguintes campos:
- Numéro (entrada): número do thread
- Début (entrada): hora de início da execução do thread
- Durée (entrada): tempo de execução do Sleep
- Fin (saída): fim da execução do thread
- linha 29: o thread nº 4 é cancelado
Todos os threads executam o seguinte método Sleep:
public static void Sleep(object sender, DoWorkEventArgs infos) {
// o parâmetro infos é processado
Data data = (Data)infos.Argument;
// exceção na tarefa nº 3
if (data.Numéro == 3) {
throw new Exception("test....");
}
// colocação em modo de suspensão por Duração segundos, com uma interrupção a cada segundos
for (int i = 1; i <= data.Durée && !tâches[data.Numéro].CancellationPending; i++) {
// espera de 1 segundo
Thread.Sleep(1000);
}
// fim da execução
data.Fin = DateTime.Now;
// inicializa-se o resultado
infos.Result = data;
infos.Cancel = tâches[data.Numéro].CancellationPending;
}
- linha 1: o método Sleep possui a assinatura padrão dos manipuladores de eventos. Ele recebe dois parâmetros:
- sender: o emissor do evento, neste caso o BackgroundWorker, que executa o método
- infos: do tipo DoWorkEventArgs, que fornece informações sobre o evento DoWork. Esse parâmetro serve tanto para transmitir informações à thread quanto para recuperar seus resultados.
- linha 3: o parâmetro passado ao método RunWorkerAsync da tarefa é encontrado na propriedade infos.Argument.
- linhas 5-7: é lançada uma exceção para a tarefa nº 3
- linhas 9-12: o thread “dorme” Durée segundos em intervalos de um segundo para permitir o teste de cancelamento da linha 9. Isso simula um trabalho de longa duração durante o qual o thread verificaria regularmente se há uma solicitação de cancelamento. Para indicar que foi cancelado, o thread deve definir a propriedade infos.Cancel como verdadeira (linha 17).
- linha 16: o thread pode retornar um resultado ao thread que o iniciou. Ele coloca esse resultado em infos.Result.
Uma vez concluídos, os threads executam o seguinte método End:
public static void End(object sender, RunWorkerCompletedEventArgs infos) {
// utiliza-se o parâmetro “infos” para exibir o resultado da execução
// exceção?
if (infos.Error != null) {
Console.WriteLine("Le thread {1} a rencontré l'erreur suivante : {0}", infos.Error.Message, sender);
} else
if (!infos.Cancelled) {
Data data = (Data)infos.Result;
Console.WriteLine("Thread {0} terminé : début {1:hh:mm:ss}, durée programmée {2} s, fin {3:hh:mm:ss}, durée effective {4}",
data.Numéro, data.Début, data.Durée, data.Fin, (data.Fin - data.Début));
} else {
Console.WriteLine("Thread {0} annulé", sender);
}
}
- linha 1: o método End possui a assinatura padrão dos manipuladores de eventos. Ele recebe dois parâmetros:
- sender: o emissor do evento, neste caso o BackgroundWorker que executa o método
- infos: do tipo RunWorkerCompletedEventArgs, que fornece informações sobre o evento RunWorkerCompleted.
- linha 4: o campo infos.Error, do tipo Exception, é preenchido somente se tiver ocorrido uma exceção.
- linha 7: o campo infos.Cancelled, do tipo booleano, assume o valor true se a thread tiver sido cancelada.
- linha 8: se não houve exceção ou cancelamento, então infos.Result é o resultado do thread executado. Usar esse resultado caso tenha ocorrido uma interrupção do thread ou se o thread tiver lançado uma exceção provoca uma exceção. Assim, nas linhas 5 e 13, não é possível exibir o número do thread interrompido ou que lançou uma exceção, pois esse número está em infos.Result. Esse problema pode ser contornado derivando-se a classe BackgroundWorker para inserir nela as informações a serem trocadas entre o thread chamador e o thread chamado, conforme feito no exemplo anterior. Utiliza-se, então, o argumento sender, que representa o BackgroundWorker, em vez do argumento infos.
Os resultados da execução são os seguintes:
10.9. Dados locais a um thread
10.9.1. O princípio
Consideremos uma aplicação de três camadas:
![]() |
Suponhamos que a aplicação seja multiusuário, uma aplicação web, por exemplo. Cada usuário é atendido por um thread dedicado a ele. O ciclo de vida do thread é o seguinte:
- o thread é criado ou solicitado a um pool de threads para atender a uma solicitação de um usuário
- se essa solicitação exigir dados, o thread executará um método da camada [ui], que chamará um método da camada [metier], que, por sua vez, chamará um método da camada [dao].
- O thread retorna a resposta ao usuário. Em seguida, ele é encerrado ou reciclado em um pool de threads.
Na operação 2, pode ser interessante que o thread possua dados próprios, c.a.d, não compartilhados com os outros threads. Esses dados poderiam, por exemplo, pertencer ao usuário específico que o thread atende. Esses dados poderiam, então, ser utilizados nas diferentes camadas [ui, metier, dao].
A classe Thread permite esse cenário graças a uma espécie de dicionário privado, no qual as chaves seriam do tipo LocalDataStoreSlot:
cria uma entrada no dicionário privado da thread para a chave name. | |
associa o valor data à chave name do dicionário privado do thread | |
recupera o valor associado à chave name do dicionário privado da thread |
Um exemplo de uso poderia ser o seguinte:
- para criar um par (clé,valeur) associado ao thread atual:
- para recuperar o valor associado a clé:
10.9.2. Aplicação do princípio
Consideremos a seguinte aplicação de três camadas:
![]() |
Suponhamos que a camada [dao] gerencie um cadastro de artigos e que sua interface seja inicialmente a seguinte:
using System.Collections.Generic;
namespace Chap8 {
public interface IDao {
int InsertArticle(Article article);
List<Article> GetAllArticles();
void DeleteAllArticles();
}
}
- linha 5: para inserir um artigo no banco de dados
- linha 6: para recuperar todos os itens do banco de dados
- linha 7: para excluir todos os itens do banco de dados
Posteriormente, surge a necessidade de um método para inserir uma tabela de itens por meio de uma transação, pois se deseja operar no modo “tudo ou nada”: ou todos os itens são inseridos ou nenhum. É possível, então, modificar a interface para integrar essa nova necessidade:
using System.Collections.Generic;
namespace Chap8 {
public interface IDao {
int InsertArticle(Article article);
void insertArticles(Article[] articles);
List<Article> GetAllArticles();
void DeleteAllArticles();
}
}
- linha 6: para adicionar uma tabela de itens ao banco de dados
Posteriormente, para outra aplicação, surge a necessidade de excluir uma lista de itens registrada em uma lista, sempre em uma transação. Percebe-se que, para atender a diferentes necessidades de negócio, a camada [dao] terá que crescer. Pode-se seguir outro caminho:
- incluir na camada [dao] apenas as operações básicas InsertArticle, DeleteArticle, UpdateArticle, SelectArticle e SelectArticles
- transferir para a camada [métier] as operações de atualização simultânea de vários itens. Essas operações utilizariam as operações elementares da camada [dao].
A vantagem dessa solução é que a mesma camada [dao] poderia ser utilizada sem alterações com diferentes camadas [metier]. Ela apresenta uma dificuldade no gerenciamento da transação que agrupa atualizações a serem realizadas de forma atômica no banco de dados:
- a transação deve ser iniciada pela camada [metier] antes que ela chame os métodos da camada [dao]
- os métodos da camada [dao] devem estar cientes da existência da transação para poderem participar dela, caso ela exista
- a transação deve ser encerrada pela camada [métier].
Para que os métodos da camada [dao] reconheçam a existência de uma possível transação em andamento, seria possível adicionar a transação como parâmetro de cada método da camada [dao]. Esse parâmetro aparecerá, então, na assinatura dos métodos da interface, o que a vinculará a uma fonte de dados específica: o banco de dados. Os dados locais da thread nos oferecem uma solução mais elegante: a camada [métier] colocará a transação nos dados locais da thread, e é lá que a camada [dao] irá buscá-la. A assinatura dos métodos da camada [dao], portanto, não precisa ser alterada.
Implementamos essa solução com o seguinte projeto do Visual Studio:
![]() |
![]() |
- em [1]: a solução como um todo
- em [2]: as referências utilizadas. Como o banco de dados [4] é um banco de dados SQL Server Compact, é necessário ter a referência [System.Data.SqlServerCe].
- em [3]: as diferentes camadas do aplicativo.
A base [4] é a base SQL Server Compact já utilizada no capítulo anterior, notadamente no parágrafo 9.3.1.
![]() |
A classe Article
Uma linha da tabela [articles] anterior é encapsulada em um objeto do tipo Article:
namespace Chap8 {
public class Article {
// propriedades
public int Id { get; set; }
public string Nom { get; set; }
public decimal Prix { get; set; }
public int StockActuel { get; set; }
public int StockMinimum { get; set; }
// construtores
public Article() {
}
public Article(int id, string nom, decimal prix, int stockActuel, int stockMinimum) {
Id = id;
Nom = nom;
Prix = prix;
StockActuel = stockActuel;
StockMinimum = stockMinimum;
}
// identidade
public override string ToString() {
return string.Format("[{0},{1},{2},{3},{4}]", Id, Nom, Prix, StockActuel, StockMinimum);
}
}
}
Interface da camada [dao]
A interface IDao da camada [dao] será a seguinte:
using System.Collections.Generic;
namespace Chap8 {
public interface IDao {
int InsertArticle(Article article);
List<Article> GetAllArticles();
void DeleteAllArticles();
}
}
- linha 5: para inserir um item na tabela [articles]
- linha 6: para inserir todas as linhas da tabela [articles] em uma lista de objetos Article
- linha 7: para excluir todas as linhas da tabela [articles]
Interface da camada [metier]
A interface IMetier da camada [metier] será a seguinte:
using System.Collections.Generic;
namespace Chap8 {
interface IMetier {
void InsertArticlesInTransaction(Article[] articles);
void InsertArticlesOutOfTransaction(Article[] articles);
List<Article> GetAllArticles();
void DeleteAllArticles();
}
}
- linha 5: para inserir, dentro de uma transação, um conjunto de itens
- linha 6: o mesmo, mas sem transação
- linha 7: para obter a lista de todos os itens
- linha 8: para excluir todos os itens
Implementação da camada [metier]
A implementação de negócio da interface IMetier será a seguinte:
using System.Collections.Generic;
using System.Data;
using System.Data.SqlServerCe;
using System.Threading;
namespace Chap8 {
public class Metier : IMetier {
// camada [dao]
public IDao Dao { get; set; }
// cadeia de conexão
public string ConnectionString { get; set; }
// inserção de uma tabela de itens dentro de uma transação
public void InsertArticlesInTransaction(Article[] articles) {
// criação da conexão com o banco de dados
using (SqlCeConnection connexion = new SqlCeConnection(ConnectionString)) {
// abertura da conexão
connexion.Open();
// transação
SqlCeTransaction transaction = null;
try {
// início da transação
transaction = connexion.BeginTransaction(IsolationLevel.ReadCommitted);
// a transação é registrada na thread
Thread.SetData(Thread.GetNamedDataSlot("transaction"), transaction);
// inserção dos itens
foreach (Article article in articles) {
Dao.InsertArticle(article);
}
// confirma-se a transação
transaction.Commit();
} catch {
// revertendo a transação
if (transaction != null)
transaction.Rollback();
}
}
}
// inserção de uma tabela de itens sem transação
public void InsertArticlesOutOfTransaction(Article[] articles) {
// inserção de itens
foreach (Article article in articles) {
Dao.InsertArticle(article);
}
}
// lista de artigos
public List<Article> GetAllArticles() {
return Dao.GetAllArticles();
}
// excluir todos os itens
public void DeleteAllArticles() {
Dao.DeleteAllArticles();
}
}
}
A classe possui as seguintes propriedades:
- linha 9: uma referência à camada [dao]
- linha 11: a string de conexão que permite conectar-se ao banco de dados de artigos
Comentaremos apenas o método InsertArticlesInTransaction, que é o único que apresenta dificuldades:
- linha 16: é criada uma conexão com o banco de dados
- linha 18: a conexão é aberta
- linha 23: é criada uma transação
- linha 25: ela é registrada nos dados locais do thread, associada à chave “transaction”
- linhas 27-29: o método de inserção unitária da camada [dao] é chamado para cada item a ser inserido
- linhas 21 e 32: toda a inserção da tabela é controlada por um try/catch
- linha 31: se chegarmos até aqui, significa que não houve nenhuma exceção. Validamos, então, a transação.
- linhas 34-35: ocorreu uma exceção; a transação é revertida
- linha 37: sai-se da cláusula using. A conexão aberta na linha 18 é fechada automaticamente.
Implementação da camada [dao]
A implementação DAO da interface IDao será a seguinte:
using System.Collections.Generic;
using System.Data;
using System.Data.SqlServerCe;
using System.Threading;
namespace Chap8 {
public class Dao : IDao {
// cadeia de conexão
public string ConnectionString { get; set; }
// consultas
public string InsertText { get; set; }
public string DeleteAllText { get; set; }
public string GetAllText { get; set; }
// implementação da interface
// inserção de artigo
public int InsertArticle(Article article) {
// há alguma transação em andamento?
SqlCeTransaction transaction = Thread.GetData(Thread.GetNamedDataSlot("transaction")) as SqlCeTransaction;
// recuperar a conexão ou criá-la
SqlCeConnection connexion = null;
if (transaction != null) {
// recuperar a conexão
connexion = transaction.Connection as SqlCeConnection;
} else {
// criá-la
connexion = new SqlCeConnection(ConnectionString);
connexion.Open();
}
try {
// preparação do comando de inserção
SqlCeCommand sqlCommand = new SqlCeCommand();
sqlCommand.Transaction = transaction;
sqlCommand.Connection = connexion;
sqlCommand.CommandText = InsertText;
sqlCommand.Parameters.Add("@nom", SqlDbType.NVarChar, 30);
sqlCommand.Parameters.Add("@prix", SqlDbType.Money);
sqlCommand.Parameters.Add("@sa", SqlDbType.Int);
sqlCommand.Parameters.Add("@sm", SqlDbType.Int);
sqlCommand.Parameters["@nom"].Value = article.Nom;
sqlCommand.Parameters["@prix"].Value = article.Prix;
sqlCommand.Parameters["@sa"].Value = article.StockActuel;
sqlCommand.Parameters["@sm"].Value = article.StockMinimum;
// execução
return sqlCommand.ExecuteNonQuery();
} finally {
// se não estivéssemos em uma transação, fecharíamos a conexão
if (transaction == null) {
connexion.Close();
}
}
}
// lista de itens
public List<Article> GetAllArticles() {
...
}
// exclusão de itens
public void DeleteAllArticles() {
...
}
}
}
A classe possui as seguintes propriedades:
- linha 9: a string de conexão que permite conectar-se ao banco de dados de artigos
- linha 11: a ordem SQL para inserir um artigo
- linha 12: o comando SQL para excluir todos os artigos
- linha 13: o comando SQL para obter todos os artigos
Essas propriedades serão inicializadas a partir do seguinte arquivo de configuração [App.config]:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<add name="dbArticlesSqlServerCe" connectionString="Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;" />
</connectionStrings>
<appSettings>
<add key="insertText" value="insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)"/>
<add key="getAllText" value="select id,nom,prix,stockactuel,stockminimum from articles"/>
<add key="deleteAllText" value="delete from articles"/>
</appSettings>
</configuration>
Comentamos o método InsertArticle:
- linha 20: recuperamos a eventual transação que a camada [metier] possa ter iniciado na thread
- linhas 23-25: se a transação estiver presente, recupera-se a conexão à qual ela foi vinculada.
- linhas 26-30: caso contrário, uma nova conexão é criada e aberta.
- linhas 33-44: prepara-se o comando de inserção. Este é configurado (ver linha g de App.config).
- linha 33: o objeto Command é criado.
- linha 34: está associada à transação atual. Se esta não existir (transação=null), isso equivale a executar a ordem SQL sem uma transação explícita. Vale lembrar que, nesse caso, ainda há uma transação implícita. Com SQL Server CE, essa transação implícita está, por padrão, no modo autocommit: a ordem SQL passa a ser committé após sua execução.
- linha 35: o objeto Command está associado à conexão atual
- linha 36: o texto SQl a ser executado é definido. Trata-se da consulta parametrizada da linha g de App.config.
- linhas 37-44: os 4 parâmetros da consulta são inicializados
- linha 46: a consulta é executada.
- linhas 49-51: é preciso lembrar que, caso não houvesse transação, uma nova conexão com o banco de dados foi aberta nas linhas 26-30. Nesse caso, ela deve ser fechada. Se houvesse uma transação, a conexão não deve ser fechada, pois é a camada [metier] que a gerencia.
Os outros dois métodos retomam o que foi visto no capítulo “Bancos de dados”:
// lista de itens
public List<Article> GetAllArticles() {
// lista de itens — vazia inicialmente
List<Article> articles = new List<Article>();
// análise da conexão
using (SqlCeConnection connexion = new SqlCeConnection(ConnectionString)) {
// abertura de conexão
connexion.Open();
// executa sqlCommand com consulta SELECT
SqlCeCommand sqlCommand = new SqlCeCommand(GetAllText, connexion);
using (SqlCeDataReader reader = sqlCommand.ExecuteReader()) {
// processamento do resultado
while (reader.Read()) {
// análise da linha atual
articles.Add(new Article(reader.GetInt32(0), reader.GetString(1), reader.GetDecimal(2), reader.GetInt32(3), reader.GetInt32(4)));
}
}
}
// retorno do resultado
return articles;
}
// exclusão dos itens
public void DeleteAllArticles() {
using (SqlCeConnection connexion = new SqlCeConnection(ConnectionString)) {
// abertura de conexão
connexion.Open();
// executa sqlCommand com solicitação de atualização
new SqlCeCommand(DeleteAllText, connexion).ExecuteNonQuery();
}
}
O aplicativo de teste [console]
A aplicação de teste [console] é a seguinte:
using System;
using System.Configuration;
namespace Chap8 {
class Program {
static void Main(string[] args) {
// processamento do arquivo de configuração
string connectionString = null;
string insertText;
string getAllText;
string deleteAllText;
try {
// cadeia de conexão
connectionString = ConfigurationManager.ConnectionStrings["dbArticlesSqlServerCe"].ConnectionString;
// outros parâmetros
insertText = ConfigurationManager.AppSettings["insertText"];
getAllText = ConfigurationManager.AppSettings["getAllText"];
deleteAllText = ConfigurationManager.AppSettings["deleteAllText"];
} catch (Exception e) {
Console.WriteLine("Erreur de configuration : {0}", e.Message);
return;
}
// criação da camada [dao]
Dao dao = new Dao();
dao.ConnectionString = connectionString;
dao.DeleteAllText = deleteAllText;
dao.GetAllText = getAllText;
dao.InsertText = insertText;
// criação de camada [métier]
Metier metier = new Metier();
metier.Dao = dao;
metier.ConnectionString = connectionString;
// cria-se uma tabela de itens
Article[] articles = new Article[2];
for (int i = 0; i < articles.Length; i++) {
articles[i] = new Article(0, "article", 100, 10, 1);
}
// exclui-se todos os itens
Console.WriteLine("Suppression de tous les articles...");
metier.DeleteAllArticles();
// insere-se a tabela fora da transação
Console.WriteLine("Insertion des articles hors transaction...");
try {
metier.InsertArticlesOutOfTransaction(articles);
} catch (Exception e){
Console.WriteLine("Exception : {0}", e.Message);
}
// exibe-se os itens
Console.WriteLine("Liste des articles");
AfficheArticles(metier);
// exclui-se todos os itens
Console.WriteLine("Suppression de tous les articles...");
metier.DeleteAllArticles();
// inserir a tabela em uma transação
Console.WriteLine("Insertion des articles dans une transaction...");
metier.InsertArticlesInTransaction(articles);
// exibe os itens
Console.WriteLine("Liste des articles");
AfficheArticles(metier);
}
private static void AfficheArticles(IMetier metier) {
// exibe os itens
foreach(Article article in metier.GetAllArticles()){
Console.WriteLine(article);
}
}
}
}
- linhas 12-22: o arquivo [App.config] é utilizado.
- linhas 24-28: a camada [dao] é instanciada e inicializada
- linhas 30-32: o mesmo é feito para a camada [metier]
- linhas 34-37: cria-se uma tabela com 2 itens com o mesmo nome. A tabela [articles] do banco de dados SQL no servidor Ce [dbarticles.sdf] possui uma restrição de exclusividade para o nome. A inserção do segundo item será, portanto, recusada. Se a inserção da tabela for feita fora de uma transação, o primeiro item será inserido primeiro e permanecerá na tabela. Se a inserção da tabela for feita dentro de uma transação, o primeiro item será inserido primeiro e, em seguida, removido durante a execução do Rollback da transação.
- linhas 39-50: inserção fora de transação da tabela com 2 itens e verificação.
- linhas 52-59: o mesmo, mas dentro de uma transação
Os resultados da execução são os seguintes:
- linhas 5-6: a inserção fora da transação deixou o primeiro artigo no banco de dados
- linha 9: a inserção feita dentro de uma transação não deixou nenhum artigo no banco de dados
10.9.3. Conclusão
O exemplo anterior demonstrou a importância dos dados locais a um thread para o gerenciamento de transações. Ele não deve ser reproduzido tal como está. Frameworks como Spring, Nhibernate, etc., utilizam essa técnica, mas a tornam ainda mais transparente: é possível que a camada [metier] utilize transações sem que a camada [dao] precise saber disso. Portanto, não há nenhum objeto Transaction no código da camada [dao]. Isso é obtido por meio de uma técnica de proxy chamada AOP (Programação Orientada a Aspectos). Mais uma vez, não podemos deixar de incentivar o leitor a utilizar esses frameworks.
10.10. Para aprofundar...
Para aprofundar o complexo tema da sincronização de threads, recomenda-se a leitura do capítulo Threading do livro C# 3.0 mencionado na introdução deste documento. Nele são apresentadas diversas técnicas de sincronização para diferentes tipos de situações.







