Skip to content

8. Os threads de execução

8.1. Introdução

Quando se inicia uma aplicação, esta é 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 - propriété statique
retorna o thread atualmente em execução
Name - propriété d'objet
nome do thread
isAlive - propriété d'objet
indica se o thread está ativo (true) ou não (false)
Start - méthode d'objet
inicia a execução de um thread
Abort - méthode d'objet
interrompe definitivamente a execução de um thread
Sleep(n) - méthode statique
suspende a execução de um thread durante n milissegundos
Suspend() - méthode d'objet
suspende temporariamente a execução de um thread
Resume() - méthode d'objet
retoma a execução de um thread suspenso
Join() - méthode d'objet
operação bloqueante — aguarda o fim do thread para passar à instrução seguinte

Vejamos uma primeira aplicação que ilustra a existência de um thread principal de execução, aquele no qual é executada a função Main de uma classe:


' utilização de threads
Imports System
Imports System.Threading

Public Module thread1
    Public Sub Main()
        ' inicialização do thread atual
        Dim main As Thread = Thread.CurrentThread
        ' exibição
        Console.Out.WriteLine(("Thread courant : " + main.Name))
        ' alteração do nome
        main.Name = "main"
        ' verificação
        Console.Out.WriteLine(("Thread courant : " + main.Name))
        ' loop infinito
        While True
            ' exibição
            Console.Out.WriteLine((main.Name + " : " + DateTime.Now.ToString("hh:mm:ss")))
            ' paragem temporária
            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 corretamente num thread
  • tem-se acesso às características desse thread através de Thread.CurrentThread
  • o papel do método Sleep. Aqui, o thread que executa Main entra em suspensão regularmente durante 1 segundo entre duas exibições.

8.2. Criação de threads de execução

É possível ter aplicações em que partes de código são executadas de forma «simultânea» em diferentes threads de execução. Quando se diz que os threads são executados simultaneamente, trata-se frequentemente de um uso incorreto da linguagem. Se a máquina tiver apenas um processador, como ainda é frequentemente o caso, os threads partilham esse processador: cada um dispõe dele, à vez, durante um breve instante (alguns milissegundos). É isso que dá a ilusão de paralelismo de execução. O tempo atribuído a um thread depende de vários fatores, incluindo a sua prioridade, que tem um valor por predefinição, mas que também pode ser definida por programação. Quando um thread dispõe do processador, utiliza-o normalmente durante todo o tempo que lhe foi atribuído. No entanto, pode libertá-lo antes do tempo:

  • entrando em espera por um evento (wait, join, suspend)
  • entrando em suspensão durante um período determinado (sleep)
  1. Um thread T é inicialmente criado pelo seu construtor
Public Sub New(ByVal start As ThreadStart)

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

Public Delegate Sub ThreadStart()

Uma construção clássica é a seguinte:

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

A função run, passada como parâmetro, será executada no arranque do thread.

  1. A execução do thread T é iniciada por T.Start(): a função [run], passada ao construtor de T, será então executada pelo thread T. O programa que executa a instrução T.start() não aguarda o fim da tarefa T: passa imediatamente para a instrução seguinte. Temos, assim, duas tarefas a serem executadas em paralelo. Muitas vezes, estas têm de poder comunicar entre si para saber em que ponto se encontra o trabalho comum a realizar. Este é o problema da sincronização das threads.
  1. Uma vez iniciado, o thread executa-se de forma autónoma. Parará quando a função start que está a executar tiver concluído o seu trabalho.
  1. É possível enviar determinados sinais à tarefa T:
    1. T.Suspend() indica-lhe que pare momentaneamente
    2. T.Resume() indica-lhe que retome o seu trabalho
    3. T.Abort() indica-lhe que pare definitivamente
  1. Também é possível aguardar o fim da sua execução através de T.join(). Trata-se de uma instrução bloqueante: o programa que a executa fica bloqueado até que a tarefa T tenha concluído o seu trabalho. É um meio de sincronização.

Analisemos o seguinte programa:


' opções
Option Strict On
Option Explicit On 

' espaços de nomes
Imports System
Imports System.Threading

Module thread2
    Public Sub Main()
        ' inicialização do thread atual
        Dim main As Thread = Thread.CurrentThread
        ' atribuir um nome à thread
        main.Name = "main"

        ' criação de threads de execução
        Dim tâches(4) As Thread
        Dim i As Integer
        For i = 0 To tâches.Length - 1
            ' criação do thread i
            tâches(i) = New Thread(New ThreadStart(AddressOf affiche))
            ' define-se o nome do thread
            tâches(i).Name = "tache_" & i
            ' inicia-se a execução do thread i
            tâches(i).Start()
        Next i
        ' fim da rotina
        Console.Out.WriteLine(("fin du thread " + main.Name))
    End Sub

    Public Sub affiche()
        ' exibição do início da execução
        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")))
        ' suspensão durante 1 s
        Thread.Sleep(1000)
        ' exibição do fim da execução
        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 mais 5 threads encarregados de executar o método estático affiche. 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 esclarecedores:

  • em primeiro lugar, verifica-se 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 sua execução antes deles. A operação
             ' inicia-se a execução do thread i
            tâches(i).Start()

inicia a execução do thread «tâscas[i]», mas, feito isso, a execução prossegue imediatamente com a instrução seguinte, sem aguardar o fim da execução do thread.

  • 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, neste caso, 6 threads e um processador. Irá distribuir o processador por estas 6 threads de acordo com regras que lhe são próprias.
  • Nos resultados, observa-se uma consequência do método Sleep. No exemplo, é o thread 0 que executa em primeiro lugar o método affiche. A mensagem de início de execução é apresentada e, em seguida, o método Sleep é executado, o que o suspende durante 1 segundo. Perde então o processador, que fica assim disponível para outro thread. O exemplo mostra que é o thread 1 que o irá obter. A thread 1 seguirá o mesmo percurso, tal como as outras threads. Quando o segundo de espera da thread 0 terminar, a sua execução pode recomeçar. O sistema atribui-lhe o processador e ela pode concluir a execução do método affiche.

Alteremos o nosso programa para concluir o método Main com as instruções:

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

A execução do novo programa resulta em:

fin du thread main

Os threads criados pela função Main não são executados. Trata-se da instrução

        Environment.Exit(0)

que causa isto: ela elimina todos os threads da aplicação e não apenas o thread Main. A solução para este problema consiste em fazer com que o método Main aguarde a conclusão da execução dos threads que criou antes de terminar a sua própria execução. Isto pode ser feito com o método Join da classe Thread:


         ' aguarda-se o fim da execução de todos os threads
        For i = 0 To tâches.Length - 1
            ' aguardando o fim da execução do thread i
            tâches(i).Join()
        Next i        'for
         ' fim da rotina
        Console.Out.WriteLine(("fin du thread " + main.Name))
        Environment.Exit(0)

Obtêm-se então 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. Importância dos threads

Agora que destacámos a existência de um thread por predefinição, aquele que executa o método Main, e que sabemos como criar outros, vamos debruçar-nos sobre a utilidade dos threads para nós e sobre a razão pela qual os apresentamos aqui. Existe um tipo de aplicações que se presta bem à utilização de threads: as aplicações cliente-servidor da Internet. Numa aplicação deste tipo, um servidor localizado numa máquina S1 responde aos pedidos de clientes localizados em máquinas remotas C1, C2, ..., Cn.

Utilizamos diariamente aplicações da Internet que correspondem a este esquema: serviços Web, correio eletrónico, consulta de fóruns, transferência de ficheiros... No esquema acima, o servidor S1 deve servir os clientes Ci de forma simultânea. Se tomarmos o exemplo de um servidor FTP (File Transfer Protocol) que fornece ficheiros aos seus clientes, sabemos que uma transferência de ficheiros pode, por vezes, demorar várias horas. É claro que está fora de questão que um cliente monopolize sozinho o servidor durante tanto tempo. O que se faz habitualmente é o servidor criar tantos threads de execução quantos forem os clientes. Cada thread fica então encarregado de atender a um cliente específico. Como o processador é partilhado ciclicamente entre todos os threads ativos da máquina, o servidor dedica algum tempo a cada cliente, garantindo assim a simultaneidade do serviço.

8.4. Acesso a recursos partilhados

No exemplo cliente-servidor acima referido, cada thread atende um cliente de forma amplamente independente. No entanto, as threads podem ter de cooperar para prestar o serviço solicitado ao seu cliente, nomeadamente no que diz respeito ao acesso a recursos partilhados. O esquema acima faz lembrar os balcões de uma grande administração, como uma estação de correios, por exemplo, onde, em cada balcão, um funcionário atende um cliente. Suponhamos que, de vez em quando, esses funcionários tenham de fazer fotocópias de documentos trazidos pelos seus clientes e que haja 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. A esta situação chama-se «acesso a um recurso partilhado» e, em informática, é bastante delicada de gerir. Vejamos o seguinte exemplo:

  • uma aplicação vai gerar n threads, sendo n passado como parâmetro
  • o recurso partilhado é um contador que deverá ser incrementado por cada thread gerado
  • No final da aplicação, o valor do contador é apresentado. Deveríamos, portanto, encontrar n.

O programa é o seguinte:


' opções
Option Explicit On 
Option Strict On

' utilização de threads
Imports System
Imports System.Threading

Public Class thread3
    ' variáveis de classe
    Private Shared cptrThreads As Integer = 0

    Public Overloads Shared Sub Main(ByVal args() As [String])
        ' manual de instruções
        Const syntaxe As String = "pg nbThreads"
        Const nbMaxThreads As Integer = 100

        ' verificação do número de argumentos
        If args.Length <> 1 Then
            ' erro
            Console.Error.WriteLine(syntaxe)
            ' paragem
            Environment.Exit(1)
        End If
        ' verificação da qualidade do argumento
        Dim nbThreads As Integer = 0
        Try
            nbThreads = Integer.Parse(args(0))
            If nbThreads < 1 Or nbThreads > nbMaxThreads Then
                Throw New Exception
            End If
        Catch
            ' erro
            Console.Error.WriteLine("Nombre de threads incorrect (entre 1 et " & nbMaxThreads & ")")
            ' fim
            Environment.Exit(2)
        End Try
        ' criação e geração de threads
        Dim threads(nbThreads - 1) As Thread
        Dim i As Integer
        For i = 0 To nbThreads - 1
            ' criação
            threads(i) = New Thread(New ThreadStart(AddressOf incrémente))
            ' denominação
            threads(i).Name = "tache_" & i
            ' início
            threads(i).Start()
        Next i
        ' aguarda a conclusão dos threads
        For i = 0 To nbThreads - 1
            threads(i).Join()
        Next i        ' affichage compteur
        Console.Out.WriteLine(("Nombre de threads générés : " & cptrThreads))
    End Sub

    Public Shared Sub incrémente()
        ' aumenta o contador de threads
        ' leitura do contador
        Dim valeur As Integer = cptrThreads
        ' acompanhamento
        Console.Out.WriteLine(("A " + DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a lu la valeur du compteur : " & cptrThreads))
        ' espera
        Thread.Sleep(1000)
        ' incremento do contador
        cptrThreads = valeur + 1
        ' acompanhamento
        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 relativa à geração de threads, que já foi estudada. Concentremo-nos, em vez disso, no método incrémente, utilizado por cada thread para incrementar o contador estático cptrThreads.

  1. O contador é lido
  2. o thread pára durante 1 s. Assim, perde o acesso ao processador
  3. o contador é incrementado

A etapa 2 existe apenas para forçar a thread a perder o processador. Este será atribuído a outra thread. Na prática, nada garante que uma thread não seja interrompida entre o momento em que vai ler o contador e o momento em que vai incrementá-lo. Existe o risco de perder o processador entre o momento em que se lê o valor do contador e aquele em que se escreve o seu valor incrementado em 1. Com efeito, a operação de incremento será objeto de várias instruções elementares ao nível do processador, que podem ser interrompidas. A etapa 2 de espera de um segundo existe, portanto, apenas para sistematizar esse 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

Ao analisar estes resultados, percebe-se claramente o que está a acontecer:

  • um primeiro thread lê o contador. Encontra o valor 0.
  • Pára durante 1 s, perdendo assim o controlo do processador
  • um segundo thread assume então o processador e também lê o valor do contador. Este continua a ser 0, uma vez que o thread anterior ainda não o incrementou. Este também fica parado durante 1 s.
  • Em 1 s, as 5 threads têm tempo para passar todas e ler o valor 0.
  • Quando forem reativados, um após o outro, irão incrementar o valor 0 que leram e escrever o valor 1 no contador, o que é confirmado pelo programa principal (Main).

De onde vem o problema? O segundo thread leu um valor errado porque o primeiro tinha sido interrompido antes de terminar o seu trabalho, que consistia em atualizar o contador na janela. Isto leva-nos ao conceito de recurso crítico e de secção crítica de um programa:

  • um recurso crítico é um recurso que só pode ser detido por um thread de cada vez. Neste caso, 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 thread durante a qual este acede a um recurso crítico. É necessário garantir que, durante essa secção crítica, ele seja o único a ter acesso ao recurso.

8.5. Acesso exclusivo a um recurso partilhado

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


         ' leitura do contador
        Dim valeur As Integer = cptrThreads
        ' em espera
        Thread.Sleep(1000)
        ' incremento do contador
        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, nenhuma outra thread deve poder executar esse mesmo código. A plataforma .NET oferece várias ferramentas para garantir o acesso unitário às 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 que acontece inicialmente), este é «atribuído» ao thread T1 que o solicitou. Se, pouco tempo depois, um thread T2 realizar a mesma operação, ficará bloqueado. Com efeito, um mutex só pode pertencer a um único thread. Será desbloqueado quando o thread T1 libertar o mutex M que detém. Assim, vários threads podem ficar bloqueados à espera do mutex M.
public void
ReleaseMutex()
A thread T1, que executa a operação M.ReleaseMutex(), abdica da posse do mutex M.Lorsque; a thread T1 perderá o processador, o sistema poderá atribuí-lo a uma das threads em espera do mutex M. Apenas uma delas o obterá por sua vez, ficando as outras em espera do M bloqueadas

Um mutex M gere o acesso a um recurso partilhado R. Um thread solicita o recurso R através de M.WaitOne() e devolve-o através de M.ReleaseMutex(). Uma secção crítica de código que só deve ser executada por um único thread de cada vez é um recurso partilhado. A sincronização da execução da secção crítica pode ser feita da seguinte forma:

M.WaitOne()
' apenas este thread entra aqui
' secção crítica
....
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 thread possa entrar na secção crítica; caso contrário, os threads que aguardam um mutex que nunca foi libertado nunca terão acesso ao processador. Além disso, é necessário evitar a situação de interbloqueio (deadlock) em que duas threads esperam uma pela outra. Consideremos as seguintes ações que se sucedem no tempo:

  • um thread T1 obtém a posse de um mutex M1 para ter acesso a um recurso partilhado R1
  • um thread T2 obtém a posse de um mutex M2 para aceder a um recurso partilhado R2
  • o thread T1 solicita o mutex M2. Fica bloqueado.
  • O thread T2 solicita o mutex M1. Está bloqueado.

Neste caso, os threads T1 e T2 estão à espera um do outro. Esta situação ocorre quando os 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 consiste em solicitar ambas as recursos simultaneamente através de um único mutex M. No entanto, isso nem sempre é possível se, por exemplo, implicar uma ocupação prolongada de um recurso dispendioso. Outra solução consiste em que um thread que possua M1 e não consiga obter M2 liberte então M1 para evitar o interbloqueio. Se aplicarmos na prática o que acabámos de ver no exemplo anterior, a nossa aplicação fica da seguinte forma:


' opções
Option Explicit On 
Option Strict On

' utilização de threads
Imports System
Imports System.Threading

Public Class thread4
    ' variáveis de classe
    Private Shared cptrThreads As Integer = 0    ' compteur de threads
    Private Shared autorisation As Mutex

    Public Overloads Shared Sub Main(ByVal args() As [String])
        ' manual de instruções
        Const syntaxe As String = "pg nbThreads"
        Const nbMaxThreads As Integer = 100

        ' verificação do número de argumentos
        If args.Length <> 1 Then
            ' erro
            Console.Error.WriteLine(syntaxe)
            ' paragem
            Environment.Exit(1)
        End If
        ' verificação da qualidade do argumento
        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

        ' inicialização da autorização de acesso a uma secção crítica
        autorisation = New Mutex

        ' criação e geração de threads
        Dim threads(nbThreads) As Thread
        Dim i As Integer
        For i = 0 To nbThreads - 1
            ' criação
            threads(i) = New Thread(New ThreadStart(AddressOf incrémente))
            ' atribuição de nomes
            threads(i).Name = "tache_" & i
            ' início
            threads(i).Start()
        Next i
        ' aguardar a conclusão dos threads
        For i = 0 To nbThreads - 1
            threads(i).Join()
        Next i
        ' exibição do contador
        Console.Out.WriteLine(("Nombre de threads générés : " & cptrThreads))
    End Sub

    Public Shared Sub incrémente()
        ' aumenta o contador de threads
        ' solicita-se autorização para entrar na secção crítica
        autorisation.WaitOne()
        ' leitura do contador
        Dim valeur As Integer = cptrThreads
        ' acompanhamento
        Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a lu la valeur du compteur : " & cptrThreads))
        ' espera
        Thread.Sleep(1000)
        ' incremento do contador
        cptrThreads = valeur + 1
        ' acompanhamento
        Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a écrit la valeur du compteur : " & cptrThreads))
        ' conceder autorização de acesso
        autorisation.ReleaseMutex()
    End Sub
End Class

Os resultados obtidos estão de acordo com o esperado:

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 por eventos

Consideremos a seguinte situação, por vezes designada por situação de produtores-consumidores.

  1. Temos uma tabela na qual alguns processos inserem 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 os seus dados na tabela.
  3. Os consumidores são iguais entre si, mas exclusivos: apenas um leitor de cada vez pode ler os dados depositados na tabela.
  4. Um consumidor só pode ler os dados da tabela quando um produtor os tiver depositado nela e um produtor só pode depositar novos dados na tabela quando os que aí se encontram tiverem sido consumidos.

Nesta exposição, é possível distinguir dois recursos partilhados:

    1. a tabela em modo de gravação
    2. a tabela de leitura

O acesso a estes dois recursos partilhados pode ser controlado por mutexes, tal como visto anteriormente, um para cada recurso. Assim que um consumidor obtiver a tabela em modo de leitura, deve verificar se existem efetivamente dados na mesma. Utilizar-se-á um evento para o notificar. Da mesma forma, um produtor que tenha obtido a tabela em modo de escrita deverá aguardar que um consumidor a tenha esvaziado. Também neste caso, utilizar-se-á um evento.

Os eventos utilizados farão parte da classe AutoResetEvent:

Image

Este tipo de evento é análogo a um valor booleano, mas evita esperas ativas ou semi-ativas. Assim, se o direito de escrita for controlado por um valor booleano peutEcrire, um produtor, antes de escrever, executará um código do tipo:

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 ocupa desnecessariamente o processador. No segundo, verifica o estado da variável booleana peutEcrire a cada 100 ms. A classe AutoResetEvent permite melhorar ainda mais a situação: o thread irá solicitar que seja acordado quando o evento que aguarda tiver ocorrido:

AutoEvent peutEcrire=new AutoResetEvent(false)        ' peutEcrire=false;
....
peutEcrire.WaitOne() ' le thread attend que l'évt peutEcrire passe à vrai

A operação

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

inicializa a variável booleana peutEcrire com o valor false. A operação

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

executada por um thread faz com que este avance se a variável booleana peutEcrire for verdadeira; caso contrário, fica bloqueado até que se torne verdadeira. Outra thread irá defini-lo como verdadeiro através da operação peutEcrire.Set() ou como falso através da operação peutEcrire.Reset().

O programa de produtores-consumidores é o seguinte:


' utilização de threads de leitura e escrita
' ilustra a utilização simultânea de recursos partilhados e de sincronização

' opções
Option Explicit On 
Option Strict On

' utilização de threads
Imports System
Imports System.Threading

Public Class lececr

    ' variáveis de classe
    Private Shared data(5) As Integer    ' ressource partagée entre threads lecteur et threads écrivain
    Private Shared lecteur As Mutex    ' variable de synchronisation pour lire le tableau
    Private Shared écrivain As Mutex    ' variable de synchronisation pour écrire dans le tableau
    Private Shared objRandom As New Random(DateTime.Now.Second)    ' un générateur de nombres aléatoires
    Private Shared peutLire As AutoResetEvent    ' signale qu'on peut lire le contenu de data
    Private Shared peutEcrire As AutoResetEvent

    Public Shared Sub Main(ByVal args() As [String])

        ' o número de threads a gerar
        Const nbThreads As Integer = 3

        ' inicialização dos indicadores
        peutLire = New AutoResetEvent(False)        ' on ne peut pas encore lire
        peutEcrire = New AutoResetEvent(True)        ' on peut déjà écrire

        ' inicialização das variáveis de sincronização
        lecteur = New Mutex         ' synchronise les lecteurs
        écrivain = New Mutex         ' synchronise les écrivains

        ' criação dos threads de leitura
        Dim lecteurs(nbThreads) As Thread
        Dim i As Integer
        For i = 0 To nbThreads - 1
            ' criação
            lecteurs(i) = New Thread(New ThreadStart(AddressOf lire))
            lecteurs(i).Name = "lecteur_" & i
            ' início
            lecteurs(i).Start()
        Next i

        ' criação dos threads de gravação
        Dim écrivains(nbThreads) As Thread
        For i = 0 To nbThreads - 1
            ' criação
            écrivains(i) = New Thread(New ThreadStart(AddressOf écrire))
            écrivains(i).Name = "écrivain_" & i
            ' início
            écrivains(i).Start()
        Next i

        'fim da mão
        Console.Out.WriteLine("fin de Main...")
    End Sub

    ' ler o conteúdo da tabela
    Public Shared Sub lire()
        ' secção crítica
        lecteur.WaitOne()        ' un seul lecteur peut passer
        peutLire.WaitOne()        ' on doit pouvoir lire

        ' leitura da tabela
        Dim i As Integer
        For i = 0 To data.Length - 1
            'espera de 1 s
            Thread.Sleep(1000)
            ' visualização
            Console.Out.WriteLine((DateTime.Now.ToString("hh:mm:ss") & " : Le lecteur " & Thread.CurrentThread.Name & " a lu le nombre " & data(i)))
        Next i

        ' já não é possível ler
        peutLire.Reset()
        ' é possível escrever
        peutEcrire.Set()
        ' fim da secção crítica
        lecteur.ReleaseMutex()
    End Sub

    ' escrever na tabela
    Public Shared Sub écrire()
        ' secção crítica
        ' apenas um escritor pode passar
        écrivain.WaitOne()
        ' é necessário aguardar a autorização de escrita
        peutEcrire.WaitOne()

        ' gravação na tabela
        Dim i As Integer
        For i = 0 To data.Length - 1
            'espera de 1 s
            Thread.Sleep(1000)
            ' exibição
            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

        ' já não é possível escrever
        peutEcrire.Reset()
        ' é possível ler
        peutLire.Set()
        'fim da secção crítica
        é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

É possível observar os seguintes pontos:

  • existe efetivamente apenas um leitor de cada vez, embora este perca o processador na secção crítica lire
  • existe efetivamente apenas um gravador de cada vez, embora este perca o processador na secção crítica écrire
  • um leitor só lê quando há algo para ler na tabela
  • um gravador só grava quando a tabela tiver sido lida na íntegra