Condições de concorrência e sincronização
Como vimos anteriormente, além de trocar dados entre si, processos e threads tem a necessidade de se sincronizarem também. A sincronização entre as várias tarefas garante a ordem de execução das etapas de um algoritmo e evita o que se chama de “condições de concorrência” ou, como em algumas traduções, “condições de corrida”.
As “condições de concorrência” ocorrem quando vários processos ou threads precisam acessar um mesmo recurso compartilhado (ou seja, “concorrem” pelo mesmo recurso) sem que sejam tomadas as precauções para evitar a interferência de uma tarefa sobre outra. Elas podem ocorrer nos mais variados níveis: seja no acesso a um contador em memória ou no controle de uma impressora.
No caso de threads, todo acesso a variáveis globais está sujeito a condições de concorrência, já que todos as threads de um processo tem acesso imediato as variáveis globais. É de responsabilidade do programador garantir que os acessos sejam feitos de forma independente, e para isso são utilizados os mecanismos de sincronização. Estes mecanismos de sincronização são usados para garantir que cada processo irá efetuar sua parte do processamento na hora certa, ou seja, quando os dados estiverem já estiverem disponíveis e nenhuma outra threads os estiver alterando.
Além de tratar as condições de concorrência, a sincronização também é utilizada para passar o trabalho de um processo para outro: assim, quando um processo termina de processar informações, ele avisa ao processo seguinte que terminou para que o trabalho possa continuar.
Vamos apresentar agora os mecanismos mais comuns para o tratamento dos problemas expostos acima:
Seções críticas e exclusão mútua: mutexes
Uma seção crítica é um trecho de um programa que não pode ser acessado simultaneamente por threads sob risco de gerar inconsistências nos dados. O caso mais comum é o de um contador que é lido, incrementado, e depois escrito de volta. Porém, o usual é que a seção crítica inclua um conjunto de variáveis interdependentes, como por exemplo uma fila de dados a serem tratados e um contador de quantos elementos ela contém. Neste caso a seção crítica é o trecho que vai desde antes do acesso à primeira variável até depois do último acesso à última variável.
Uma seção crítica é um trecho de um programa que não pode ser acessado simultaneamente por threads sob risco de gerar inconsistências nos dados. O caso mais comum é o de um contador que é lido, incrementado, e depois escrito de volta. Porém, o usual é que a seção crítica inclua um conjunto de variáveis interdependentes, como por exemplo uma fila de dados a serem tratados e um contador de quantos elementos ela contém. Neste caso a seção crítica é o trecho que vai desde antes do acesso à primeira variável até depois do último acesso à última variável.
Exclusão mútua é uma técnica que visa garantir que apenas uma thread irá acessar a seção crítica de cada vez (ou seja, um exclui o outro). Para implementar a exclusão mútua, utiliza-se uma estrutura chamada MUTEX (que é um acrônimo para MUTual EXclusion), que tem várias implementações (dependendo da linguagem de programação e do sistema operacional), mas funciona sempre da mesma forma:
- Define-se objeto do tipo MUTEX, que deve ser compartilhada por todos as threads que irão participar da operação;
- Quando a primeira thread entrar na seção crítica do programa, ele chama a operação EnterMutex do objeto; a thread agora pode acessar livremente os valores e estruturas dentro da seção crítica;
- Se uma outra thread tentar operação EnterMutex enquanto a primeira thread ainda estiver dentro da seção crítica, esta segunda será suspensa até que a primeira termine o acesso;
- A primeira thread termina o acesso e chama a função LeaveMutex passando a variável mutex definida;
- Neste momento, o sistema permite a entrada da segunda thread na seção crítica já que não há mais uma outra thread com o MUTEX.
Semáforos
Semáforos são estruturas bem parecidas com os MUTEXES, que podem ser utilizadas para controle de recursos e para sincronização. Antes de explicar como usamos os MUTEXES, vamos ver como eles funcionam.
Semáforos são estruturas bem parecidas com os MUTEXES, que podem ser utilizadas para controle de recursos e para sincronização. Antes de explicar como usamos os MUTEXES, vamos ver como eles funcionam.
Ao ser criado, é atribuído ao semáforo um valor inicial. Então, a partir deste ponto, duas operações podem ser efetuadas com semáforo: a primeira é Down (o valor é decrementado em 1). Caso este valor chegue a zero, a thread é colocada em espera até que o valor fique maior que zero.
A outra operação do semáforo é Up, que incrementa o valor em 1. Caso o valor anterior ao incremento seja zero, então a primeira thread que ficou suspensa quando o semáforo chegou a zero é liberada da espera e volta a executar.
Nota: todas as operações são implementadas de forma que não haja condições de concorrência, já que o próprio semáforo tem um contador a ser protegido.
Uma vez que o semáforo permite que mais que uma thread chame a operação Down, que seria análoga ao EnterMutex, fica claro que estas estruturas não servem para proteger variáveis compartilhadas. Porém, elas são usadas para solucionar duas classes distintas de problemas.
Controle de recursos
Imagine que uma determinada operação, por ser muito custosa, não deva ser executada por mais que 3 threads ao mesmo tempo sob risco de desestabilizar o sistema. Para solucionar este problema, podemos usar um semáforo para controlar o número de threads executando a tarefa simultaneamente.
Imagine que uma determinada operação, por ser muito custosa, não deva ser executada por mais que 3 threads ao mesmo tempo sob risco de desestabilizar o sistema. Para solucionar este problema, podemos usar um semáforo para controlar o número de threads executando a tarefa simultaneamente.
Basta inicializar o semáforo com o número máximo de threads que poderão efetuar a operação simultaneamente. Assim, quando uma thread precisar executar a tarefa, chama a operação Down, e de forma inversa, quando a thread terminar de executar a tarefa, chama a operação Up.
Desta forma, o semáforo garante mais threads executando a tarefa do que o valor inicial atribuído ao mesmo.
Sincronização entre produtores e consumidores de dados
Imagine um programa onde uma thread produz dados e os coloca numa fila (vamos chamá-lo de produtor).
Estes dados devem ser processados posteriormente por outra thread (que será chamado de consumidor), e o problema que temos é suspender a thread consumidor quando a fila estiver vazia.
Imagine um programa onde uma thread produz dados e os coloca numa fila (vamos chamá-lo de produtor).
Estes dados devem ser processados posteriormente por outra thread (que será chamado de consumidor), e o problema que temos é suspender a thread consumidor quando a fila estiver vazia.
Para fazer isso, tudo que temos que fazer é criar um semáforo com valor inicial de zero.
Cada vez que o produtor colocar um dado na fila, ele chama a operação Up, incrementando o semáforo e deixando registrado que existe mais um elemento a ser processado pelo consumidor.
Cada vez que o consumidor precisar de mais dados para processar, ele chama a operação Down logo antes de ler o dado da fila. Caso haja algum dado na fila, o consumidor irá continuar a operação imediatamente. Do contrário, ele irá ficar suspenso até que o produtor chame a operação Up indicando que existem novos dados a serem processados.
Obrigado e até a próxima.
Ariel Nigri
FONTE:WINCO SISTEMAS
Obrigado e até a próxima.
Ariel Nigri
FONTE:WINCO SISTEMAS
Nenhum comentário:
Postar um comentário