Skip to content

8. Os threads de execução

8.1. Introdução

Quando se inicia um aplicativo, ele é executado em um fluxo de execução chamado thread. A classe .NET, que modela um thread, é a classe System.Threading.Thread e possui a seguinte definição:

Image

Utilizaremos apenas algumas das propriedades e métodos dessa 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 por 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 término do thread para passar para a instrução seguinte

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


' uso 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")))
            ' parada temporária
            Thread.Sleep(1000)
        End While
    End Sub
End Module

Resultados na tela:

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 em um thread
  • é possível acessar as características desse thread por meio de Thread.CurrentThread
  • a função do método Sleep. Aqui, a thread que executa Main entra em suspensão regularmente por 1 segundo entre duas exibições.

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

É possível ter aplicativos em que trechos de código são executados de forma “simultânea” em diferentes threads de execução. Quando se diz que os threads são executados simultaneamente, muitas vezes comete-se um equívoco de linguagem. Se a máquina tiver apenas um processador, como ainda é comum, os threads compartilham esse processador: cada um deles tem acesso a ele, por sua vez, por um breve instante (alguns milissegundos). É isso que dá a ilusão de paralelismo de execução. O tempo concedido a um thread depende de vários fatores, entre os quais sua prioridade, que tem um valor padrão, mas que também pode ser definida por programação. Quando um thread dispõe do processador, ele normalmente o utiliza durante todo o tempo que lhe foi concedido. No entanto, ele pode liberá-lo antes do tempo:

  • entrando em espera por um evento (wait, join, suspend)
  • entrando em modo de suspensão por um período determinado (sleep)
  1. Um thread T é inicialmente criado por 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 ao iniciar o 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 término da tarefa T: ele passa imediatamente para a instrução seguinte. Temos, então, duas tarefas sendo executadas em paralelo. Frequentemente, elas precisam se comunicar entre si para saber em que ponto está o trabalho comum a ser realizado. Esse é o problema da sincronização de threads.
  1. Uma vez iniciado, o thread é executado de forma autônoma. Ele será interrompido quando a função start que ele está executando tiver concluído seu trabalho.
  1. É possível enviar certos sinais à tarefa T:
    1. T.Suspend() instrui-a a parar momentaneamente
    2. T.Resume() instrui-a a retomar seu trabalho
    3. T.Abort() instrui-a a parar definitivamente
  1. Também é possível aguardar o término de sua execução por meio de T.join(). Trata-se de uma instrução bloqueante: o programa que a executa fica bloqueado até que a tarefa T tenha concluído seu trabalho. Essa é uma forma de sincronização.

Vamos examinar o seguinte programa:


' 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
        ' atribuição de 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 da thread i
            tâches(i) = New Thread(New ThreadStart(AddressOf affiche))
            ' definindo 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 por 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 outros 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

Esses resultados são muito esclarecedores:

  • vemos, em primeiro lugar, que o início da execução de um thread não é bloqueante. O método Main iniciou a execução de 5 threads em paralelo e concluiu sua execução antes deles. A operação
             ' iniciando a execução da thread i
            tâches(i).Start()

inicia a execução do thread tarefas[i], mas, feito isso, a execução prossegue imediatamente com a instrução seguinte, sem aguardar o término da execução do thread.

  • Todas as threads criadas devem executar o método affiche. A ordem de execução é imprevisível. Mesmo que, no exemplo, a ordem de execução pareça seguir a ordem das solicitações de execução, não se pode tirar conclusões gerais a partir disso. O sistema operacional possui, neste caso, 6 threads e um processador. Ele distribuirá o processador entre essas 6 threads de acordo com regras próprias.
  • Nos resultados, observa-se uma consequência do método Sleep. No exemplo, é o thread 0 que executa primeiro o método affiche. A mensagem de início de execução é exibida e, em seguida, ele executa o método Sleep, que o suspende por 1 segundo. Ele então perde o processador, que fica disponível para outro thread. O exemplo mostra que é o thread 1 que o obterá. A thread 1 seguirá o mesmo caminho, assim como as outras threads. Quando o segundo de espera da thread 0 terminar, sua execução poderá ser retomada. O sistema lhe concede o processador e ela pode concluir a execução do método affiche.

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

         ' fim da rotina
        Console.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 isso: ela encerra todas as threads do aplicativo e não apenas a thread Main. A solução para esse problema é que o método Main aguarde o término da execução dos threads que ele criou antes de encerrar a si mesmo. Isso pode ser feito com o método Join da classe Thread:


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

Assim, obtêm-se 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 destacamos a existência de um thread padrão — aquele que executa o método Main — e que sabemos como criar outros, vamos nos deter na utilidade dos threads para nós e no motivo pelo qual os apresentamos aqui. Existe um tipo de aplicativo que se adapta bem ao uso de threads: os aplicativos cliente-servidor da Internet. Nesse tipo de aplicação, um servidor localizado em uma máquina S1 responde às solicitações de clientes localizados em máquinas remotas C1, C2, ..., Cn.

Todos os dias utilizamos aplicações da Internet que se enquadram nesse esquema: serviços da Web, e-mail, consulta de fóruns, transferência de arquivos... No esquema acima, o servidor S1 deve atender os clientes Ci simultaneamente. Se tomarmos o exemplo de um servidor FTP (Protocolo de Transferência de Arquivos) que fornece arquivos aos seus clientes, sabemos que uma transferência de arquivo pode, às vezes, levar várias horas. É claro que está fora de questão que um único cliente monopolize o servidor por todo esse tempo. O que se faz normalmente é que o servidor crie tantos threads de execução quantos forem os clientes. Cada thread fica então encarregado de atender a um cliente específico. Como o processador é compartilhado ciclicamente entre todos os threads ativos da máquina, o servidor dedica um pouco de tempo a cada cliente, garantindo assim a simultaneidade do serviço.

8.4. Acesso a recursos compartilhados

No exemplo cliente-servidor mencionado acima, cada thread atende a um cliente de forma amplamente independente. No entanto, as threads podem ser levadas a cooperar para prestar o serviço solicitado ao seu cliente, especialmente no que diz respeito ao acesso a recursos compartilhados. O esquema acima lembra os guichês de um grande órgão público, como uma agência dos correios, por exemplo, onde, em cada guichê, um funcionário atende um cliente. Suponhamos que, de vez em quando, esses funcionários precisem fazer fotocópias de documentos trazidos por seus clientes e que haja apenas uma fotocopiadora. Dois funcionários não podem usar a fotocopiadora ao mesmo tempo. Se o funcionário i encontrar a fotocopiadora sendo usada pelo funcionário j, ele terá que esperar. Essa situação é chamada de acesso a um recurso compartilhado e, na informática, é bastante delicada de se gerenciar. Vejamos o seguinte exemplo:

  • um aplicativo irá gerar n threads, sendo que n é passado como parâmetro
  • o recurso compartilhado é um contador que deverá ser incrementado por cada thread gerado
  • Ao final da execução do programa, o valor do contador é exibido. Portanto, deveríamos encontrar n.

O programa é o seguinte:


' opções
Option Explicit On 
Option Strict On

' uso 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)
            ' interrupção
            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))
            ' nomeação
            threads(i).Name = "tache_" & i
            ' inicialização
            threads(i).Start()
        Next i
        ' aguardando o término 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()
        ' incrementa 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. Vamos nos concentrar, em vez disso, no método incrémente, utilizado por cada thread para incrementar o contador estático cptrThreads.

  1. o contador é lido
  2. a thread fica parada por 1 s. Assim, ela perde o processador
  3. o contador é incrementado

A etapa 2 existe apenas para forçar a thread a perder o processador. Este será cedido a outra thread. Na prática, nada garante que uma thread não seja interrompida entre o momento em que ela lerá o contador e o momento em que o incrementará. Existe o risco de perder o processador entre o momento em que se lê o valor do contador e aquele em que se grava seu valor incrementado em 1. De fato, a operação de incremento será composta por várias instruções elementares no nível do processador, que podem ser interrompidas. A etapa 2, com um segundo de espera, 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 esses resultados, fica claro o que está acontecendo:

  • um primeiro thread lê o contador. Ele encontra 0.
  • Ele fica parado por 1 s, perdendo assim o controle do processador
  • um segundo thread, então, assume o processador e também lê o valor do contador. Ele ainda está em 0, já que o thread anterior ainda não o incrementou. Ele também fica parado por 1 s.
  • Em 1 s, as 5 threads têm tempo de passar todas e ler o valor 0.
  • Quando forem reativados, um após o outro, eles irão incrementar o valor 0 que leram e gravar o valor 1 no contador, o que é confirmado pelo programa principal (Main).

De onde vem o problema? O segundo thread leu um valor incorreto porque o primeiro foi interrompido antes de concluir seu trabalho, que era atualizar o contador na janela. Isso nos leva ao conceito de recurso crítico e de seção crítica de um programa:

  • um recurso crítico é um recurso que só pode ser mantido por um thread por vez. Aqui, o recurso crítico é o contador.
  • uma seção crítica de um programa é uma sequência de instruções no fluxo de execução de um thread durante a qual ele acessa um recurso crítico. É preciso garantir que, durante essa seção crítica, ele seja o único a ter acesso ao recurso.

8.5. Acesso exclusivo a um recurso compartilhado

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


         ' leitura do contador
        Dim valeur As Integer = cptrThreads
        ' em espera
        Thread.Sleep(1000)
        ' incremento do contador
        cptrThreads = valeur + 1

Para executar esse 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 exclusivo às seções críticas do código. Usaremos 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()
A thread T1, que executa a operação M.WaitOne(), solicita a posse do objeto de sincronização M. Se o mutex M não estiver sob a posse de nenhuma thread (o que ocorre inicialmente), ele é “concedido” ao thread T1 que o solicitou. Se, pouco depois, um thread T2 realizar a mesma operação, ele ficará bloqueado. De fato, um mutex só pode pertencer a uma thread. Ele será desbloqueado quando a thread T1 liberar o mutex M que está detendo. Assim, várias threads podem ficar bloqueadas aguardando o mutex M.
public void
ReleaseMutex()
A thread T1, que executa a operação M.ReleaseMutex(), libera a posse do mutex M.Lorsque; a thread T1 perderá o processador, o sistema poderá atribuí-lo a um dos threads em espera pelo mutex M. Apenas um deles o obterá por sua vez; os demais, que aguardam o mutex M, permanecerão bloqueados

Um mutex M gerencia o acesso a um recurso compartilhado R. Uma thread solicita o recurso R por meio de M.WaitOne() e o libera por meio de M.ReleaseMutex(). Uma seção crítica de código que deve ser executada por apenas uma thread por vez é um recurso compartilhado. A sincronização da execução da seção crítica pode ser feita da seguinte forma:

M.WaitOne()
' apenas este thread deve entrar aqui
' seção crítica
....
M.ReleaseMutex()

onde M é um objeto Mutex. É claro que nunca se deve esquecer de liberar um Mutex que se tornou desnecessário para que outra thread possa entrar na seção crítica; caso contrário, as threads que aguardam um mutex que nunca foi liberado nunca terão acesso ao processador. Além disso, é preciso evitar a situação de interbloqueio (deadlock) na qual duas threads aguardam uma à outra. Consideremos as seguintes ações que ocorrem sequencialmente no tempo:

  • um thread T1 obtém a posse de um mutex M1 para ter acesso a um recurso compartilhado R1
  • um thread T2 obtém a posse de um mutex M2 para acessar um recurso compartilhado R2
  • o thread T1 solicita o mutex M2. Ele fica bloqueado.
  • O thread T2 está solicitando o mutex M1. Ele está bloqueado.

Nesse caso, as threads T1 e T2 estão aguardando uma à outra. Essa situação ocorre quando os threads precisam de dois recursos compartilhados: o recurso R1, controlado pelo mutex M1, e o recurso R2, controlado pelo mutex M2. Uma solução possível é solicitar os dois recursos ao mesmo tempo por meio de um único mutex M. Mas isso nem sempre é possível se, por exemplo, isso implicar em uma ocupação prolongada de um recurso caro. Outra solução é que um thread que possua M1 e não consiga obter M2 libere, então, M1 para evitar o interbloqueio. Se colocarmos em prática o que acabamos de ver no exemplo anterior, nossa aplicação fica da seguinte forma:


' opções
Option Explicit On 
Option Strict On

' uso 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)
            ' interrupção
            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 seçã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))
            ' nomeação
            threads(i).Name = "tache_" & i
            ' inicialização
            threads(i).Start()
        Next i
        ' aguardando 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()
        ' incrementa o contador de threads
        ' solicita-se autorização para entrar na seçã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 permissã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, às vezes chamada de 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 por vez pode inserir seus dados na tabela.
  3. Os consumidores são iguais entre si, mas exclusivos: apenas um leitor por vez pode ler os dados armazenados na tabela.
  4. Um consumidor só pode ler os dados da tabela quando um produtor os tiver gravado nela, e um produtor só pode gravar novos dados na tabela quando os que já estão nela tiverem sido consumidos.

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

    1. a tabela para gravação
    2. a tabela em leitura

O acesso a esses dois recursos compartilhados pode ser controlado por mutexes, conforme visto anteriormente, um para cada recurso. Assim que um consumidor obtiver a tabela em modo de leitura, ele deve verificar se há realmente dados nela. Utilizaremos um evento para notificá-lo. Da mesma forma, um produtor que tenha obtido a tabela em modo de gravação deverá aguardar até que um consumidor a tenha esvaziado. Utilizaremos, mais uma vez, um evento.

Os eventos utilizados farão parte da classe AutoResetEvent:

Image

Esse tipo de evento é análogo a um booleano, mas evita esperas ativas ou semi-ativas. Assim, se o direito de gravação for controlado por um booleano peutEcrire, um produtor, antes de gravar, 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 sobrecarrega desnecessariamente o processador. No segundo, ele verifica o estado da variável booleana peutEcrire a cada 100 ms. A classe AutoResetEvent permite melhorar ainda mais a situação: o thread solicitará ser acordado quando o evento que ele aguarda ocorrer:

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 ela se torne verdadeira. Outra thread o definirá como verdadeiro por meio da operação peutEcrire.Set() ou como falso por meio da operação peutEcrire.Reset().

O programa de produtores-consumidores é o seguinte:


' uso de threads de leitura e gravação
' ilustra o uso simultâneo de recursos compartilhados e sincronização

' opções
Option Explicit On 
Option Strict On

' uso 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 serem geradas
        Const nbThreads As Integer = 3

        ' inicialização dos sinalizadores
        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
            ' inicialização
            lecteurs(i).Start()
        Next i

        ' criação das 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
            ' inicialização
            é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()
        ' seçã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)
            ' exibição
            Console.Out.WriteLine((DateTime.Now.ToString("hh:mm:ss") & " : Le lecteur " & Thread.CurrentThread.Name & " a lu le nombre " & data(i)))
        Next i

        ' não é mais possível ler
        peutLire.Reset()
        ' é possível gravar
        peutEcrire.Set()
        ' fim da seção crítica
        lecteur.ReleaseMutex()
    End Sub

    ' gravar na tabela
    Public Shared Sub écrire()
        ' seção crítica
        ' apenas um escritor pode passar
        écrivain.WaitOne()
        ' é preciso aguardar a autorização para gravar
        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

        ' não é mais possível gravar
        peutEcrire.Reset()
        ' é possível ler
        peutLire.Set()
        'fim da seçã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:

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