Skip to content

8. Tópicos de execução

8.1. Introdução

Quando uma aplicação é iniciada, ela é executada num fluxo de execução denominado «thread». A classe .NET que modela um thread é a classe System.Threading.Thread e tem a seguinte definição:

Image

Iremos utilizar apenas algumas das propriedades e métodos desta classe:

CurrentThread - propriedade estática
retorna o thread atualmente em execução
Nome - propriedade do objeto
nome da thread
isAlive - propriedade do objeto
indica se a thread está ativa (true) ou não (false)
Start - método do objeto
inicia a execução de um thread
Abort - método de objeto
interrompe permanentemente a execução de um thread
Sleep(n) - método estático
pausa uma thread por n milissegundos
Suspend() - método de objeto
suspende temporariamente a execução de um thread
Resume() - método de objeto
retoma a execução de uma thread suspensa
Join() - método de objeto
operação de bloqueio - aguarda que a thread termine antes de prosseguir para a instrução seguinte

Vejamos uma aplicação simples que demonstra a existência de uma thread de execução principal, aquela na qual a função Main de uma classe é executada:


' use of threads
Imports System
Imports System.Threading
 
Public Module thread1
    Public Sub Main()
        ' init current thread
        Dim main As Thread = Thread.CurrentThread
        ' display
        Console.Out.WriteLine(("Thread courant : " + main.Name))
        ' we change the name
        main.Name = "main"
        ' check
        Console.Out.WriteLine(("Thread courant : " + main.Name))
        ' infinite loop
        While True
            ' display
            Console.Out.WriteLine((main.Name + " : " + DateTime.Now.ToString("hh:mm:ss")))
            ' temporary shutdown
            Thread.Sleep(1000)
        End While
    End Sub
End Module

Resultados no ecrã:

dos>thread1
Thread courant :
Thread courant : main
main : 06:13:55
main : 06:13:56
main : 06:13:57
main : 06:13:58
main : 06:13:59

O exemplo anterior ilustra os seguintes pontos:

  • a função Main é executada numa thread
  • podemos aceder às propriedades desta thread através de Thread.CurrentThread
  • o papel do método Sleep. Aqui, a thread que executa Main fica em espera durante 1 segundo entre cada exibição.

8.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, estamos frequentemente a usar o termo de forma imprecisa. 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 breve momento (alguns milissegundos). É isto que cria a ilusão de execução paralela. O tempo alocado 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, normalmente usa-o durante todo o tempo que lhe foi atribuído. No entanto, pode libertá-lo mais cedo:

  • aguardando um evento (wait, join, suspend)
  • entrando em modo de espera por um período especificado (sleep)
  1. Uma thread T é criada inicialmente pelo seu construtor
Public Sub New(ByVal start As ThreadStart)

ThreadStart é do tipo delegado e define o protótipo de uma função sem parâmetros:

Public Delegate Sub ThreadStart()

Uma construção típica é a seguinte:

dim T as Thread=new Thread(new ThreadStart(run));

A função run passada como parâmetro será executada quando o thread for iniciado.

  1. A execução da thread T é iniciada por T.Start(): a função [run] passada ao construtor de T será então executada pela thread T. O programa que executa a instrução T.Start() não aguarda que a tarefa T termine: passa imediatamente para a instrução seguinte. Temos agora duas tarefas a decorrer em paralelo. Estas precisam frequentemente de comunicar entre si para conhecer o estado do trabalho partilhado a realizar. Este é o problema da sincronização de threads.
  1. Uma vez iniciada, a thread executa-se de forma autónoma. Ela irá parar quando a função de início que está a executar tiver concluído o seu trabalho.
  1. Podemos enviar determinados sinais à tarefa T:
    1. T.Suspend() diz-lhe para fazer uma pausa temporária
    2. T.Resume() diz-lhe para retomar o seu trabalho
    3. T.Abort() diz-lhe para parar definitivamente
  1. Também pode aguardar que ela termine a execução utilizando 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. É um meio de sincronização.

Vamos examinar o seguinte programa:


' options
Option Strict On
Option Explicit On 
 
' namespaces
Imports System
Imports System.Threading
 
Module thread2
    Public Sub Main()
        ' init Current thread
        Dim main As Thread = Thread.CurrentThread
        ' name the Thread
        main.Name = "main"
 
        ' creation of execution threads
        Dim tâches(4) As Thread
        Dim i As Integer
        For i = 0 To tâches.Length - 1
            ' create thread i
            tâches(i) = New Thread(New ThreadStart(AddressOf affiche))
            ' set the thread name
            tâches(i).Name = "tache_" & i
            ' start execution of thread i
            tâches(i).Start()
        Next i
        ' end of hand
        Console.Out.WriteLine(("fin du thread " + main.Name))
    End Sub
 
    Public Sub affiche()
        ' display start of execution
        Console.Out.WriteLine(("Début d'exécution de la méthode affiche dans le Thread " + Thread.CurrentThread.Name + " : " + DateTime.Now.ToString("hh:mm:ss")))
        ' sleep for 1 s
        Thread.Sleep(1000)
        ' display end of run
        Console.Out.WriteLine(("Fin d'exécution de la méthode affiche dans le Thread " + Thread.CurrentThread.Name + " : " + DateTime.Now.ToString("hh:mm:ss")))
    End Sub
End Module

O thread principal, aquele que executa a função Main, cria 5 outros threads responsáveis pela execução do método estático display. Os resultados são os seguintes:

dos>thread2
fin du thread main
Début d'exécution de la méthode affiche dans le Thread tache_0 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_1 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_2 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_3 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_4 : 05:27:53
Fin d'exécution de la méthode affiche dans le Thread tache_0 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_1 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_2 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_3 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_4 : 05:27:54

Estes resultados são muito informativos:

  • Em primeiro lugar, vemos que o início da execução de um thread não é bloqueante. O método Main iniciou a execução de 5 threads em paralelo e terminou a execução antes deles. 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.

  • Todas as threads criadas devem executar o método display. 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 aqui tem 6 threads e um processador. Ele irá alocar o processador a estas 6 threads de acordo com as suas próprias regras.
  • Os resultados mostram um efeito do método Sleep. No exemplo, a thread 0 é a primeira a executar o método display. A mensagem de início de execução é exibida, depois executa o método Sleep, que a suspende por 1 segundo. Em seguida, perde o processador, que fica disponível para outra thread. O exemplo mostra que a thread 1 irá obtê-lo. A thread 1 seguirá o mesmo caminho, tal como as outras threads. Quando o período de suspensão de 1 segundo para a thread 0 terminar, a sua execução pode ser retomada. O sistema concede-lhe o processador e ela pode concluir a execução do método display.

Vamos modificar o nosso programa para terminar o método Main com as seguintes instruções:

        ' fin de main
        Console.Out.WriteLine(("fin du thread " + main.Name))
        Environment.Exit(0)

A execução do novo programa produz:

fin du thread main

Os threads criados pela função Main não são executados. É a instrução

        Environment.Exit(0)

que faz isso: ela encerra todas as threads da aplicação, não apenas a thread Main. A solução para este problema é que o método Main aguarde que as threads que criou terminem de ser executadas antes de se encerrar. Isto pode ser feito utilizando o método Join da classe Thread:


        ' on attend la fin d'exécution de tous les threads
        For i = 0 To tâches.Length - 1
            ' attente de la fin d'exécution du thread i
            tâches(i).Join()
        Next i        'for
        ' fin de main
        Console.Out.WriteLine(("fin du thread " + main.Name))
        Environment.Exit(0)

Isto produz os seguintes resultados:

Début d'exécution de la méthode affiche dans le Thread tache_1 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_2 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_3 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_4 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_0 : 05:34:48
Fin d'exécution de la méthode affiche dans le Thread tache_2 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_1 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_3 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_0 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_4 : 05:34:50
fin du thread main

8.3. Vantagens dos threads

Agora que destacámos a existência de um thread padrão — aquele que executa o método Main — e sabemos como criar outros, vamos considerar as vantagens dos threads para nós e por que razão os estamos a apresentar aqui. Existe um tipo de aplicação que se presta bem ao uso de threads: as aplicações cliente-servidor na Internet. Numa aplicação deste tipo, um servidor localizado na máquina S1 responde a pedidos de clientes localizados em máquinas remotas C1, C2, ..., Cn.

Utilizamos aplicações da Internet que seguem este padrão todos os dias: serviços web, e-mail, navegação em fóruns, transferências de ficheiros... No diagrama acima, o servidor S1 deve atender os clientes C1, C2, ..., Cn simultaneamente. Se tomarmos o exemplo de um servidor FTP (File Transfer Protocol) que entrega ficheiros aos seus clientes, sabemos que uma transferência de ficheiros pode, por vezes, demorar várias horas. É, evidentemente, impensável que um único cliente monopolize o servidor durante um período tão longo. 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. Uma vez que o processador é partilhado ciclicamente entre todos os threads ativos na máquina, o servidor dedica um pouco de tempo a cada cliente, garantindo assim a concorrência do serviço.

8.4. Acesso a recursos partilhados

No exemplo cliente-servidor mencionado acima, cada thread atende um cliente de forma amplamente independente. No entanto, as threads podem precisar de cooperar para fornecer o serviço solicitado ao seu cliente, particularmente ao aceder a recursos partilhados. O diagrama acima assemelha-se aos balcões de um grande serviço público, como uma estação de correios, onde um funcionário em cada balcão atende um cliente. Suponha que, de vez em quando, esses funcionários precisam de fazer fotocópias de documentos trazidos pelos seus clientes e que existe apenas uma fotocopiadora. Dois funcionários não podem utilizar a fotocopiadora ao mesmo tempo. Se o funcionário i encontrar a fotocopiadora a ser utilizada pelo funcionário j, terá de esperar. Esta situação é designada por acesso a um recurso partilhado e, na ciência da computação, é bastante complexa de gerir. Considere o seguinte exemplo:

  • uma aplicação irá gerar n threads, em que 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 é exibido. Devemos, portanto, obter n.

O programa é o seguinte:


' options
Option Explicit On 
Option Strict On
 
' use of threads
Imports System
Imports System.Threading
 
Public Class thread3
    ' class variables
    Private Shared cptrThreads As Integer = 0
 
    Public Overloads Shared Sub Main(ByVal args() As [String])
        ' instructions for use
        Const syntaxe As String = "pg nbThreads"
        Const nbMaxThreads As Integer = 100
 
        ' verification no. of arguments
        If args.Length <> 1 Then
            ' error
            Console.Error.WriteLine(syntaxe)
            ' stop
            Environment.Exit(1)
        End If
        ' argument quality check
        Dim nbThreads As Integer = 0
        Try
            nbThreads = Integer.Parse(args(0))
            If nbThreads < 1 Or nbThreads > nbMaxThreads Then
                Throw New Exception
            End If
        Catch
            ' error
            Console.Error.WriteLine("Nombre de threads incorrect (entre 1 et " & nbMaxThreads & ")")
            ' end
            Environment.Exit(2)
        End Try
        ' thread creation and generation
        Dim threads(nbThreads - 1) As Thread
        Dim i As Integer
        For i = 0 To nbThreads - 1
            ' creation
            threads(i) = New Thread(New ThreadStart(AddressOf incrémente))
            ' naming
            threads(i).Name = "tache_" & i
            ' launch
            threads(i).Start()
        Next i
        ' waiting for threads to finish
        For i = 0 To nbThreads - 1
            threads(i).Join()
        Next i        ' counter display
        Console.Out.WriteLine(("Nombre de threads générés : " & cptrThreads))
    End Sub
 
    Public Shared Sub incrémente()
        ' increases thread counter
        ' meter reading
        Dim valeur As Integer = cptrThreads
        ' follow-up
        Console.Out.WriteLine(("A " + DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a lu la valeur du compteur : " & cptrThreads))
        ' waiting
        Thread.Sleep(1000)
        ' counter incrementation
        cptrThreads = valeur + 1
        ' follow-up
        Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a écrit la valeur du compteur : " & cptrThreads))
    End Sub
End Class

Não nos deteremos na parte da criação de threads, que já abordámos. Em vez disso, vamos concentrar-nos no método Increment, utilizado por cada thread para incrementar o contador estático cptrThreads.

  1. O contador é lido
  2. a thread faz uma pausa de 1 segundo. Por isso, perde a CPU
  3. o contador é incrementado

O Passo 2 existe apenas para forçar a thread a perder o processador. O processador será atribuído 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. Existe o risco de perder a CPU entre o momento em que o valor do contador é lido e o momento em que o seu valor, incrementado em 1, é gravado. Com efeito, a operação de incremento envolverá várias instruções básicas ao nível do processador que podem ser interrompidas. O Passo 2, a pausa de um segundo, existe, portanto, apenas para ter em conta este risco. Os resultados obtidos são os seguintes:

dos>thread3 5
A 05:44:34, le thread tache_0 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_1 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_2 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_3 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_4 a lu la valeur du compteur : 0
A 05:44:35, le thread tache_0 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_1 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_2 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_3 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_4 a écrit la valeur du compteur : 1
Nombre de threads générés : 1

Olhando para estes resultados, fica claro o que está a acontecer:

  • Um primeiro thread lê o contador. Encontra 0.
  • Ele faz uma pausa de 1 segundo, cedendo assim a CPU
  • Uma segunda thread assume então a CPU e também lê o valor do contador. Este continua a ser 0, uma vez que a thread anterior ainda não o incrementou. Também faz uma pausa de 1 segundo.
  • Em 1 segundo, todas as 5 threads têm tempo para ser executadas e ler o valor 0.
  • Quando acordam uma após a outra, incrementam o valor 0 que leram e escrevem o valor 1 no contador, o que é confirmado pelo programa principal (Main).

De onde vem o problema? A segunda thread leu um valor incorreto porque a primeira thread foi interrompida antes de terminar a sua tarefa, que era atualizar o contador na janela. Isto leva-nos ao conceito de recursos críticos e secções críticas num programa:

  • Um recurso crítico é um recurso que pode ser detido por apenas uma 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 uma thread durante a qual esta acede a um recurso crítico. Temos de garantir que, durante esta secção crítica, esta é a única com acesso ao recurso.

8.5. Acesso exclusivo a um recurso partilhado

No nosso exemplo, a secção crítica é o código localizado entre a leitura do contador e a gravação do seu novo valor:


        ' meter reading
        Dim valeur As Integer = cptrThreads
        ' waiting
        Thread.Sleep(1000)
        ' counter incrementation
        cptrThreads = valeur + 1

Para executar este código, é necessário garantir que a thread esteja sozinha. Ela pode ser interrompida, mas durante essa interrupção, nenhuma outra thread deve poder executar este mesmo código. A plataforma .NET oferece várias ferramentas para garantir a entrada em modo single-threaded em secções críticas do código. Iremos utilizar a classe Mutex:

Image

Aqui, utilizaremos apenas os seguintes construtores e métodos:

public Mutex()
cria um objeto de sincronização M
public bool WaitOne()
O thread T1, que executa a operação M.WaitOne(), solicita a posse do objeto de sincronização M. Se o mutex M não estiver na posse de nenhum thread (o caso inicial), ele é «concedido» ao thread T1, que o solicitou. Se, pouco depois, a thread T2 realizar a mesma operação, será bloqueada. Na verdade, um mutex só pode pertencer a uma única thread. Será libertado 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 a operaçã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 obterá por sua vez; as outras que aguardam M permanecem 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 através de 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 sincronização da execução da secção crítica pode ser alcançada da seguinte forma:

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

onde M é um objeto Mutex. É claro que nunca se deve esquecer de libertar um Mutex que já não seja necessário, para que outro segmento possa entrar na secção crítica; caso contrário, os segmentos que aguardam um Mutex que nunca é libertado nunca terão acesso ao processador. Além disso, é necessário evitar uma situação de impasse em que dois segmentos aguardam um pelo outro. Considere as seguintes ações a ocorrerem em sequência:

  • uma thread T1 adquire a posse de um Mutex M1 para aceder a um recurso partilhado R1
  • uma thread T2 adquire um Mutex M2 para aceder a um recurso partilhado R2
  • A thread T1 solicita o Mutex M2. Ela é bloqueada.
  • A thread T2 solicita o Mutex M1. Ela fica bloqueada.

Aqui, as threads T1 e T2 estão à espera uma da outra. Esta situação ocorre 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 é adquirir ambos os recursos simultaneamente utilizando um único Mutex M. No entanto, isto nem sempre é viável se, por exemplo, resultar num bloqueio prolongado de um recurso dispendioso. Outra solução é que uma thread que detém M1 e que não consegue obter M2 liberte M1 para evitar um impasse. Se aplicarmos o que acabámos de ver ao exemplo anterior, a nossa aplicação fica da seguinte forma:


' options
Option Explicit On 
Option Strict On
 
' use of threads
Imports System
Imports System.Threading
 
Public Class thread4
    ' class variables
    Private Shared cptrThreads As Integer = 0    ' thread counter
    Private Shared autorisation As Mutex
 
    Public Overloads Shared Sub Main(ByVal args() As [String])
        ' instructions for use
        Const syntaxe As String = "pg nbThreads"
        Const nbMaxThreads As Integer = 100
 
        ' verification no. of arguments
        If args.Length <> 1 Then
            ' error
            Console.Error.WriteLine(syntaxe)
            ' stop
            Environment.Exit(1)
        End If
        ' argument quality check
        Dim nbThreads As Integer = 0
        Try
            nbThreads = Integer.Parse(args(0))
            If nbThreads < 1 Or nbThreads > nbMaxThreads Then
                Throw New Exception
            End If
        Catch
        End Try
 
        ' initialize access authorization to a critical section
        autorisation = New Mutex
 
        ' thread creation and generation
        Dim threads(nbThreads) As Thread
        Dim i As Integer
        For i = 0 To nbThreads - 1
            ' creation
            threads(i) = New Thread(New ThreadStart(AddressOf incrémente))
            ' naming
            threads(i).Name = "tache_" & i
            ' launch
            threads(i).Start()
        Next i
        ' waiting for threads to finish
        For i = 0 To nbThreads - 1
            threads(i).Join()
        Next i
        ' counter display
        Console.Out.WriteLine(("Nombre de threads générés : " & cptrThreads))
    End Sub
 
    Public Shared Sub incrémente()
        ' increases thread counter
        ' we request permission to enter the critical secton
        autorisation.WaitOne()
        ' meter reading
        Dim valeur As Integer = cptrThreads
        ' follow-up
        Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a lu la valeur du compteur : " & cptrThreads))
        ' waiting
        Thread.Sleep(1000)
        ' counter incrementation
        cptrThreads = valeur + 1
        ' follow-up
        Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a écrit la valeur du compteur : " & cptrThreads))
        ' access authorization is returned
        autorisation.ReleaseMutex()
    End Sub
End Class

Os resultados obtidos são os esperados:

dos>thread4 5
A 05:51:10, le thread tache_0 a lu la valeur du compteur : 0
A 05:51:11, le thread tache_0 a écrit la valeur du compteur : 1
A 05:51:11, le thread tache_1 a lu la valeur du compteur : 1
A 05:51:12, le thread tache_1 a écrit la valeur du compteur : 2
A 05:51:12, le thread tache_2 a lu la valeur du compteur : 2
A 05:51:13, le thread tache_2 a écrit la valeur du compteur : 3
A 05:51:13, le thread tache_3 a lu la valeur du compteur : 3
A 05:51:14, le thread tache_3 a écrit la valeur du compteur : 4
A 05:51:14, le thread tache_4 a lu la valeur du compteur : 4
A 05:51:15, le thread tache_4 a écrit la valeur du compteur : 5
Nombre de threads générés : 5

8.6. Sincronização baseada em eventos

Considere a seguinte situação, por vezes referida como um cenário produtor-consumidor.

  1. Temos uma matriz na qual alguns processos depositam dados (os produtores) e outros os leem (os consumidores).
  2. Os produtores são iguais entre si, mas exclusivos: apenas um produtor de cada vez pode depositar dados na matriz.
  3. Os consumidores são iguais entre si, mas mutuamente exclusivos: apenas um leitor de cada vez pode ler os dados armazenados na matriz.
  4. Um consumidor só pode ler dados da tabela depois de um produtor os ter gravado lá, e um produtor só pode gravar novos dados na tabela depois de os dados existentes terem sido consumidos.

Nesta explicação, podemos distinguir dois recursos partilhados:

    1. a tabela gravável
    2. a matriz de leitura apenas

O acesso a estes dois recursos partilhados pode ser controlado por mutexes, como visto anteriormente, um para cada recurso. Assim que um consumidor obtiver a matriz de leitura, deve verificar se existem efetivamente dados nela. Será utilizado um evento para o notificar disso. Da mesma forma, um produtor que tenha obtido a matriz de escrita deve esperar até que um consumidor a tenha esvaziado. Também aqui será utilizado um evento.

Os eventos utilizados serão da classe AutoResetEvent:

Image

Este tipo de evento é análogo a um booleano, mas evita esperas ativas ou semi-ativas. Assim, se o acesso de escrita for controlado por um booleano *canWrite*, um produtor executará código como o seguinte antes de escrever:

while(peutEcrire==false)        ' attente active

ou

while(peutEcrire==false) ' attente semi-active
    Thread.Sleep(100)                ' attente de 100ms
end while

No primeiro método, o thread sobrecarrega desnecessariamente o processador. No segundo, verifica o estado do booleano canWrite a cada 100 ms. A classe AutoResetEvent permite uma melhoria adicional: o thread solicitará ser acordado quando o evento pelo qual está à espera ocorrer:

AutoEvent peutEcrire=new AutoResetEvent(false)        ' peutEcrire=false;
....
peutEcrire.WaitOne() ' thread waits for peutEcrire event to change to true

A operação

AutoEvent peutEcrire=new AutoResetEvent(false)        ' peutEcrire=false;

inicializa o booleano canWrite como false. A operação

peutEcrire.WaitOne() ' le thread attend que l'évt peutEcrire passe à vrai

executada por um thread faz com que esse thread prossiga se o booleano canWrite* for verdadeiro; caso contrário, fica bloqueado até que se torne verdadeiro. Outro thread irá defini-lo como verdadeiro utilizando a operação canWrite.Set() ou como falso utilizando a operação canWrite.Reset()*.

O programa produtor-consumidor é o seguinte:


' use of reader and writer threads
' illustrates the simultaneous use of shared resources and synchronization
 
' options
Option Explicit On 
Option Strict On
 
' use of threads
Imports System
Imports System.Threading
 
Public Class lececr
 
    ' class variables
    Private Shared data(5) As Integer    ' resource shared between reader and writer threads
    Private Shared lecteur As Mutex    ' synchronization variable to read the table
    Private Shared écrivain As Mutex    ' synchronization variable to write to the table
    Private Shared objRandom As New Random(DateTime.Now.Second)    ' a random number generator
    Private Shared peutLire As AutoResetEvent    ' indicates that you can read the contents of data
    Private Shared peutEcrire As AutoResetEvent
 
    Public Shared Sub Main(ByVal args() As [String])
 
        ' number of threads to generate
        Const nbThreads As Integer = 3
 
        ' flag initialization
        peutLire = New AutoResetEvent(False)        ' cannot be read yet
        peutEcrire = New AutoResetEvent(True)        ' we can already write
 
        ' initialization of synchronization variables
        lecteur = New Mutex         ' synchronizes drives
        écrivain = New Mutex         ' synchronizes writers
 
        ' creation of reader threads
        Dim lecteurs(nbThreads) As Thread
        Dim i As Integer
        For i = 0 To nbThreads - 1
            ' creation
            lecteurs(i) = New Thread(New ThreadStart(AddressOf lire))
            lecteurs(i).Name = "lecteur_" & i
            ' launch
            lecteurs(i).Start()
        Next i
 
        ' creating writer threads
        Dim écrivains(nbThreads) As Thread
        For i = 0 To nbThreads - 1
            ' creation
            écrivains(i) = New Thread(New ThreadStart(AddressOf écrire))
            écrivains(i).Name = "écrivain_" & i
            ' launch
            écrivains(i).Start()
        Next i
 
        'end of hand
        Console.Out.WriteLine("fin de Main...")
    End Sub
 
    ' read the contents of the table
    Public Shared Sub lire()
        ' review section
        lecteur.WaitOne()        ' a single reader can pass
        peutLire.WaitOne()        ' you must be able to read
 
        ' table reading
        Dim i As Integer
        For i = 0 To data.Length - 1
            'wait 1 s
            Thread.Sleep(1000)
            ' display
            Console.Out.WriteLine((DateTime.Now.ToString("hh:mm:ss") & " : Le lecteur " & Thread.CurrentThread.Name & " a lu le nombre " & data(i)))
        Next i
 
        ' we can no longer read
        peutLire.Reset()
        ' we can write
        peutEcrire.Set()
        ' end of critical section
        lecteur.ReleaseMutex()
    End Sub
 
    ' write in the table
    Public Shared Sub écrire()
        ' review section
        ' only one writer can pass
        écrivain.WaitOne()
        ' we have to wait for write authorization
        peutEcrire.WaitOne()
 
        ' writing table
        Dim i As Integer
        For i = 0 To data.Length - 1
            'wait 1 s
            Thread.Sleep(1000)
            ' display
            data(i) = objRandom.Next(0, 1000)
            Console.Out.WriteLine((DateTime.Now.ToString("hh:mm:ss") & " : L'écrivain " & Thread.CurrentThread.Name & " a écrit le nombre " & data(i)))
        Next i
 
        ' we can no longer write
        peutEcrire.Reset()
        ' you can read
        peutLire.Set()
        'end of critical section
        écrivain.ReleaseMutex()
    End Sub
End Class

A execução produz os seguintes resultados:

dos>lececr
fin de Main...
05:56:56 : L'écrivain écrivain_0 a écrit le nombre 459
05:56:57 : L'écrivain écrivain_0 a écrit le nombre 955
05:56:58 : L'écrivain écrivain_0 a écrit le nombre 212
05:56:59 : L'écrivain écrivain_0 a écrit le nombre 297
05:57:00 : L'écrivain écrivain_0 a écrit le nombre 37
05:57:01 : L'écrivain écrivain_0 a écrit le nombre 623
05:57:02 : Le lecteur lecteur_0 a lu le nombre 459
05:57:03 : Le lecteur lecteur_0 a lu le nombre 955
05:57:04 : Le lecteur lecteur_0 a lu le nombre 212
05:57:05 : Le lecteur lecteur_0 a lu le nombre 297
05:57:06 : Le lecteur lecteur_0 a lu le nombre 37
05:57:07 : Le lecteur lecteur_0 a lu le nombre 623
05:57:08 : L'écrivain écrivain_1 a écrit le nombre 549
05:57:09 : L'écrivain écrivain_1 a écrit le nombre 34
05:57:10 : L'écrivain écrivain_1 a écrit le nombre 781
05:57:11 : L'écrivain écrivain_1 a écrit le nombre 555
05:57:12 : L'écrivain écrivain_1 a écrit le nombre 812
05:57:13 : L'écrivain écrivain_1 a écrit le nombre 406
05:57:14 : Le lecteur lecteur_1 a lu le nombre 549
05:57:15 : Le lecteur lecteur_1 a lu le nombre 34
05:57:16 : Le lecteur lecteur_1 a lu le nombre 781
05:57:17 : Le lecteur lecteur_1 a lu le nombre 555
05:57:18 : Le lecteur lecteur_1 a lu le nombre 812
05:57:19 : Le lecteur lecteur_1 a lu le nombre 406
05:57:20 : L'écrivain écrivain_2 a écrit le nombre 442
05:57:21 : L'écrivain écrivain_2 a écrit le nombre 83
^C

Podem ser observados os seguintes pontos:

  • existe, de facto, apenas um leitor de cada vez, mesmo que este perca a CPU na secção crítica de leitura
  • existe, de facto, apenas um gravador de cada vez, mesmo que este perca a CPU na secção crítica de gravação
  • Um leitor só lê quando há algo para ler na tabela
  • Um gravador só grava quando a matriz tiver sido totalmente lida