10. Threads de execução
10.1. A classe Thread
Quando uma aplicação é iniciada, ela é executada num fluxo de execução denominado thread. A classe .NET que modela um thread é a System.Threading.Thread e tem a seguinte definição:
Fabricantes
![]() |
Nos exemplos seguintes, utilizaremos apenas os construtores [1,3]. O construtor [1] admite como parâmetro um método com a assinatura [2], ou seja, com um parâmetro do tipo object e que não retorna nenhum resultado. O construtor [3] aceita como parâmetro um método com a assinatura [4], ou seja, que não tem 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 o código que solicita esta propriedade está localizado
- string Name: nome da thread
- bool IsAlive: indica se a thread está em execução ou não.
Métodos
Os métodos mais utilizados são:
- Start(), Start(object obj): inicia a execução assíncrona da thread, possivelmente passando-lhe informações num objeto.
- Abort(), Abort(object obj) : para encerrar forçosamente uma thread
- Join() : a thread T1 que executa T2.Join fica bloqueada até que T2 termine. Existem variantes para terminar a espera após um tempo definido.
- Sleep(int n) : método estático - a thread que executa o método é suspensa durante n milissegundos. Em seguida, perde o processador, que é cedido a outra thread.
Vamos dar uma olhada numa primeira aplicação que demonstra a existência de uma thread principal de execução, aquela na qual a função Main de uma classe:
using System;
using System.Threading;
namespace Chap8 {
class Program {
static void Main(string[] args) {
// init current thread
Thread main = Thread.CurrentThread;
// display
Console.WriteLine("Thread courant : {0}", main.Name);
// we change the name
main.Name = "main";
// check
Console.WriteLine("Thread courant : {0}", main.Name);
// infinite loop
while (true) {
// display
Console.WriteLine("{0} : {1:hh:mm:ss}", main.Name, DateTime.Now);
// temporary shutdown
Thread.Sleep(1000);
}//while
}
}
}
- linha 8: recupera uma referência à thread na qual o método [main] está a ser executado
- linhas 10-14: exibir e modificar o seu nome
- linhas 17-22: um loop que exibe uma mensagem a cada segundo
- linha 21: o thread no qual o método [main] está a ser executado será suspenso durante 1 segundo
Os resultados no ecrã são os seguintes:
- linha 1: o segmento atual não tinha nome
- linha 2: agora tem um
- linhas 3-7: exibir a cada segundo
- linha 8: o programa é interrompido por Ctrl-C.
10.2. Criação de threads de execução
É possível ter aplicações em que partes do código são executadas «simultaneamente» em diferentes threads de execução. Quando dizemos que as threads são executadas simultaneamente, isso é frequentemente um equívoco. Se a máquina tiver apenas um processador, como ainda é frequentemente o caso, as threads partilham esse processador: cada uma tem acesso a ele, por sua vez, durante um curto período de tempo (alguns milissegundos). Isto dá a ilusão de execução paralela. A porção de tempo alocada a uma thread depende de vários fatores, incluindo a sua prioridade, que tem um valor padrão, mas também pode ser definida programaticamente. Quando uma thread tem o processador, utiliza-o normalmente durante todo o tempo alocado. No entanto, pode libertá-lo antecipadamente:
- aguardando um evento (Wait, Join)
- colocando-se em modo de suspensão por um período de tempo definido (Sleep)
- Uma thread T é criada inicialmente por um dos fabricantes apresentados acima, por exemplo:
onde Start é um método com uma das duas assinaturas seguintes:
A criação de um segmento de execução não o inicia.
- A 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 T.Start() não aguarda que a tarefa T termine: passa imediatamente para a instrução seguinte. Isto significa que duas tarefas estão a ser executadas em paralelo. Em muitos casos, elas precisam de poder comunicar entre si para acompanhar o progresso do seu trabalho conjunto. Este é o problema da sincronização de threads.
- Uma vez iniciada, a thread T executa-se de forma autónoma. Ela irá parar quando o Start que executa tiver concluído o seu trabalho.
- A thread T pode ser forçada a terminar:
- T.Abort() solicita que a thread T termine.
- Também é possível aguardar o fim da sua execução através de T.Join(). Esta é uma instrução de bloqueio: o programa que a executa fica bloqueado até que a tarefa T tenha concluído o seu trabalho. Este é um meio de sincronização.
Vejamos o seguinte programa:
using System;
using System.Threading;
namespace Chap8 {
class Program {
public static void Main() {
// init Current thread
Thread main = Thread.CurrentThread;
// name the Thread
main.Name = "Main";
// creation of execution threads
Thread[] tâches = new Thread[5];
for (int i = 0; i < tâches.Length; i++) {
// create thread i
tâches[i] = new Thread(Affiche);
// set the thread name
tâches[i].Name = i.ToString();
// start execution of thread i
tâches[i].Start();
}
// end of hand
Console.WriteLine("Fin du thread {0} à {1:hh:mm:ss}",main.Name,DateTime.Now);
}
public static void Affiche() {
// display start of execution
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);
// sleep for 1 s
Thread.Sleep(1000);
// display end of run
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: atribuem um nome ao thread que executa o método [Main]
- linhas 13-21: são criadas e executadas 5 threads. As referências das threads são armazenadas numa matriz para recuperação posterior. Cada thread executa o Poster linhas 27-35.
- linha 20: a thread n.º i é iniciada. Esta operação é não-bloqueante. A thread n.º i será executada em paralelo com a thread do método [Main] que a lançou.
- linha 24: a thread que executa o método [Main] termina.
- linhas 27-35: o método [Display] efetua exibições. Exibe o nome da thread que o está a executar, bem como as horas de início e fim da execução.
- linha 31: qualquer thread que esteja a executar o método [Display] irá parar durante 1 segundo. O processador será então cedido a outra thread que esteja à espera do processador. No final do segundo de paragem, a thread que estava parada será candidata ao processador. Receberá o processador quando chegar a sua vez. Isto depende de vários fatores, incluindo a prioridade de outras threads que estejam à espera do processador.
Os resultados são os seguintes:
Estes resultados são altamente instrutivos:
- em primeiro lugar, podemos ver que o lançamento da execução de um thread não é bloqueante. O Main lançou a execução de 5 threads em paralelo e concluiu a sua execução antes deles. A operação
// on lance l'exécution du thread i
tâches[i].Start();
inicia a execução da thread tasks[i], mas assim que isso é feito, a execução continua imediatamente com a instrução seguinte, sem esperar que a thread termine a sua execução.
- Todos os threads criados devem executar o método Affiche. A ordem de execução é imprevisível. Embora no exemplo a ordem de execução pareça seguir a ordem dos pedidos de execução, não se podem tirar conclusões gerais a partir disso. O sistema operativo tem aqui 6 threads e um processador. Ele distribuirá o processador por esses 6 threads de acordo com as suas próprias regras.
- Os resultados são uma consequência do método Sleep. No exemplo, a thread 0 é a primeira a executar o método Affiche. A mensagem de início de execução é exibida e, em seguida, executa-se o Sleep, que o suspende por 1 segundo. Em seguida, perde o processador, que fica disponível para outro thread. O exemplo mostra que o thread 1 o irá obter. O thread 1 seguirá o mesmo caminho que os outros threads. Quando o segundo de suspensão do thread 0 terminar, a sua execução pode ser retomada. O sistema atribui-lhe o processador e ele pode terminar de executar o método Affiche.
Vamos modificar o nosso programa para encerrar a função Main com as seguintes instruções:
// end of hand
Console.WriteLine("Fin du thread " + main.Name);
// stop all threads
Environment.Exit(0);
A execução do novo programa produz os seguintes resultados:
- linhas 1-5: os threads criados pelo Main iniciam a execução e são interrompidos por 1 segundo
- linha 6: o thread [Main] recupera o processador e executa a instrução:
Esta instrução interrompe todas as threads e não apenas a Main.
Se o Main quiser aguardar que as threads que criou terminem a execução, pode utilizar a classe Join da Thread:
public static void Main() {
...
// we wait for all threads
for (int i = 0; i < tâches.Length; i++) {
// wait for thread i to finish execution
tâches[i].Join();
}
// end of hand
Console.WriteLine("Fin du thread {0} à {1:hh:mm:ss}", main.Name, DateTime.Now);
}
- linha 6: o thread [Main] aguarda cada um dos threads. Primeiro fica bloqueado à espera do thread n.º 1, depois do thread n.º 2, etc... Finalmente, quando sai do ciclo das linhas 2-5, todos os 5 threads que iniciou estão concluídos.
Os resultados são os seguintes:
- linha 11: o thread [Main] terminou após os threads que tinha iniciado.
10.3. As vantagens dos fios
Agora que destacámos a existência de um thread padrão, aquele que executa o Main, e sabemos como criar novos, vamos ver o que os threads significam para nós e por que os estamos a introduzir aqui. Há um tipo de aplicação que se presta bem ao uso de threads, e são as aplicações cliente-servidor da Internet. Iremos apresentá-las no capítulo seguinte. Numa aplicação cliente-servidor da Internet, um servidor na máquina S1 responde a pedidos de clientes em máquinas remotas C1, C2, ..., Cn.
![]() |
Todos os dias, usamos aplicações da Internet que correspondem a este diagrama: serviços Web, e-mail, consulta de fóruns, transferência de ficheiros... No diagrama acima, o servidor S1 deve atender os clientes Ci simultaneamente. Se tomarmos o exemplo de um servidor FTP (File Transfer Protocol) a entregar ficheiros aos seus clientes, sabemos que uma transferência de ficheiros pode, por vezes, demorar vários minutos. É claro que está fora de questão um cliente monopolizar o servidor durante esse período de tempo. O que se faz normalmente é o servidor criar tantos threads de execução quantos forem os clientes. Cada thread é então responsável por lidar com um cliente específico. Como o processador é partilhado ciclicamente entre todos os threads ativos da máquina, o servidor dedica um pouco de tempo a cada cliente, garantindo um serviço simultâneo.
![]() |
Na prática, o servidor utiliza um conjunto de threads com um número limitado de threads, 50, por exemplo. O 51.º cliente é então convidado a aguardar.
10.4. Troca de informações entre threads
Nos exemplos anteriores, uma thread foi inicializada da seguinte forma:
onde Run era um método com a seguinte assinatura:
Também é possível utilizar a seguinte assinatura:
Isto permite que as informações sejam transmitidas para o thread iniciado. Por exemplo,
irá iniciar o t, que, por definição, executará o Run a ele associado, passando-lhe o parâmetro efetivo obj1. Aqui está um exemplo:
using System;
using System.Threading;
namespace Chap8 {
class Program4 {
public static void Main() {
// init Current thread
Thread main = Thread.CurrentThread;
// name the Thread
main.Name = "Main";
// creation of execution threads
Thread[] tâches = new Thread[5];
Data[] data = new Data[5];
for (int i = 0; i < tâches.Length; i++) {
// create thread i
tâches[i] = new Thread(Sleep);
// set the thread name
tâches[i].Name = i.ToString();
// start execution of thread i
tâches[i].Start(data[i] = new Data { Début = DateTime.Now, Durée = i+1 });
}
// we wait for all threads
for (int i = 0; i < tâches.Length; i++) {
// wait for thread i to finish execution
tâches[i].Join();
// result display
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));
}
// end of hand
Console.WriteLine("Fin du thread {0} à {1:hh:mm:ss}", main.Name, DateTime.Now);
}
public static void Sleep(object infos) {
// parameter is retrieved
Data data = (Data)infos;
// sleep mode for Duration
Thread.Sleep(data.Durée*1000);
// end of execution
data.Fin = DateTime.Now;
}
}
internal class Data {
// miscellaneous information
public DateTime Début { get; set; }
public int Durée { get; set; }
public DateTime Fin { get; set; }
}
}
- linhas 45-50: informação do tipo [Data] passada para os threads :
- Start: hora de início da execução da thread - definida pela thread de lançamento
- Duração: duração em segundos do Sleep executado pela thread lançada - definida pela thread lançadora
- Fim: hora de início da execução da thread - definida pela thread lançada
- 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 está associada ao método Sleep na linha 35
- linha 21: um objeto do tipo [Data] é passado para o Start, que lança a thread. Neste objeto, registámos a hora de início da execução da thread e a duração em segundos durante a qual ela deve permanecer em espera. Este objeto é armazenado na tabela na linha 14.
- linhas 24-30: a thread [Main] aguarda que todas as threads que iniciou terminem.
- linhas 28-29: a thread [Main] recupera o objeto data[i] da thread n.º i e exibe o seu conteúdo.
- linhas 35-42: o método Sleep executado pelas threads
- linha 37: o parâmetro de tipo [Data] é recuperado
- linha 39: o parâmetro do campo Duration é utilizado para definir o Sleep
- linha 41: o campo End do parâmetro é inicializado
Os resultados são os seguintes:
Este exemplo mostra que duas threads podem trocar informações:
- o thread lançador pode controlar a execução do thread lançado, fornecendo-lhe informações
- o thread lançado pode devolver resultados ao thread de lançamento.
Para que a thread lançada saiba quando os resultados pelos quais está à espera estão disponíveis, deve ser avisada quando a thread lançada terminar. Aqui, esperou que ela terminasse utilizando o Join. Existem outras formas de fazer o mesmo. Veremos essas formas mais tarde.
10.5. Acesso concorrente a recursos partilhados
10.5.1. Acesso simultâneo não sincronizado
No parágrafo sobre a troca de informações entre threads, a informação trocada era apenas entre duas threads e em momentos muito específicos. Tratava-se de uma passagem de parâmetros clássica. Noutros casos, a informação é partilhada por várias threads, que podem querer lê-la ou atualizá-la ao mesmo tempo. Isto levanta o problema da integridade da informação. Suponha que a informação partilhada é uma estrutura S com vários itens de informação I1, I2, ... In.
- uma thread T1 começa a atualizar a estrutura S: modifica o campo I1 e é interrompida antes de concluir toda a atualização da estrutura S
- uma thread T2 que recupera o processador lê então a estrutura S para tomar decisões. Ela lê uma estrutura num estado instável: alguns campos estão atualizados, outros não.
Chamamos a esta situação «acesso a um recurso partilhado», neste caso a estrutura S, e muitas vezes é bastante complicado de gerir. Vejamos o seguinte exemplo para ilustrar os problemas que podem surgir:
- uma aplicação irá gerar n threads, sendo n passado como parâmetro
- o recurso partilhado é um contador que deve ser incrementado por cada thread gerada
- no final da aplicação, o valor do contador é apresentado. Devemos, portanto, determinar n.
O programa é o seguinte:
using System;
using System.Threading;
namespace Chap8 {
class Program {
// class variables
static int cptrThreads = 0; // thread counter
//hand
public static void Main(string[] args) {
// instructions for use
const string syntaxe = "pg nbThreads";
const int nbMaxThreads = 100;
// verification no. of arguments
if (args.Length != 1) {
// error
Console.WriteLine(syntaxe);
// stop
Environment.Exit(1);
}
// argument quality check
int nbThreads = 0;
bool erreur = false;
try {
nbThreads = int.Parse(args[0]);
if (nbThreads < 1 || nbThreads > nbMaxThreads)
erreur = true;
} catch {
// error
erreur = true;
}
// mistake?
if (erreur) {
// error
Console.Error.WriteLine("Nombre de threads incorrect (entre 1 et 100)");
// end
Environment.Exit(2);
}
// thread creation and generation
Thread[] threads = new Thread[nbThreads];
for (int i = 0; i < nbThreads; i++) {
// creation
threads[i] = new Thread(Incrémente);
// naming
threads[i].Name = "" + i;
// launch
threads[i].Start();
}//for
// waiting for threads to finish
for (int i = 0; i < nbThreads; i++) {
threads[i].Join();
}
// counter display
Console.WriteLine("Nombre de threads générés : " + cptrThreads);
}
public static void Incrémente() {
// increases thread counter
// meter reading
int valeur = cptrThreads;
// follow-up
Console.WriteLine("A {0:hh:mm:ss}, le thread {1} a lu la valeur du compteur : {2}", DateTime.Now, Thread.CurrentThread.Name, cptrThreads);
// waiting
Thread.Sleep(1000);
// counter incrementation
cptrThreads = valeur + 1;
// follow-up
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 da geração de threads, que já foi abordada. Em vez disso, vamos dar uma olhada no Increment, da linha 59, usado por cada thread para incrementar o contador estático cptrThreads na linha 8.
- linha 62: o contador é lido
- linha 66: a thread pára durante 1 segundo. Por conseguinte, perde o processador
- linha 68: o contador é incrementado
O Passo 2 existe apenas para forçar a thread a perder o processador. O processador será cedido a outra thread. Na prática, não há garantia de que uma thread não seja interrompida entre o momento em que lê o contador e o momento em que o incrementa. Mesmo que escreva cptrThreads++, dando a ilusão de uma única instrução, existe o risco de perder o processador entre a leitura do valor do contador e a gravação do seu valor incrementado em 1. Na verdade, a operação de alto nível cptrThreads++ será objeto de várias instruções elementares ao nível do processador. A fase de espera de um segundo do passo 2 existe, portanto, apenas para sistematizar este risco.
Os resultados obtidos com 5 threads são os seguintes:
Ao ler estes resultados, é fácil perceber o que se passa:
- linha 1: um primeiro thread lê o contador. Encontra 0. Pára durante 1 segundo e perde o processador
- linha 2: um segundo thread assume o processador e também lê o valor do contador. Este continua a 0, uma vez que o thread anterior ainda não o incrementou. Este também pára durante 1 segundo e perde o processador.
- linhas 1-5: em 1 s, todas as 5 threads têm tempo para passar e ler o valor 0.
- linhas 6-10: quando acordarem uma após a outra, irão incrementar o valor 0 que leram e escrever o valor 1 no contador, conforme confirmado pelo programa principal (Main) na linha 11.
Qual é o problema? A segunda thread leu o valor errado porque a primeira tinha sido interrompida antes de concluir a sua tarefa de atualizar o contador na janela. Isto leva-nos à noção de recursos críticos e secções críticas de um programa:
- um recurso crítico é um recurso que só pode ser detido por um thread de cada vez. Aqui, o recurso crítico é o contador.
- Uma secção crítica de um programa é uma sequência de instruções no fluxo de execução de um segmento durante a qual este acede a um recurso crítico. É necessário garantir que, durante esta secção crítica, esse seja o único segmento a aceder ao recurso.
No nosso exemplo, a secção crítica é o código entre a leitura do contador e a gravação do seu novo valor:
// lecture compteur
int valeur = cptrThreads;
// attente
Thread.Sleep(1000);
// incrémentation compteur
cptrThreads = valeur + 1;
Para executar este código, é necessário garantir que uma thread esteja sozinha. Ela pode ser interrompida, mas durante essa interrupção, outra thread não deve poder executar o mesmo código. A plataforma .NET oferece várias ferramentas para garantir a entrada unitária em secções críticas do código. Vamos dar uma olhada em algumas delas.
10.5.2. A cláusula lock
A cláusula lock é utilizada para definir uma secção crítica da seguinte forma:
obj deve ser uma referência a um objeto visível para todas as threads que executam a secção crítica. O bloqueio garante que apenas uma thread de cada vez executará a secção crítica. O exemplo anterior é reescrito da seguinte forma:
using System;
using System.Threading;
namespace Chap8 {
class Program2 {
// class variables
static int cptrThreads = 0; // thread counter
static object synchro = new object(); // synchronization object
//hand
public static void Main(string[] args) {
...
// waiting for threads to finish
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);
}
// counter display
Console.WriteLine("Nombre de threads générés : " + cptrThreads);
}
public static void Incrémente() {
// increases thread counter
// exclusive access to the meter is required
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) {
// meter reading
int valeur = cptrThreads;
// follow-up
Console.WriteLine("A {0:hh:mm:ss}, le thread {1} a lu la valeur du compteur : {2}", DateTime.Now, Thread.CurrentThread.Name, cptrThreads);
// waiting
Thread.Sleep(1000);
// counter incrementation
cptrThreads = valeur + 1;
// follow-up
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 sincroniza todas as threads.
- linhas 16-23: o método [Main] aguarda as threads na ordem inversa à da sua criação.
- linhas 29-40: a secção crítica do método Increment foi delimitada pelo bloqueio.
Os resultados obtidos com 3 threads são os seguintes:
- o thread 0 é o primeiro a entrar na secção crítica: linhas 1, 2, 6, 8
- os outros dois threads ficarão bloqueados até que o thread 0 saia da secção crítica: linhas 3 e 4
- o thread 1 é o próximo: linhas 7, 9, 10
- o thread 2 é o próximo: linhas 11, 12, 13
- linha 14: a thread Main, que aguarda a conclusão da thread 2, é avisada
- linha 15: a thread Main está agora à espera que a thread 1 termine. Esta thread já terminou. A thread Main é notificada imediatamente, linha 16.
- linhas 17-18: o mesmo processo ocorre com a thread 0
- linha 19: o número de threads está correto
10.5.3. A classe Mutex
A classe System.Threading.Mutex também pode ser utilizada para delimitar secções críticas. Difere do lock em termos de visibilidade:
- a cláusula lock sincroniza threads na mesma aplicação
- a classe Mutex permite sincronizar threads de diferentes aplicações.
Iremos utilizar o seguinte construtor e métodos:
cria um Mutex M | |
A thread T1 que executa o M.WaitOne() solicita a propriedade do objeto de sincronização M. Se o Mutex M não estiver detido por nenhuma thread (como estava no início), ele é «concedido» à thread T1 que o solicitou. Se, pouco depois, uma thread T2 realizar a mesma operação, será bloqueada. Isto porque um Mutex só pode pertencer a uma thread. Será desbloqueado quando a thread T1 libertar o Mutex M que detém. Várias threads podem, assim, ficar bloqueadas enquanto aguardam o Mutex M. | |
A thread T1 que executa o M.ReleaseMutex() renuncia à posse do Mutex M. Quando a thread T1 perde o processador, o sistema pode atribuí-lo a uma das threads que aguardam o Mutex M. Apenas uma o receberá por sua vez, enquanto as outras que aguardam M permanecerão bloqueadas |
Um Mutex M gere o acesso a um recurso partilhado R. Uma thread solicita o recurso R através de M.WaitOne() e liberta-o com M.ReleaseMutex(). Uma secção crítica de código que deve ser executada por apenas uma thread de cada vez é um recurso partilhado. A execução da secção crítica pode ser sincronizada da seguinte forma:
onde M é um objeto Mutex. Não se esqueça de libertar um Mutex que já não seja necessário, para que outra thread possa entrar na secção crítica; caso contrário, as threads que aguardam o Mutex que nunca foi libertado nunca terão acesso ao processador.
Se aplicarmos o que acabámos de ver ao exemplo anterior, a nossa aplicação fica da seguinte forma:
using System;
using System.Threading;
namespace Chap8 {
class Program3 {
// class variables
static int cptrThreads = 0; // thread counter
static Mutex synchro = new Mutex(); // synchronization object
//hand
public static void Main(string[] args) {
...
}
public static void Incrémente() {
....
synchro.WaitOne();
try {
...
} finally {
...
synchro.ReleaseMutex();
}
}
}
}
- linha 9: o objeto de sincronização de threads é agora um Mutex.
- linha 18: início da secção crítica — apenas um thread precisa de entrar. Bloqueamos até que a sincronização Mutex esteja livre.
- linha 33: como um Mutex deve ser sempre libertado, haja ou não uma exceção, gerimos a secção crítica com um try / finally para libertar o Mutex no finally.
- linha 23: o Mutex é libertado assim que a secção crítica for ultrapassada.
Os resultados são os mesmos de antes.
10.5.4. A classe AutoResetEvent
Um objeto AutoResetEvent é uma barreira que permite a passagem de apenas uma thread de cada vez, tal como as duas ferramentas anteriores, lock e Mutex. Criamos um AutoResetEvent da seguinte forma:
O valor booleano estado indica se a barreira está fechada (false) ou aberta (true). Uma thread que pretenda passar pela barreira indicá-lo-á da seguinte forma:
- se a barreira estiver aberta, a thread passa e a barreira é fechada atrás dela. Se várias threads estivessem à espera, podemos ter a certeza de que apenas uma passará.
- Se a barreira estiver fechada, a thread fica bloqueada. Outra thread irá abri-la no momento certo. Este momento depende inteiramente do problema em questão. A barreira será aberta pela operação:
Pode acontecer que um thread queira fechar uma barreira. Pode fazê-lo através de:
Se, no exemplo anterior, substituirmos o objeto Mutex por um objeto do tipo AutoResetEvent, o código passa a ser:
using System;
using System.Threading;
namespace Chap8 {
class Program4 {
// class variables
static int cptrThreads = 0; // thread counter
static EventWaitHandle synchro = new AutoResetEvent(false); // synchronization object
//hand
public static void Main(string[] args) {
....
// we open the critical section barrier
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();
// waiting for threads to finish
...
// counter display
Console.WriteLine("Nombre de threads générés : " + cptrThreads);
}
public static void Incrémente() {
// increases thread counter
// exclusive access to the meter is required
...
synchro.WaitOne();
try {
...
} finally {
// release the resource
...
synchro.Set();
}
}
}
}
- linha 9: a barreira é criada fechada. Será aberta pela linha 16 do Main.
- linha 27: o segmento responsável por incrementar o contador de segmentos solicita autorização para entrar na secção crítica. Os vários segmentos irão acumular-se em frente à barreira fechada. Quando o Main a abrir, um dos segmentos em espera irá passar.
- linha 33: quando termina o seu trabalho, reabre a barreira, permitindo que outra thread entre.
Os resultados são semelhantes aos anteriores.
10.5.5. A classe Interlocked
A classe Interlocked permite tornar atómico um grupo de operações. Dentro de um grupo de operações atómico, ou todas as operações são executadas pela thread que executa o grupo, ou nenhuma. Não se fica num estado em que algumas foram executadas e outras não. Os objetos de sincronização Lock, Mutex e AutoResetEvent foram todos concebidos para tornar atómico um grupo de operações. Isto é conseguido através do bloqueio de threads. A classe Interlocked permite evitar o bloqueio de threads para operações simples, mas frequentes. A classe Interlocked oferece os seguintes métodos estáticos:

O método Incrementally tem a seguinte assinatura:
Incrementa o aluguer. A operação é garantidamente atómica.
O nosso programa de contagem de threads pode então ser o seguinte:
using System;
using System.Threading;
namespace Chap8 {
class Program5 {
// class variables
static int cptrThreads = 0; // thread counter
//hand
public static void Main(string[] args) {
...
}
public static void Incrémente() {
// increments the thread counter
Interlocked.Increment(ref cptrThreads);
}
}
}
- linha 17: o contador de threads é incrementado atomicamente.
10.6. Acesso concorrente a múltiplos recursos partilhados
10.6.1. Um exemplo
Nos nossos exemplos anteriores, um único recurso era partilhado pelas diferentes threads. A situação pode tornar-se mais complicada se houver vários recursos e estes forem dependentes uns dos outros. Isto pode levar a uma situação de interbloqueio. Esta situação, também conhecida como impasse, ocorre quando duas threads esperam uma pela outra. Considere as seguintes ações que se sucedem no tempo:
- uma thread T1 obtém a posse de um Mutex M1 para aceder a um recurso partilhado R1
- uma thread T2 obtém a posse de um Mutex M2 para aceder a um recurso partilhado R2
- a thread T1 solicita o Mutex M2. Fica bloqueada.
- a thread T2 solicita o Mutex M1. Fica bloqueada.
Aqui, as threads T1 e T2 esperam uma pela outra. Este caso surge quando as threads necessitam de dois recursos partilhados: o recurso R1 controlado pelo Mutex M1 e o recurso R2 controlado pelo Mutex M2. Uma solução possível é solicitar ambos os recursos ao mesmo tempo, utilizando um único Mutex M. Mas isto nem sempre é possível, se, por exemplo, envolver uma mobilização demorada de um recurso dispendioso. Outra solução é que uma thread que tenha M1 e não consiga obter M2 libere M1 para evitar o interbloqueio.
- Temos uma matriz na qual algumas threads depositam dados (escritores) e outras os leem (leitores).
- Os escritores são iguais, mas exclusivos: apenas um escritor de cada vez pode inserir dados na tabela.
- Os leitores são iguais, mas exclusivos: apenas um leitor de cada vez pode ler os dados depositados na tabela.
- Um leitor só pode ler os dados na tabela depois de um escritor ter depositado dados nela, e um escritor só pode depositar novos dados na tabela depois de os dados nela terem sido lidos por um leitor.
Podem distinguir-se dois recursos partilhados:
- o quadro de escrita: apenas um escritor de cada vez pode ter acesso a ele.
- o quadro de visualização de leitura apenas: apenas um leitor de cada vez pode aceder-lhe.
e uma ordem de utilização destes recursos:
- um leitor deve sempre vir depois de um escritor.
- Um escritor deve sempre vir depois de um leitor, exceto na primeira vez.
O acesso a estes dois recursos pode ser controlado com duas barreiras do tipo AutoResetEvent:
- a barreira peutEcrire controlará o acesso dos escritores ao quadro.
- a barreira peutLire controlará o acesso dos leitores ao quadro.
- A barreira peutEcrire será criada inicialmente aberta, permitindo que um primeiro escritor passe e bloqueando todos os outros.
- A barreira peutLire será criada e inicialmente fechada, bloqueando todos os leitores.
- Quando um escritor termina o seu trabalho, abre o portão peutLire para deixar um leitor entrar.
- Quando um leitor termina o seu trabalho, abre o portão peutEcrire para deixar um escritor entrar.
O programa que ilustra esta sincronização orientada por eventos é o seguinte:
using System;
using System.Threading;
namespace Chap8 {
class Program {
// use of reader and writer threads
// illustrates the use of synchronization events
// class variables
static int[] data = new int[3 ]; // resource shared between reader and writer threads
static Random objRandom = new Random(DateTime.Now.Second ); // a random number generator
static AutoResetEvent peutLir e; // indicates that the contents of data can be read
static AutoResetEvent peutEcrir e; // indicates that you can write the contents of data
//hand
public static void Main(string[] args) {
// number of threads to generate
const int nbThreads = 2;
// flag initialization
peutLire = new AutoResetEvent(f als e); // cannot be read yet
peutEcrire = new AutoResetEvent( tru e); // we can already write
// creation of reader threads
Thread[] lecteurs = new Thread[nbThreads];
for (int i = 0; i < nbThreads; i++) {
// creation
lecteurs[i] = new Thread(Lire);
lecteurs[i].Name = "L" + i.ToString();
// launch
lecteurs[i].Start();
}
// creating writer threads
Thread[] écrivains = new Thread[nbThreads];
for (int i = 0; i < nbThreads; i++) {
// creation
écrivains[i] = new Thread(Ecrire);
écrivains[i].Name = "E" + i.ToString();
// launch
écrivains[i].Start();
}
//end of hand
Console.WriteLine("Fin de Main...");
}
// read the contents of the table
public static void Lire() {
...
}
// write in the table
public static void Ecrire() {
....
}
}
}
- linha 11: os dados da tabela são o recurso partilhado entre os threads de leitura e de escrita. São partilhados para leitura pelos threads de leitura e para escrita pelos threads de escrita.
- linha 13: o objeto peutLire é utilizado para indicar às threads de leitura que podem ler os dados da matriz. É definido como verdadeiro pela thread de escrita que preencheu os dados da tabela. É inicializado como falso, na linha 23. Uma thread de escrita deve primeiro preencher a matriz antes de definir o evento peutLire como verdadeiro.
- linha 14: o objeto peutEcrire é utilizado para avisar as threads de gravação de que podem escrever nos dados. É definido como true pela thread de leitura que utilizou todos os dados da matriz. É inicializado como true, na linha 24. Os dados da tabela estão livres para gravação.
- linhas 27-34: criar e iniciar threads de leitura
- linhas 37-44: criar e iniciar threads de gravação
O método Read executado pelas threads de leitura é o seguinte:
public static void Lire() {
// follow-up
Console.WriteLine("Méthode [Lire] démarrée par le thread n° {0}", Thread.CurrentThread.Name);
// we have to wait for reading authorization
peutLire.WaitOne();
// table reading
for (int i = 0; i < data.Length; i++) {
//wait 1 s
Thread.Sleep(1000);
// display
Console.WriteLine("{0:hh:mm:ss} : Le lecteur {1} a lu le nombre {2}", DateTime.Now, Thread.CurrentThread.Name, data[i]);
}
// we can write
peutEcrire.Set();
// follow-up
Console.WriteLine("Méthode [Lire] terminée par le thread n° {0}", Thread.CurrentThread.Name);
}
- linha 5: aguardamos que um thread de gravação sinalize que a matriz foi preenchida. Quando este sinal é recebido, apenas um dos threads de leitura que aguardam este sinal pode passar.
- linhas 7-12: dados da operação da tabela com um Sleep no meio para forçar o thread a perder o processador.
- linha 14: informa aos threads de gravação que a matriz foi lida e pode ser preenchida novamente.
O método Write executado pelos threads de gravação é o seguinte:
public static void Ecrire() {
// follow-up
Console.WriteLine("Méthode [Ecrire] démarrée par le thread n° {0}", Thread.CurrentThread.Name);
// we have to wait for write authorization
peutEcrire.WaitOne();
// writing table
for (int i = 0; i < data.Length; i++) {
//wait 1 s
Thread.Sleep(1000);
// display
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]);
}
// on peut lire
peutLire.Set();
// follow-up
Console.WriteLine("Méthode [Ecrire] terminée par le thread n° {0}", Thread.CurrentThread.Name);
}
- linha 5: aguardamos que um thread de leitura sinalize que a matriz foi lida. Quando este sinal é recebido, apenas um dos threads de escrita que aguardam este sinal pode passar.
- linhas 7-13: dados da operação da tabela com um Sleep no meio para forçar o thread a perder o processador.
- linha 15: informa aos threads de leitura que a matriz foi preenchida e pode ser lida novamente.
A execução dá os seguintes resultados:
Os seguintes pontos merecem destaque:
- existe apenas 1 unidade de cada vez, embora perca o processador na secção crítica Read
- existe apenas 1 gravador de cada vez, embora ele perca o processador na secção de revisão Write
- um leitor só lê quando há algo para ler na tabela
- um gravador não grava até que a imagem tenha sido totalmente lida
10.6.2. A classe Monitor
No exemplo anterior:
- há dois recursos partilhados para gerir
- para um determinado recurso, os threads são iguais.
Quando as threads de gravação estão bloqueadas no peutEcrire.WaitOne, uma delas, qualquer uma delas, é desbloqueada pela operação peutEcrire.Set. Se a operação anterior envolver a abertura da porta para um gravador específico, as coisas tornam-se mais complicadas.
A analogia é com um estabelecimento que atende o público em balcões, onde cada balcão é especializado. Quando os clientes chegam, pegam um bilhete na máquina de bilhetes para o balcão X e, em seguida, sentam-se. Cada bilhete é numerado, e os clientes são chamados pelo seu número através de um altifalante. Enquanto esperam, os clientes podem fazer o que quiserem. Podem ler ou cochilar. De cada vez, é acordado pelo altifalante a anunciar que o número Y foi chamado para o balcão X. Se for ele, o cliente levanta-se e dirige-se ao balcão X; caso contrário, continua o que estava a fazer.
Podemos fazer o mesmo aqui. Tomemos os escritores, por exemplo:
as suas filas estão bloqueadas | |
o segmento que estava a ler a matriz informa aos escritores que a matriz está disponível. Esse segmento ou outro define o segmento do escritor para passar a barreira. | |
cada thread verifica se é o escolhido. Se for, passa pela barreira. Se não for, volta para a espera. |
A classe Monitor é utilizada para implementar este cenário.

Descrevemos agora uma construção padrão (padrão), proposta no capítulo Threading do livro C# 3.0 referido na introdução deste documento, capaz de resolver problemas de barreira com condições de entrada.
- Em primeiro lugar, os threads que partilham um recurso (o contador, etc.) acedem a ele através de um objeto a que chamaremos token. Para abrir a porta que dá acesso ao contador, é necessário ter o token, e existe apenas um token. Os threads devem, portanto, passar o token entre si.
- Para chegar ao contador, as threads primeiro solicitam o :
Se o token estiver livre, é atribuído à thread que executou a operação anterior; caso contrário, a thread é colocada em espera pelo token.
- Se o acesso ao contador for desordenado, ou seja, se a ordem de entrada não for relevante, a operação anterior é suficiente. A thread com o token acede ao contador. Se o acesso for ordenado, a thread com o token verifica se cumpre a condição para aceder ao contador:
Se a thread não for a esperada no balcão, ela cede a sua vez devolvendo o token. Entra num estado bloqueado. Será ativada assim que o token estiver disponível novamente. Verificará então novamente se cumpre a condição para ir ao balcão. A operação Monitor.Wait(token) que liberta o token só pode ser realizada se a thread for a proprietária do token. Caso contrário, é lançada uma exceção.
- A thread que verifica a condição para ir ao contador vai para lá:
- // trabalho no contador
- ....
Antes de sair do contador, o segmento de execução deve devolver o seu token; caso contrário, os segmentos de execução bloqueados à espera dele permanecerão bloqueados indefinidamente. Existem duas situações diferentes:
- a primeira situação é aquela em que a thread que detém o token é também a responsável por sinalizar às threads que aguardam o token que este está livre. Fá-lo da seguinte forma:
Na linha 6, acorda as threads que aguardam o token. Isto significa que elas se tornam elegíveis para receber o token. Não significa que o recebam imediatamente. Na linha 8, o token é libertado. Todas as threads elegíveis receberão o token por sua vez, de forma indeterminada. Isto dar-lhes-á a oportunidade de verificar novamente se cumprem a condição de acesso. A thread que libertou o token modificou esta condição na linha 4 para permitir a entrada de uma nova thread. A primeira thread a verificar esta condição mantém o token e passa para o contador por sua vez.
- A segunda situação é aquela em que a thread detentora do token não é a responsável por sinalizar às threads que aguardam o token que este está livre. No entanto, ela deve libertá-lo, porque a thread responsável por enviar este sinal deve ser a detentora do token. Fá-lo-á utilizando a operação:
O token está agora disponível, mas as threads que o aguardam (que executaram uma operação Wait(token)) não são notificadas. Esta tarefa é confiada a outra thread, que, em algum momento, executará código semelhante ao seguinte:
No final, a construção padrão proposta no capítulo Threading do livro C# 3.0 é a seguinte:
- define counter access token :
- solicitar acesso ao contador:
lock(jeton){
while (! jeNeSuisPasCeluiQuiEstAttendu)
Monitor.Wait(jeton);
}
// passage au guichet
...
é equivalente a
Note que, neste esquema, o token é libertado imediatamente, assim que a barreira é ultrapassada. Outra thread pode então verificar a condição de acesso. A construção anterior permite, portanto, que todas as threads verifiquem a condição de acesso. Se não for isso que pretende, pode escrever:
lock(jeton){
while (! jeNeSuisPasCeluiQuiEstAttendu)
Monitor.Wait(jeton);
// passage au guichet
...
}
onde o token só é libertado após passar pelo balcão.
- modificar as condições de acesso ao contador e notificar outras threads
lock(jeton){
// modifier la condition d'accès au guichet
...
// en avertir les threads en attente du jeton
Monitor.PulseAll(jeton);
}
Acima, a condição de acesso só pode ser modificada pela thread que detém o token. Também pode escrever:
// modifier la condition d'accès au guichet
...
// en avertir les threads en attente du jeton
Monitor.PulseAll(jeton);
// libérer le jeton
Monitor.Exit(jeton);
se o thread já tiver o token.
Com esta informação, podemos reescrever a aplicação de leitores/escritores, definindo uma ordem para que os leitores e escritores acedam aos seus respetivos contadores. O código é o seguinte:
using System;
using System.Threading;
namespace Chap8 {
class Program2 {
// use of reader and writer threads
// illustrates the use of synchronization events
// class variables
static int[] data = new int[3 ]; // resource shared between reader and writer threads
static Random objRandom = new Random(DateTime.Now.Second ); // a random number generator
static object peutLire = new object( ); // indicates that the contents of data can be read
static object peutEcrire = new object( ); // indicates that you can write the contents of data
static bool lectureAutorisée = fals e; // to authorize the reading of the table
static bool écritureAutorisée = fals e; // to authorize writing in the table
static string[] ordreLectur e; // sets the order of readers
static string[] ordreEcritur e; // sets the order for writers
static int lecteurSuivant = 0; // indicates the next drive number
static int écrivainSuivant = 0; // indicates the number of the following writer
//hand
public static void Main(string[] args) {
// number of threads to generate
const int nbThreads = 5;
// creation of reader threads
Thread[] lecteurs = new Thread[nbThreads];
for (int i = 0; i < nbThreads; i++) {
// creation
lecteurs[i] = new Thread(Lire);
lecteurs[i].Name = "L" + i.ToString();
// launch
lecteurs[i].Start();
}
// create playback order
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);
}
// creating writer threads
Thread[] écrivains = new Thread[nbThreads];
for (int i = 0; i < nbThreads; i++) {
// creation
écrivains[i] = new Thread(Ecrire);
écrivains[i].Name = "E" + i.ToString();
// launch
écrivains[i].Start();
}
// creation of writing order
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);
}
// write authorization
lock (peutEcrire) {
écritureAutorisée = true;
Monitor.Pulse(peutEcrire);
}
//end of hand
Console.WriteLine("Fin de Main...");
}
// read the contents of the table
public static void Lire() {
...
}
// write in the table
public static void Ecrire() {
...
}
}
}
O acesso à mesa de leitura está sujeito às seguintes condições:
- linha 13: o token peutLire
- linha 15: o valor booleano readingAuthorized
- linha 17: a tabela ordenada de leitores. Os leitores dirigem-se ao balcão de leitura pela ordem desta tabela, que contém os seus nomes.
- linha 19: lecteurSuivant indica o número do próximo leitor autorizado a dirigir-se ao balcão.
O acesso ao balcão de atendimento está sujeito às seguintes condições:
- linha 14: o token peutEcrire
- linha 16: o valor booleano writingAuthorized
- linha 18: a tabela ordenada de escritores. Os escritores dirigem-se à secretária pela ordem desta tabela que contém os seus nomes.
- linha 20: writerNext indica o número do próximo escritor autorizado a avançar para o contador.
Os outros elementos do código são os seguintes:
- linhas 29-36: criam e iniciam threads de leitura. Todas elas ficarão bloqueadas porque a leitura não está autorizada (linha 15).
- linhas 39-43: a sua ordem de passagem pelo contador será na ordem inversa à da sua criação.
- linhas 46-53: criam e iniciam threads de gravação. Todas elas serão bloqueadas porque a gravação não é permitida (linha 16).
- linhas 56-60: a sua ordem de passagem pelo contador será na ordem da sua criação.
- linha 64: a escrita é autorizada
- linha 65: os escritores são avisados de que algo mudou.
O método Read é o seguinte:
public static void Lire() {
// follow-up
Console.WriteLine("Méthode [Lire] démarrée par le thread n° {0}", Thread.CurrentThread.Name);
// we have to wait for reading authorization
lock (peutLire) {
while (!lectureAutorisée || ordreLecture[lecteurSuivant] != Thread.CurrentThread.Name) {
Monitor.Wait(peutLire);
}
// table reading
for (int i = 0; i < data.Length; i++) {
//wait 1 s
Thread.Sleep(1000);
// display
Console.WriteLine("{0:hh:mm:ss} : Le lecteur {1} a lu le nombre {2}", DateTime.Now, Thread.CurrentThread.Name, data[i]);
}
// next reader
lectureAutorisée = false;
lecteurSuivant++;
// writers are warned that they can write
lock (peutEcrire) {
écritureAutorisée = true;
Monitor.PulseAll(peutEcrire);
}
// follow-up
Console.WriteLine("Méthode [Lire] terminée par le thread n° {0}", Thread.CurrentThread.Name);
}
}
- todo o acesso ao balcão é controlado pelo bloqueio nas linhas 5-27. O leitor que obtém o token mantém-no durante toda a sua visita ao balcão
- linhas 6-8: um leitor que tenha adquirido o token na linha 5 liberta-o se a leitura não for autorizada ou se não for a sua vez de passar.
- linhas 10-15: passagem pelo balcão (operação da tabela)
- linhas 17-18: o segmento altera as condições de acesso ao balcão de leitura. Note-se que ainda possui o token de leitura e que estas modificações ainda não permitem que um leitor passe.
- linhas 20-23: o thread altera as condições de acesso à secretária de escrita e avisa todos os escritores em espera de que algo mudou.
- linha 27: o bloqueio termina, o token peutLire é libertado. Uma thread de leitura poderia então adquiri-lo na linha 5, mas não passaria na condição de acesso, uma vez que o booleano readingAuthorized é falso. Além disso, todas as threads à espera do peutLire permanecem assim, uma vez que o PulseAll(peutLire) ainda não ocorreu.
O método Write é o seguinte:
public static void Ecrire() {
// follow-up
Console.WriteLine("Méthode [Ecrire] démarrée par le thread n° {0}", Thread.CurrentThread.Name);
// we have to wait for write authorization
lock (peutEcrire) {
while (!écritureAutorisée || ordreEcriture[écrivainSuivant] != Thread.CurrentThread.Name) {
Monitor.Wait(peutEcrire);
}
// writing table
for (int i = 0; i < data.Length; i++) {
//wait 1 s
Thread.Sleep(1000);
// display
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]);
}
// next writer
écritureAutorisée = false;
écrivainSuivant++;
// readers waiting for the peutLire token are woken up
lock (peutLire) {
lectureAutorisée = true;
Monitor.PulseAll(peutLire);
}
// follow-up
Console.WriteLine("Méthode [Ecrire] terminée par le thread n° {0}", Thread.CurrentThread.Name);
}
}
- todo o acesso à secretária de escrita é controlado pelo bloqueio nas linhas 5-27. O escritor que recolhe o token mantém-no durante todo o tempo em que estiver no balcão
- linhas 6-8: um escritor que tenha adquirido o token na linha 5 liberta-o se a escrita não for autorizada ou se não for a sua vez de passar.
- linhas 10-16: passagem pelo balcão (operação da tabela)
- linhas 18-19: o segmento altera as condições de acesso à secretária de escrita. Note-se que ainda possui o token de escrita e que estas modificações ainda não permitem que um escritor passe.
- linhas 21-24: o thread altera as condições de acesso à secretária de leitura e avisa todos os leitores em espera de que algo mudou.
- linha 27: o bloqueio termina, o token peutEcrire é libertado. Uma thread de escrita poderia então adquiri-lo na linha 5, mas não passaria na condição de acesso, uma vez que o booleano writingAuthorized é falso. Além disso, todas as threads à espera do peutEcrire permanecem assim, pendentes de uma nova operação PulseAll(peutEcrire).
Um exemplo de execução é o seguinte:
10.7. Conjuntos de threads
Até agora, para gerir:
- criávamos-os com Thread T=new Thread(...)
- e, em seguida, executávamos com T.Start()
Vimos no capítulo «Bases de dados» que, com alguns SGBD, era possível ter conjuntos de ligações abertas:
- n ligações são abertas no arranque do conjunto
- quando um thread solicita uma conexão, recebe uma das conexões abertas no pool
- quando a thread fecha a conexão, esta não é encerrada, mas devolvida ao conjunto
A utilização de um conjunto de conexões é transparente para o código. A vantagem reside no melhor desempenho: abrir uma conexão é dispendioso. Aqui, 10 conexões abertas podem atender centenas de solicitações.
Existe um sistema semelhante para threads:
- min threads são criadas no arranque do pool. O valor de min é definido utilizando o ThreadPool.SetMinThreads(min1,min2). Um conjunto de threads pode ser utilizado para executar tarefas assíncronas de bloqueio ou sem bloqueio. O primeiro parâmetro, min1, define o número de threads de bloqueio; o segundo, min2, define o número de threads assíncronas. Os valores atuais destas duas variáveis podem ser obtidos através de ThreadPool.GetMinThreads(out min1,out min2).
- Se este número não for suficiente, o pool criará outras threads para responder aos pedidos até ao limite de max threads. O valor de max é definido utilizando o ThreadPool.SetMaxThreads(max1,max2). Ambos os parâmetros têm o mesmo significado que no SetMinThreads. Os valores atuais destes dois parâmetros podem ser obtidos através de ThreadPool.GetMaxThreads(out max1,out max2). Quando as max1 threads forem atingidas, os pedidos de threads para tarefas de bloqueio serão colocados em fila para uma thread livre no pool.
Um pool de threads oferece várias vantagens:
- tal como acontece com o pool de ligações, poupamos tempo na criação de threads: 10 threads podem atender centenas de pedidos.
- protegemos a aplicação: ao definir um número máximo de threads, evitamos sobrecarregar a aplicação com demasiadas solicitações. Estas serão colocadas numa fila de ficheiros.
Para atribuir uma tarefa a um thread no pool, utilize 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 a um thread que execute o método WaitCallBack sem passar um parâmetro. O método 2 faz o mesmo, mas passa um parâmetro do tipo object para o WaitCallBack.
O programa seguinte ilustra estes conceitos:
using System;
using System.Threading;
namespace Chap8 {
class Program {
public static void Main() {
// init Current thread
Thread main = Thread.CurrentThread;
// name the Thread
main.Name = "Main";
// we use a thread pool
int min1, min2;
// set the minimum number of blocking threads
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);
// set the maximum number of blocking threads
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);
// 7 threads are executed
for (int i = 0; i < 7; i++) {
// start execution of thread i in a pool
ThreadPool.QueueUserWorkItem(Sleep, new Data2 { Numéro = i.ToString(), Début = DateTime.Now, Durée = i + 10 });
}
// end of hand
Console.Write("Tapez [entrée] pour terminer le thread {0} à {1:hh:mm:ss:FF}", main.Name, DateTime.Now);
// waiting
Console.ReadLine();
}
public static void Sleep(object infos) {
// parameter is retrieved
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);
// pool status
int cpt1, cpt2;
ThreadPool.GetAvailableThreads(out cpt1, out cpt2);
Console.WriteLine("Nombre de threads pour tâches bloquantes disponibles dans le pool : {0}", cpt1);
// sleep mode for Duration
Thread.Sleep(data.Durée * 1000);
// end of execution
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 {
// miscellaneous information
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: o número mínimo atual de threads no pool de threads é solicitado e exibido
- linha 18: altera o número mínimo de threads para tarefas de bloqueio para 2
- linhas 19-21: os novos mínimos são exibidos
- linhas 22-28: faça o mesmo para definir o número máximo de threads para tarefas de bloqueio: 5
- linhas 30-33: são executadas 7 tarefas num conjunto de 5 threads. 5 tarefas devem obter 1 thread, as primeiras 2 rapidamente, uma vez que 2 threads estão sempre presentes, as outras 3 com um tempo de espera de 0,5 segundos. 2 tarefas devem esperar que um thread fique disponível.
- linha 32: as tarefas executam o Sleep nas 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 o parâmetro passado para o Sleep.
- linha 43: a tarefa identifica-se na consola
- linhas 45-47: exibem o número de threads atualmente disponíveis. Queremos ver como isso evolui.
- linha 49: a tarefa pára por alguns segundos (tarefa de bloqueio).
- linha 52: quando ela acorda, exibimos algumas informações sobre a sua conta.
Os resultados são os seguintes.
Para os números min e max de threads no pool:
Para executar as 7 threads:
- linhas 1-6: as primeiras 3 tarefas são executadas sucessivamente. Encontram imediatamente 1 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. Tivemos de criar um. Este mecanismo é possível até 5 (MaxThreads=5).
- linha 10: não há mais threads disponíveis: as tarefas 5 e 6 terão de esperar.
- linhas 11-12: a tarefa 0 termina. A tarefa 5 ocupa a sua thread.
- linhas 13-14: a tarefa 1 termina. A tarefa 6 ocupa a sua 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 namespace [System.ComponentModel]. É utilizada da mesma forma que um thread, mas possui algumas características especiais que podem torná-la mais interessante do que a classe [Thread] em certos casos:
- ela emite os seguintes eventos:
- DoWork: uma thread solicitou a execução do BackgroundWorker
- ProgressChanged: o objeto BackgroundWorker executou o ReportProgress. Isto é utilizado para indicar uma percentagem de conclusão.
- RunWorkerCompleted: o objeto BackgroundWorker concluiu o seu trabalho. Pode tê-lo concluído normalmente, ou com um cancelamento ou exceção.
Estes eventos tornam o BackgroundWorker útil em interfaces gráficas: uma tarefa demorada será confiada a um BackgroundWorker, que poderá informar sobre o seu progresso com o ProgressChanged e a sua conclusão com o evento RunWorkerCompleted. O trabalho a ser realizado pelo BackgroundWorker será executado por um método associado ao DoWork.
- É possível solicitar o seu cancelamento. Numa interface gráfica, uma tarefa demorada pode ser cancelada pelo utilizador.
- Os objetos BackgroundWorker pertencem a um pool e são reciclados conforme necessário. Uma aplicação que necessite de um BackgroundWorker irá obtê-lo do pool, que lhe fornecerá um thread existente mas não utilizado. Reciclar threads desta forma, em vez de criar um novo thread de cada vez, melhora o desempenho.
Utilizamos esta ferramenta na aplicação anterior quando o acesso ao contador não é controlado:
using System;
using System.Threading;
using System.ComponentModel;
namespace Chap8 {
class Program2 {
// use of reader and writer threads
// illustrates the simultaneous use of shared resources and synchronization
// class variables
const int nbThreads = 2; // total number of threads
static int nbLecteursTerminés = 0; // number of terminated threads
static int[] data = new int[5]; // shared array between reader and writer threads
static object appli; // synchronizes access to number of completed threads
static Random objRandom = new Random(DateTime.Now.Second); // a random number generator
static AutoResetEvent peutLire; // indicates that the contents of the table can be read
static AutoResetEvent peutEcrire; // points out that we can write in the table
static AutoResetEvent finLecteurs; // signals the end of readers
//hand
public static void Main(string[] args) {
// give the thread a name
Thread.CurrentThread.Name = "Main";
// flag initialization
peutLire = new AutoResetEvent(fals e); // cannot be read yet
peutEcrire = new AutoResetEvent(tru e); // we can already write
finLecteurs = new AutoResetEvent(false); // application not completed
// synchronizes access to terminated thread counter
appli = new object();
// creation of reader threads
MyBackgroundWorker[] lecteurs = new MyBackgroundWorker[nbThreads];
for (int i = 0; i < nbThreads; i++) {
// creation
lecteurs[i] = new MyBackgroundWorker();
lecteurs[i].Numéro = "L" + i;
lecteurs[i].DoWork += Lire;
lecteurs[i].RunWorkerCompleted += EndLecteur;
// launch
lecteurs[i].RunWorkerAsync();
}
// creating writer threads
MyBackgroundWorker[] écrivains = new MyBackgroundWorker[nbThreads];
for (int i = 0; i < nbThreads; i++) {
// creation
écrivains[i] = new MyBackgroundWorker();
écrivains[i].Numéro = "E" + i;
écrivains[i].DoWork += Ecrire;
// launch
écrivains[i].RunWorkerAsync();
}
// wait for all threads to finish
finLecteurs.WaitOne();
//end of hand
Console.WriteLine("Fin de Main...");
}
public static void EndLecteur(object sender, RunWorkerCompletedEventArgs infos) {
...
}
// read the contents of the table
public static void Lire(object sender, DoWorkEventArgs infos) {
...
}
// write in the table
public static void Ecrire(object sender, DoWorkEventArgs infos) {
...
}
}
// thread
internal class MyBackgroundWorker : BackgroundWorker {
// miscellaneous information
public string Numéro { get; set; }
}
}
Apresentamos apenas as alterações:
- a classe Thread é substituída pela MyBackgroundWorker nas linhas 79-82. O método da classe BackgroundWorker foi derivado para atribuir um número à thread. Poderíamos ter feito as coisas de forma diferente, passando um objeto para o RunWorkerAsync nas linhas 43 e 54, objeto que contém o número da thread.
- linha 58: o método Main termina depois de todas as threads de leitura terem concluído o seu trabalho. Para tal, na linha 12, o contador nbReadersTerminated conta o número de threads de leitura que concluíram o seu trabalho. Este contador é incrementado pelo EndLecteur nas linhas 63-65, que é executado sempre que uma thread de leitura termina. É este procedimento que controla o AutoResetEvent finLecteurs na linha 18, que é sincronizado na linha 59 com o Hand.
- linha 16: uma vez que várias threads de leitura podem querer incrementar o contador nbReadersTerminated ao mesmo tempo, o acesso exclusivo ao mesmo é garantido pelo objeto de sincronização app. Este caso é improvável, mas teoricamente possível.
- linhas 35-44: criação de threads de leitura
- linha 38: criação do tipo de thread MyBackgroundWorker
- linha 39: é-lhe atribuído um No
- linha 40: é atribuída a função de leitura
- linha 41: o método EndLecteur será executado após o término da thread
- linha 43: o thread é iniciado
- linhas 47-55: criação de threads de gravação
- linha 50: criação do tipo de thread MyBackgroundWorker
- linha 51: é-lhe atribuído um No
- linha 52: é-lhe atribuída a operação Write a executar
- linha 54: o thread é iniciado
Os métodos Read e Write permanecem inalterados. O método EndLecteur é executado no final de cada thread de leitura. O seu código é o seguinte:
public static void EndLecteur(object sender, RunWorkerCompletedEventArgs infos) {
// increment no. of completed drives
lock (appli) {
nbLecteursTerminés++;
if (nbLecteursTerminés == nbThreads)
finLecteurs.Set();
}
}
A função do método EndLecteur é notificar o Main de que todos os leitores concluíram o seu trabalho.
- linha 4: o contador nbReadersTerminated é incrementado.
- linhas 5-6: se todos os leitores tiverem concluído o seu trabalho, o evento finLecteurs é definido como verdadeiro para evitar que o Main aguarde este evento.
- Como o EndLecteur é executado por várias threads, a secção crítica anterior é protegida pelo bloqueio na linha 3.
A execução produz resultados semelhantes aos da versão com threads.
10.8.2. Exemplo 2
O código a seguir ilustra outros pontos da classe BackgroundWorker :
- a capacidade de cancelar a tarefa
- uma exceção lançada na tarefa é reportada
- passar 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() {
// init Current thread
Thread main = Thread.CurrentThread;
// name the Thread
main.Name = "Main";
// thread creation
for (int i = 0; i < tâches.Length; i++) {
// create thread n° i
tâches[i] = new BackgroundWorker();
// initialize it
tâches[i].DoWork += Sleep;
tâches[i].RunWorkerCompleted += End;
tâches[i].WorkerSupportsCancellation = true;
// launch it
tâches[i].RunWorkerAsync(new Data { Numéro = i, Début = DateTime.Now, Durée = i + 1 });
}
// cancel the last thread
tâches[4].CancelAsync();
// end of hand
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 {
// miscellaneous information
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: o BackgroundWorker
- linhas 18-27: criação da thread
- linha 20: criação da thread
- linha 22: a thread irá executar o Sleep linhas 39-41
- linha 23: o método End nas linhas 43-45 será executado no final da thread
- linha 24: a thread pode ser cancelada
- linha 26: a thread é iniciada com um parâmetro do tipo [Data], definido nas linhas 49-52. Este objeto possui os seguintes campos:
- Número (entrada): número da thread
- Start (entrada): hora de início da thread
- Duração (entrada): tempo de execução do Sleep
- Fim (saída): fim da execução da thread
- linha 29: a thread n.º 4 é cancelada
Todas as threads executam o Sleep a seguir:
public static void Sleep(object sender, DoWorkEventArgs infos) {
// we use the info parameter
Data data = (Data)infos.Argument;
// exception for task no. 3
if (data.Numéro == 3) {
throw new Exception("test....");
}
// sleep mode for Duration, stopping every second
for (int i = 1; i <= data.Durée && !tâches[data.Numéro].CancellationPending; i++) {
// wait 1 second
Thread.Sleep(1000);
}
// end of execution
data.Fin = DateTime.Now;
// initialize the result
infos.Result = data;
infos.Cancel = tâches[data.Numéro].CancellationPending;
}
- linha 1: o método Sleep tem a assinatura padrão de um manipulador de eventos. Recebe dois parâmetros:
- sender: o remetente do evento, neste caso o BackgroundWorker que executa o
- news: do tipo DoWorkEventArgs, que fornece informações sobre o evento DoWork. Este parâmetro é utilizado tanto para transmitir informações à thread como para recuperar os seus resultados.
- linha 3: o parâmetro passado para o RunWorkerAsync da tarefa encontra-se em infos.Argument.
- linhas 5-7: é lançada uma exceção para a tarefa n.º 3
- linhas 9-12: a thread "dorme" durante Duration segundos, em incrementos de um segundo, para permitir o teste de cancelamento na linha 9. Isto simula uma tarefa de longa duração durante a qual a thread verificaria regularmente se existe um pedido de cancelamento. Para indicar que foi cancelada, a thread deve definir a propriedade infos.Cancel como true (linha 17).
- linha 16: o thread pode devolver um resultado ao thread que o iniciou. Coloca este resultado em infos.Result.
Uma vez concluídas, as threads executam o End next :
public static void End(object sender, RunWorkerCompletedEventArgs infos) {
// the infos parameter is used to display the result of execution
// exception?
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 tem a assinatura padrão de um manipulador de eventos. Recebe dois parâmetros:
- sender : o remetente do evento, neste caso o BackgroundWorker que executa o
- news : do tipo RunWorkerCompletedEventArgs, que fornece informações sobre o evento RunWorkerCompleted.
- linha 4: o campo infos.Error do tipo Exception é preenchido apenas 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 tiver havido nenhuma exceção ou cancelamento, então infos.Result é o resultado da thread executada. Utilizar este resultado se a thread tiver sido cancelada ou tiver lançado uma exceção provoca uma exceção. Assim, nas linhas 5 e 13, não conseguimos exibir o número da thread que foi cancelada ou que lançou uma exceção, uma vez que este número se encontra em infos.Result. Este problema pode ser contornado derivando a classe BackgroundWorker para armazenar a informação a ser trocada entre a thread chamadora e a thread chamada, tal como no exemplo anterior. Utilizamos então o argumento sender, que representa o BackgroundWorker, em vez de news.
Os resultados são os seguintes:
10.9. Dados locais do thread
10.9.1. O princípio
Considere uma aplicação de três camadas:
![]() |
Vamos supor que a aplicação é multiutilizador, uma aplicação web, por exemplo. Cada utilizador é atendido por um thread dedicado. O ciclo de vida do thread é o seguinte:
- o thread é criado ou solicitado a partir de um pool de threads para satisfazer um pedido do utilizador
- se esta 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 devolve a resposta ao utilizador. Em seguida, desaparece ou é reciclado para um pool de threads.
Na operação 2, pode ser interessante que a thread tenha os seus próprios dados, ou seja, que não sejam partilhados com outras threads. Estes dados poderiam, por exemplo, pertencer ao utilizador específico a quem a thread está a prestar serviço. Estes dados poderiam então ser utilizados nas várias camadas [ui, metier, dao].
A classe Thread permite este cenário graças a uma espécie de dicionário privado em que as chaves seriam do tipo LocalDataStoreSlot:
cria uma entrada no dicionário privado da thread para o nome da chave. | |
associa o valor data à chave name a partir do dicionário privado da thread | |
recupera o valor associado ao nome do dicionário privado da thread |
Um modelo de utilização poderia ser o seguinte:
- para criar um par (chave, valor) associado à thread atual:
- para recuperar o valor associado à chave:
10.9.2. Aplicação do princípio
Considere a seguinte aplicação de três camadas:
![]() |
Vamos supor que a camada [dao] gere uma base de dados de artigos e que a 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 item na base de dados
- linha 6: para recuperar todos os artigos da base de dados
- linha 7: para eliminar todos os artigos da base de dados
Mais tarde, precisamos de um método para inserir uma matriz de artigos utilizando uma transação, porque queremos operar num modo «tudo ou nada»: ou todos os artigos são inseridos, ou nenhum. Podemos então modificar a interface para integrar este novo requisito:
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 matriz de artigos à base de dados
Mais tarde, para outra aplicação, surge a necessidade de eliminar uma lista de artigos guardados numa lista, ainda dentro de uma transação. Como podemos ver, a camada [dao] irá expandir-se para responder a diferentes necessidades de negócio. Podemos seguir outro caminho:
- colocar apenas operações básicas na camada [dao] InsertArticle, DeleteArticle, UpdateArticle, SelectArticle, SelectArticles
- transferir para a camada [business] a atualização simultânea de vários artigos. Estas utilizariam as operações elementares da camada [dao].
A vantagem desta solução é que a mesma camada [dao] pode ser utilizada sem alterações com diferentes camadas [metier]. No entanto, introduz uma dificuldade na gestão da transação, que agrupa as atualizações a serem feitas de forma atómica no:
- a transação deve ser iniciada pela camada [metier] antes de esta chamar os métodos da camada [dao]
- os métodos na camada [dao] devem estar cientes da existência da transação para poderem participar nela, caso exista
- a transação deve ser encerrada pela camada [business].
Para garantir que os métodos da camada [dao] estejam cientes da existência de qualquer transação atual, poderíamos adicionar a transação como um parâmetro a cada método da camada [dao]. Este parâmetro aparecerá então na assinatura dos métodos da interface, ligando-a a uma fonte de dados específica: a base de dados. Os dados locais da thread oferecem-nos uma solução mais elegante: a camada [business] colocará a transação nos dados locais da thread, e a camada [dao] irá buscá-la a partir daí. A assinatura do método da camada [dao] não precisa de ser alterada.
Estamos a implementar esta solução com o seguinte projeto do Visual Studio:
![]() |
![]() |
- em [1]: a solução como um todo
- em [2]: as referências utilizadas. Como [4] é uma base de dados SQL Server Compact, é necessária a referência [System.Data.SqlServerCe].
- em [3]: as diferentes camadas da aplicação.
A base [4] é a base de dados SQL Server Compact já utilizada no capítulo anterior, em particular no parágrafo 9.3.1.
![]() |
A classe Article
Uma linha da tabela anterior [articles] é encapsulada num objeto do tipo Article :
namespace Chap8 {
public class Article {
// properties
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; }
// manufacturers
public Article() {
}
public Article(int id, string nom, decimal prix, int stockActuel, int stockMinimum) {
Id = id;
Nom = nom;
Prix = prix;
StockActuel = stockActuel;
StockMinimum = stockMinimum;
}
// identity
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 colocar todas as linhas da tabela [articles] numa lista de objetos Article
- linha 7: para eliminar 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 artigos
- linha 6: o mesmo, mas sem transação
- linha 7: para obter uma lista de todos os artigos
- linha 8: para eliminar todos os artigos
Implementação da camada [metier]
A implementação 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 {
// layer [dao]
public IDao Dao { get; set; }
// connecting chain
public string ConnectionString { get; set; }
// insert an array of articles inside a transaction
public void InsertArticlesInTransaction(Article[] articles) {
// create the connection to the
using (SqlCeConnection connexion = new SqlCeConnection(ConnectionString)) {
// opening connection
connexion.Open();
// transaction
SqlCeTransaction transaction = null;
try {
// start of transaction
transaction = connexion.BeginTransaction(IsolationLevel.ReadCommitted);
// register the transaction in the thread
Thread.SetData(Thread.GetNamedDataSlot("transaction"), transaction);
// articles insertion
foreach (Article article in articles) {
Dao.InsertArticle(article);
}
// validate the transaction
transaction.Commit();
} catch {
// we undo the transaction
if (transaction != null)
transaction.Rollback();
}
}
}
// insertion of an array of articles without transaction
public void InsertArticlesOutOfTransaction(Article[] articles) {
// articles insertion
foreach (Article article in articles) {
Dao.InsertArticle(article);
}
}
// articles list
public List<Article> GetAllArticles() {
return Dao.GetAllArticles();
}
// delete all articles
public void DeleteAllArticles() {
Dao.DeleteAllArticles();
}
}
}
A classe tem as seguintes propriedades:
- linha 9: uma referência à camada [dao]
- linha 11: a cadeia de ligação utilizada para se ligar à base de dados de artigos
Comentamos apenas o método InsertArticlesInTransaction, que por si só apresenta dificuldades:
- linha 16: é criada uma ligação à base de dados
- linha 18: agora aberta
- linha 23: é criada uma transação
- linha 25: guardada nos dados locais do thread, associada à chave «transaction»
- linhas 27-29: o método de inserção da unidade da camada [dao] é chamado para cada item a ser inserido
- linhas 21 e 32: toda a inserção da matriz é controlada por um try / catch
- linha 31: se chegar a este ponto, não ocorreu nenhuma exceção. A transação é então validada.
- linhas 34-35: ocorreu uma exceção, a transação é desfeita
- linha 37: saia da cláusula utilizando. A ligação aberta na linha 18 é automaticamente encerrada.
Implementação da camada [dao]
A implementação da interface DAO IDao será a seguinte:
using System.Collections.Generic;
using System.Data;
using System.Data.SqlServerCe;
using System.Threading;
namespace Chap8 {
public class Dao : IDao {
// connecting chain
public string ConnectionString { get; set; }
// requests
public string InsertText { get; set; }
public string DeleteAllText { get; set; }
public string GetAllText { get; set; }
// interface implementation
// article insertion
public int InsertArticle(Article article) {
// is there a transaction in progress?
SqlCeTransaction transaction = Thread.GetData(Thread.GetNamedDataSlot("transaction")) as SqlCeTransaction;
// retrieve or create connection
SqlCeConnection connexion = null;
if (transaction != null) {
// recover connection
connexion = transaction.Connection as SqlCeConnection;
} else {
// create it
connexion = new SqlCeConnection(ConnectionString);
connexion.Open();
}
try {
// preparation of insertion order
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;
// execution
return sqlCommand.ExecuteNonQuery();
} finally {
// if you were not in a transaction, you close the connection
if (transaction == null) {
connexion.Close();
}
}
}
// articles list
public List<Article> GetAllArticles() {
...
}
// deletion of articles
public void DeleteAllArticles() {
...
}
}
}
A classe tem as seguintes propriedades:
- linha 9: a cadeia de ligação utilizada para se ligar à base de dados de artigos
- linha 11: comando SQL para inserir um item
- linha 12: comando SQL para suprimir todos os artigos
- linha 13: comando SQL para obter todos os artigos
Estas propriedades serão inicializadas a partir do seguinte ficheiro 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: recupera quaisquer transações colocadas pela camada [metier] na thread
- linhas 23-25: se a transação estiver presente, a ligação à qual estava associada é recuperada.
- linhas 26-30: caso contrário, é criada e aberta uma nova ligação.
- linhas 33-44: prepara o comando de inserção. Este é parametrizado (ver linha g do App.config).
- linha 33: o objeto Command é criado.
- linha 34: é associado à transação atual. Se a transação atual não existir (transaction=null), isto equivale a executar o comando SQL sem uma transação explícita. Neste caso, existe ainda uma transação implícita. Com o SQL Server CE, esta transação implícita assume por predefinição o modo autocommit: o comando SQL é confirmado após a execução.
- linha 35: o objeto Command é associado à ligação atual
- linha 36: o texto SQL a ser executado é definido. Esta é a consulta parametrizada na linha g do App.config.
- linhas 37-44: os 4 parâmetros da consulta são inicializados
- linha 46: o pedido é executado.
- linhas 49-51: lembre-se de que, se não houvesse transação, uma nova ligação foi aberta com a base, linhas 26-30. Neste caso, ela deve ser fechada. Se houvesse uma transação, a ligação não deve ser fechada, pois é a camada [metier] que a gere.
Os outros dois métodos baseiam-se no que vimos no capítulo «Bases de dados»:
// list of items
public List<Article> GetAllArticles() {
// item list - empty at start
List<Article> articles = new List<Article>();
// operation connection
using (SqlCeConnection connexion = new SqlCeConnection(ConnectionString)) {
// opening connection
connexion.Open();
// executes sqlCommand with select query
SqlCeCommand sqlCommand = new SqlCeCommand(GetAllText, connexion);
using (SqlCeDataReader reader = sqlCommand.ExecuteReader()) {
// operating income
while (reader.Read()) {
// current line operation
articles.Add(new Article(reader.GetInt32(0), reader.GetString(1), reader.GetDecimal(2), reader.GetInt32(3), reader.GetInt32(4)));
}
}
}
// we return the result
return articles;
}
// article deletion
public void DeleteAllArticles() {
using (SqlCeConnection connexion = new SqlCeConnection(ConnectionString)) {
// opening connection
connexion.Open();
// executes sqlCommand with update request
new SqlCeCommand(DeleteAllText, connexion).ExecuteNonQuery();
}
}
A aplicação 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) {
// using the configuration file
string connectionString = null;
string insertText;
string getAllText;
string deleteAllText;
try {
// connecting chain
connectionString = ConfigurationManager.ConnectionStrings["dbArticlesSqlServerCe"].ConnectionString;
// other parameters
insertText = ConfigurationManager.AppSettings["insertText"];
getAllText = ConfigurationManager.AppSettings["getAllText"];
deleteAllText = ConfigurationManager.AppSettings["deleteAllText"];
} catch (Exception e) {
Console.WriteLine("Erreur de configuration : {0}", e.Message);
return;
}
// layer creation [dao]
Dao dao = new Dao();
dao.ConnectionString = connectionString;
dao.DeleteAllText = deleteAllText;
dao.GetAllText = getAllText;
dao.InsertText = insertText;
// layer creation [job]
Metier metier = new Metier();
metier.Dao = dao;
metier.ConnectionString = connectionString;
// we create an array of articles
Article[] articles = new Article[2];
for (int i = 0; i < articles.Length; i++) {
articles[i] = new Article(0, "article", 100, 10, 1);
}
// we delete all articles
Console.WriteLine("Suppression de tous les articles...");
metier.DeleteAllArticles();
// insert the table outside the transaction
Console.WriteLine("Insertion des articles hors transaction...");
try {
metier.InsertArticlesOutOfTransaction(articles);
} catch (Exception e){
Console.WriteLine("Exception : {0}", e.Message);
}
// we display the articles
Console.WriteLine("Liste des articles");
AfficheArticles(metier);
// we delete all articles
Console.WriteLine("Suppression de tous les articles...");
metier.DeleteAllArticles();
// insert the array in a transaction
Console.WriteLine("Insertion des articles dans une transaction...");
metier.InsertArticlesInTransaction(articles);
// we display the articles
Console.WriteLine("Liste des articles");
AfficheArticles(metier);
}
private static void AfficheArticles(IMetier metier) {
// we display the articles
foreach(Article article in metier.GetAllArticles()){
Console.WriteLine(article);
}
}
}
}
- linhas 12-22: o ficheiro [App.config] é utilizado.
- linhas 24-28: a camada [dao] é instanciada e inicializada
- linhas 30-32: o mesmo se aplica à camada [metier]
- linhas 34-37: crie uma tabela com 2 artigos com o mesmo nome. A tabela [articles] na base de dados do SQL Server [dbarticles.sdf] tem uma restrição de unicidade no nome. A inserção do segundo item será, portanto, recusada. Se a matriz for inserida fora de uma transação, o primeiro item será inserido primeiro e permanecerá inserido. Se a matriz for inserida numa transação, o primeiro item será inserido primeiro e, em seguida, removido quando a transação for revertida.
- linhas 39-50: inserção fora de transação de 2 matrizes de artigos e verificação.
- linhas 52-59: igual ao anterior, mas dentro de uma transação
Os resultados são os seguintes:
- linhas 5-6: a inserção fora da transação deixou o primeiro item na base de dados
- linha 9: a inserção numa transação não deixou nenhum item na base de dados
10.9.3. Conclusão
O exemplo anterior demonstrou as vantagens dos dados locais do thread para a gestão de transações. Não deve ser reproduzido tal como está. Frameworks como o Spring, o Nhibernate, ... utilizam esta técnica, mas tornam-na ainda mais transparente: é possível que a camada [metier] utilize transações sem que a camada [dao] precise de saber. Não há transações no código da camada [dao]. Isto é conseguido através de uma técnica de proxy chamada AOP (Programação Orientada a Aspetos). Mais uma vez, recomendamos vivamente que utilize estas estruturas.
10.10. Para saber mais...
Para uma análise mais aprofundada do complexo campo da sincronização de threads, leia o capítulo «Threading» do livro C# 3.0 referido na introdução deste documento. Apresenta inúmeras técnicas de sincronização para diferentes tipos de situações.







