Skip to content

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:

1
2
3
4
5
6
7
8
Thread courant :
Thread courant : main
main : 04:19:00
main : 04:19:01
main : 04:19:02
main : 04:19:03
main : 04:19:04
^CAppuyez sur une touche pour continuer...
  • 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)
  1. Uma thread T é criada inicialmente por um dos fabricantes apresentados acima, por exemplo:
Thread thread=new Thread(Start);

onde Start é um método com uma das duas assinaturas seguintes:

void Start();
void Start(object obj);

A criação de um segmento de execução não o inicia.

  1. 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.
  2. 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.
  3. A thread T pode ser forçada a terminar:
    1. T.Abort() solicita que a thread T termine.
  4. 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:

Début d'exécution de la méthode Affiche dans le Thread 0 : 10:30:44
Début d'exécution de la méthode Affiche dans le Thread 1 : 10:30:44
Début d'exécution de la méthode Affiche dans le Thread 2 : 10:30:44
Début d'exécution de la méthode Affiche dans le Thread 3 : 10:30:44
Début d'exécution de la méthode Affiche dans le Thread 4 : 10:30:44
Fin du thread Main à 10:30:44
Fin d'exécution de la méthode Affiche dans le Thread 0 : 10:30:45
Fin d'exécution de la méthode Affiche dans le Thread 1 : 10:30:45
Fin d'exécution de la méthode Affiche dans le Thread 2 : 10:30:45
Fin d'exécution de la méthode Affiche dans le Thread 3 : 10:30:45
Fin d'exécution de la méthode Affiche dans le Thread 4 : 10:30:45

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:

1
2
3
4
5
6
Début d'exécution de la méthode Affiche dans le Thread 0 : 10:33:18
Début d'exécution de la méthode Affiche dans le Thread 1 : 10:33:18
Début d'exécution de la méthode Affiche dans le Thread 2 : 10:33:18
Début d'exécution de la méthode Affiche dans le Thread 3 : 10:33:18
Début d'exécution de la méthode Affiche dans le Thread 4 : 10:33:18
Fin du thread Main à 10:33:18
  • 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:
        Environment.Exit(0);

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:

Début d'exécution de la méthode Affiche dans le Thread 0 : 10:35:18
Début d'exécution de la méthode Affiche dans le Thread 1 : 10:35:18
Début d'exécution de la méthode Affiche dans le Thread 2 : 10:35:18
Début d'exécution de la méthode Affiche dans le Thread 3 : 10:35:18
Début d'exécution de la méthode Affiche dans le Thread 4 : 10:35:18
Fin d'exécution de la méthode Affiche dans le Thread 0 : 10:35:19
Fin d'exécution de la méthode Affiche dans le Thread 1 : 10:35:19
Fin d'exécution de la méthode Affiche dans le Thread 2 : 10:35:19
Fin d'exécution de la méthode Affiche dans le Thread 3 : 10:35:19
Fin d'exécution de la méthode Affiche dans le Thread 4 : 10:35:19
Fin du thread Main à 10:35:19
  • 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:

Thread t=new Thread(Run);

onde Run era um método com a seguinte assinatura:

void Run();

Também é possível utilizar a seguinte assinatura:

void Run(object obj);

Isto permite que as informações sejam transmitidas para o thread iniciado. Por exemplo,

t.Start(obj1);

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:

1
2
3
4
5
6
Thread 0 terminé : début 11:18:50, durée programmée 1 s, fin 11:18:51, durée effective 00:00:01.0156250
Thread 1 terminé : début 11:18:50, durée programmée 2 s, fin 11:18:52, durée effective 00:00:02
Thread 2 terminé : début 11:18:50, durée programmée 3 s, fin 11:18:53, durée effective 00:00:03
Thread 3 terminé : début 11:18:50, durée programmée 4 s, fin 11:18:54, durée effective 00:00:04
Thread 4 terminé : début 11:18:50, durée programmée 5 s, fin 11:18:55, durée effective 00:00:05
Fin du thread Main à 11:18:55

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.

  1. linha 62: o contador é lido
  2. linha 66: a thread pára durante 1 segundo. Por conseguinte, perde o processador
  3. 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:

A 12:00:56, le thread 3  a lu la valeur du compteur : 0
A 12:00:56, le thread 2  a lu la valeur du compteur : 0
A 12:00:56, le thread 1  a lu la valeur du compteur : 0
A 12:00:56, le thread 0  a lu la valeur du compteur : 0
A 12:00:56, le thread 4  a lu la valeur du compteur : 0
A 12:00:57, le thread 3  a écrit la valeur du compteur : 1
A 12:00:57, le thread 2  a écrit la valeur du compteur : 1
A 12:00:57, le thread 1  a écrit la valeur du compteur : 1
A 12:00:57, le thread 0  a écrit la valeur du compteur : 1
A 12:00:57, le thread 4  a écrit la valeur du compteur : 1
Nombre de threads générés : 1

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:

lock(obj){section critique}

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:

A 09:37:09, le thread 0 attend l'autorisation d'entrer dans la section critique
A 09:37:09, le thread 0 a lu la valeur du compteur : 0
A 09:37:09, le thread 1 attend l'autorisation d'entrer dans la section critique
A 09:37:09, le thread 2 attend l'autorisation d'entrer dans la section critique
A 09:37:09, le thread Main attend la fin du thread 2
A 09:37:10, le thread 0 a écrit la valeur du compteur : 1
A 09:37:10, le thread 1 a lu la valeur du compteur : 1
A 09:37:10, le thread 0 a quitté la section critique
A 09:37:11, le thread 1 a écrit la valeur du compteur : 2
A 09:37:11, le thread 1 a quitté la section critique
A 09:37:11, le thread 2 a lu la valeur du compteur : 2
A 09:37:12, le thread 2 a écrit la valeur du compteur : 3
A 09:37:12, le thread 2 a quitté la section critique
A 09:37:12, le thread Main a été prévenu de la fin du thread 2
A 09:37:12, le thread Main attend la fin du thread 1
A 09:37:12, le thread Main a été prévenu de la fin du thread 1
A 09:37:12, le thread Main attend la fin du thread 0
A 09:37:12, le thread Main a été prévenu de la fin du thread 0
Nombre de threads générés : 3
  • 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:

public Mutex()
cria um Mutex M
public bool WaitOne()
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.
public void ReleaseMutex()
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:

M.WaitOne();
// le thread est seul à entrer ici
// section critique
....
M.ReleaseMutex();

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:

AutoResetEvent barrière=new AutoresetEvent(bool état);

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:

barrière.WaitOne();
  • 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:
barrière.Set(); 

Pode acontecer que um thread queira fechar uma barreira. Pode fazê-lo através de:

barrière.Reset(); 

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:

Image

O método Incrementally tem a seguinte assinatura:

public static int Increment(ref int location);

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.

  1. Temos uma matriz na qual algumas threads depositam dados (escritores) e outras os leem (leitores).
  2. Os escritores são iguais, mas exclusivos: apenas um escritor de cada vez pode inserir dados na tabela.
  3. Os leitores são iguais, mas exclusivos: apenas um leitor de cada vez pode ler os dados depositados na tabela.
  4. 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:

Méthode [Lire] démarrée par le thread n° L0
Méthode [Lire] démarrée par le thread n° L1
Méthode [Ecrire] démarrée par le thread n° E0
Méthode [Ecrire] démarrée par le thread n° E1
Fin de Main...
02:29:18 : L'écrivain E0 a écrit le nombre 607
02:29:19 : L'écrivain E0 a écrit le nombre 805
02:29:20 : L'écrivain E0 a écrit le nombre 650
Méthode [Ecrire] terminée par le thread n° E0
02:29:21 : Le lecteur L0 a lu le nombre 607
02:29:22 : Le lecteur L0 a lu le nombre 805
02:29:23 : Le lecteur L0 a lu le nombre 650
Méthode [Lire] terminée par le thread n° L0
02:29:24 : L'écrivain E1 a écrit le nombre 186
02:29:25 : L'écrivain E1 a écrit le nombre 881
02:29:26 : L'écrivain E1 a écrit le nombre 415
Méthode [Ecrire] terminée par le thread n° E1
02:29:27 : Le lecteur L1 a lu le nombre 186
02:29:28 : Le lecteur L1 a lu le nombre 881
02:29:29 : Le lecteur L1 a lu le nombre 415
Méthode [Lire] terminée par le thread n° L1

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:

vários escritores esperam pelo mesmo balcão
as suas filas estão bloqueadas
a porta fica livre e o número do escritor seguinte é chamado
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 escritor verifica o seu número e apenas aquele cujo número foi chamado
vai para o guichê. Os outros voltam a ficar em espera.
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.

Image

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.
object jeton=new object();
  • Para chegar ao contador, as threads primeiro solicitam o :
Monitor.Enter(jeton);

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:
while (! jeNeSuisPasCeluiQuiEstAttendu) {Monitor.Wait(jeton);}

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á:
  1. // trabalho no contador
  2. ....

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:
1
2
3
4
5
6
7
8
// travail au guichet
....
// modification condition d'accès au guichet
...
// réveil des threads en attente du jeton
Monitor.PulseAll(jeton);
// libération du jeton
Monitor.Exit(jeton);

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:
Monitor.Exit(jeton);

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:

1
2
3
4
5
6
7
8
// acquisition jeton
Monitor.Enter(jeton);
// modification condition d'accès au guichet
....
// réveil des threads en attente du jeton
Monitor.PulseAll(jeton);
// libération du jeton
Monitor.Exit(jeton);

No final, a construção padrão proposta no capítulo Threading do livro C# 3.0 é a seguinte:

  • define counter access token :
object jeton=new object();
  • solicitar acesso ao contador:
lock(jeton){
    while (! jeNeSuisPasCeluiQuiEstAttendu) 
        Monitor.Wait(jeton);
}
// passage au guichet
...
lock(jeton){...} 

é equivalente a

Monitor.Enter(jeton);
try{...} finally{Monitor.Exit(jeton);}

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:

Méthode [Lire] démarrée par le thread n° L0
Méthode [Lire] démarrée par le thread n° L2
Méthode [Lire] démarrée par le thread n° L1
Le lecteur L2 est en position 0
Le lecteur L1 est en position 1
Le lecteur L0 est en position 2
Méthode [Ecrire] démarrée par le thread n° E0
Méthode [Ecrire] démarrée par le thread n° E1
L'écrivain E0 est en position 0
L'écrivain E1 est en position 1
L'écrivain E2 est en position 2
Fin de Main...
Méthode [Ecrire] démarrée par le thread n° E2
12:09:05 : L'écrivain E0 a écrit le nombre 815
12:09:06 : L'écrivain E0 a écrit le nombre 990
12:09:07 : L'écrivain E0 a écrit le nombre 563
Méthode [Ecrire] terminée par le thread n° E0
12:09:08 : Le lecteur L2 a lu le nombre 815
12:09:09 : Le lecteur L2 a lu le nombre 990
12:09:10 : Le lecteur L2 a lu le nombre 563
Méthode [Lire] terminée par le thread n° L2
12:09:11 : L'écrivain E1 a écrit le nombre 411
12:09:12 : L'écrivain E1 a écrit le nombre 11
12:09:13 : L'écrivain E1 a écrit le nombre 54
Méthode [Ecrire] terminée par le thread n° E1
12:09:14 : Le lecteur L1 a lu le nombre 411
12:09:15 : Le lecteur L1 a lu le nombre 11
12:09:16 : Le lecteur L1 a lu le nombre 54
Méthode [Lire] terminée par le thread n° L1
12:09:17 : L'écrivain E2 a écrit le nombre 698
12:09:18 : L'écrivain E2 a écrit le nombre 448
12:09:19 : L'écrivain E2 a écrit le nombre 472
Méthode [Ecrire] terminée par le thread n° E2
12:09:20 : Le lecteur L0 a lu le nombre 698
12:09:21 : Le lecteur L0 a lu le nombre 448
12:09:22 : Le lecteur L0 a lu le nombre 472
Méthode [Lire] terminée par le thread n° L0

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:

  1. ThreadPool.QueueWorkItem(WaitCallBack)
  2. 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:

1
2
3
4
5
6
Nombre minimum de tâches bloquantes dans le pool : 2
Nombre minimum de tâches asynchrones dans le pool : 2
Nombre minimum de tâches bloquantes dans le pool après changement : 3
Nombre maximum de tâches bloquantes dans le pool : 500
Nombre maximum de tâches asynchrones dans le pool : 1000
Nombre maximum de tâches bloquantes dans le pool après changement : 5

Para executar as 7 threads:

A 03:07:37:04, le thread n° 0 va dormir pendant 10 seconde(s)
Nombre de threads pour tâches bloquantes disponibles dans le pool : 3
A 03:07:37:04, le thread n° 2 va dormir pendant 12 seconde(s)
Nombre de threads pour tâches bloquantes disponibles dans le pool : 2
A 03:07:37:04, le thread n° 1 va dormir pendant 11 seconde(s)
Nombre de threads pour tâches bloquantes disponibles dans le pool : 2
A 03:07:38:04, le thread n° 3 va dormir pendant 13 seconde(s)
Nombre de threads pour tâches bloquantes disponibles dans le pool : 1
A 03:07:38:54, le thread n° 4 va dormir pendant 14 seconde(s)
Nombre de threads pour tâches bloquantes disponibles dans le pool : 0
A 03:07:47:04, le thread n° 0 se termine. Il était programmé pour durer 10 seconde(s). Il a duré 00:00:10 seconde(s)
A 03:07:47:04, le thread n° 5 va dormir pendant 15 seconde(s)
Nombre de threads pour tâches bloquantes disponibles dans le pool : 0
A 03:07:48:04, le thread n° 1 se termine. Il était programmé pour durer 11 seconde(s). Il a duré 00:00:11 seconde(s)
A 03:07:48:04, le thread n° 6 va dormir pendant 16 seconde(s)
Nombre de threads pour tâches bloquantes disponibles dans le pool : 0
A 03:07:49:04, le thread n° 2 se termine. Il était programmé pour durer 12 seconde(s). Il a duré 00:00:12 seconde(s)
A 03:07:51:04, le thread n° 3 se termine. Il était programmé pour durer 13 seconde(s). Il a duré 00:00:14 seconde(s)
A 03:07:52:54, le thread n° 4 se termine. Il était programmé pour durer 14 seconde(s). Il a duré 00:00:15.5000000 seconde(s)
A 03:08:02:04, le thread n° 5 se termine. Il était programmé pour durer 15 seconde(s). Il a duré 00:00:25 seconde(s)
A 03:08:04:04, le thread n° 6 se termine. Il était programmé pour durer 16 seconde(s). Il a duré 00:00:27 seconde(s)
  • 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:

1
2
3
4
5
6
Fin du thread Main. Laissez les autres threads se terminer puis tapez [entrée] pour terminer...
Thread 0 terminé : début 05:19:46, durée programmée 1 s, fin 05:19:47, durée effective 00:00:01
Le thread System.ComponentModel.BackgroundWorker a rencontré l'erreur suivante : test....
Thread System.ComponentModel.BackgroundWorker annulé
Thread 1 terminé : début 05:19:46, durée programmée 2 s, fin 05:19:49, durée effective 00:00:03
Thread 2 terminé : début 05:19:46, durée programmée 3 s, fin 05:19:50, durée effective 00:00:04

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:

  1. o thread é criado ou solicitado a partir de um pool de threads para satisfazer um pedido do utilizador
  2. 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].
  3. 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:
Thread.SetData(Thread.GetNamedDataSlot("clé"),valeur);
  • para recuperar o valor associado à chave:
Thread.GetData(Thread.GetNamedDataSlot("clé"));

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:

1
2
3
4
5
6
7
8
9
Suppression de tous les articles...
Insertion des articles hors transaction...
Exception : A duplicate value cannot be inserted into a unique index. [ Table na
me = ARTICLES,Constraint name = UQ__ARTICLES__0000000000000010 ]
Liste des articles
[7,article,100,10,1]
Suppression de tous les articles...
Insertion des articles dans une transaction...
Liste des articles
  • 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.