SlideShare uma empresa Scribd logo
1 de 29
Baixar para ler offline
THREADS: O PROBLEMA DOS LEITORES E
            ESCRITORES IMPLEMENTADO EM C#
Daniel Ramon Silva Pinheiro, Danilo Santos Souza, Maria de Fátima A. S.
                     Colaço, Rafael Oliveira Vasconcelos


RESUMO: Este artigo tem como objetivo apresentar a utilização de threads
através da implementação de um problema que as utilizam. O problema
utilizado é caso dos leitores e escritores, que modela o acesso a uma base de
dados sendo requisitada para operações de leitura e escrita de forma a seguir
alguns critérios visando a garantia da integridade dos dados da base.
Aborda-se também alguns conceitos básicos de threads para um melhor
entendimento da implementação e da solução do problema proposto.
Além dos conceitos básicos, também é abordado neste artigo, exemplos de
trechos de códigos programados na linguagem de programação C#, utilizando-
se dos recursos disponíveis para a manipulação de threads como: criar e iniciar
threads, sincronismo, prioridade, nomear, acordar e dormir, bloquear,
interromper e resumir ou recomeçar.
De forma prática e em conjunto com os recursos da linguagem já citada,
mostra-se a implementação da resolução do problema dos leitores e escritores
visando o estudo de threads não só na teoria.

PALAVRAS-CHAVE: Processo, Região Crítica, Sistema Operacional, Thread.


ABSTRACT: This article aims to present the use of threads through the
implementation of a problem using them. The problem used is the readers and
writers, which shapes access to a database being requested for operations of
reading and writing in order to follow certain criteria aimed at ensuring the
integrity of the data base.
It also approached some basic concepts of threads to a better understanding of
the implementation of the solution proposed.
In addition to the basic concepts, is also approached in this article, examples of
stretch of code programmed in the programming language C#, using the
resources available for the manipulation of threads as: create and start threads,
synchronization, priority, label, wake and sleep, block, interrupt and resume or
start over.
From a practical way and together with the resources of the language already
quoted, it is shown in the implementation of the resolution of the problem of
readers and writers seeking the study of threads not only in theory.

KEYWORDS. Critical Area, Operating System, Process, Thread
1. INTRODUÇÃO


      Devido    à   evolução    da    tecnologia,   principalmente   no    mundo
computacional, ocorreu a necessidade de novas formas de executar processos
nos sistemas operacionais, com o intuito de ganho em processamento.
      A partir dessas evoluções nos processos nasceu um conceito
caracterizado como thread. Mas a principio o que seria processo? O que seria
thread? O que thread tem haver com processamento?
      De forma básica um processo é um programa em execução e uma
thread é a execução de parte de um processo. Pelo fato de thread ser parte de
um processo, possui o mesmo espaço de endereçamento compartilhando uma
mesma região de memória podendo assim um processo ter uma ou várias
threads. É ai que entra o poder da thread com base no ganho de
processamento, principalmente em ambientes multiprocessados. Os processos
podem ser independentes, onde cada processo executa sem a necessidade de
compartilhamento    de   variável,    e   concorrente   que   ao   contrário   dos
independentes os processos compartilham uma ou mais variáveis, onde essa
região compartilhada se caracteriza como região critica. Vale ressaltar também
que vários processos podem tentar acessar a mesma região critica e o
resultado depender da ordem em que eles são executados, definindo a idéia de
condição de corrida. Exemplos e maiores detalhes das situações citadas
anteriormente serão abordados ao decorrer do trabalho.
      O uso do compartilhamento de variável faz com que aconteçam alguns
problemas no uso das threads pelo fato das mesmas herdarem algumas
propriedades dos processos. Existem diversos problemas existentes no uso
das threads. Demonstraremos um problema especifico que é o caso do
problema dos leitores e escritores.
      O problema dos leitores e escritores de forma simples é um problema
onde se tem uma região critica onde threads podem ler (somente querer saber
o que consta na região critica) ou escrever (querer alterar valor na região
critica), levando em considerações alguns critérios. O mesmo será descrito de
forma mais detalhada no trabalho com o uso de exemplos e apresentando a
solução do mesmo.
Estamos falando de thread, computador, processamento, processos,
mas como implementar threads no mundo computacional? Qual linguagem
utilizar?
       Atualmente existem diversas linguagens com diversos recursos para
criarmos a idéia de thread no mundo computacional. Com ênfase na linguagem
C# alguns recursos serão demonstrados junto com exemplos de código e
formas de como usar esse recursos.
       Demonstrado uma idéia prática de thread em conjunto com os recursos
da linguagem C# o caso do problema dos leitores e escritores foi implementado
na linguagem já citada, existindo assim um tópico exclusivo apresentando o
código da implementação e para facilitar o entendimento o uso de comentários
no código.
       Enfim tudo isso citado junto com mais alguns complementos serão
apresentados neste artigo, com o objetivo de demonstrar o uso das threads
desde a parte teórica até a parte prática, finalizando o artigo com uma
conclusão retratando a opinião dos autores com relação ao tema abordado.


2. THREADS


       Literalmente thread significa, em português, linha de execução.
Conceitualmente falando, thread é uma forma de um processo dividir-se em
duas ou mais tarefas que podem ser executadas simultaneamente. Podem
porque nos hardwares equipados com múltiplos núcleos, as linhas de execução
de uma thread, podem ser executadas paralelamente, uma em cada núcleo, já
nos hardwares com um único núcleo, cada linha de execução é processada de
forma aparentemente simultânea, pois a mudança entre uma linha e outra é
feita de forma tão rápida que para o usuário isso está acontecendo
paralelamente.
       Para o progresso deste artigo que trata de threads é de fundamental
importância uma breve diferenciação das threads e dos processos, uma vez
que os dois são distintos, mas semelhantes. Então, o que seria um processo?
       Processo, na área da computação é um módulo executável único que é
executado concorrentemente com outros módulos executáveis. Onde um
módulo executável é um conjunto de instruções de um programa que devem
ser seguidos para a conclusão do mesmo.
      Para   exemplificar, existem os sistemas operacionais           multitarefa
(Windows ou Linux, por exemplo) que executam vários processos que rodam
concorrentemente com os outros processos para que tenham suas linhas de
código executadas pelo processador. Além de também poderem rodar
simultaneamente com outros processos interagindo para que a aplicação
ofereça um melhor desempenho e confiabilidade.
      Processos são módulos separados e carregáveis. Threads, não podem
ser carregados, eles são iniciados dentro de um processo, onde um processo
pode executar várias threads ao mesmo tempo. Funcionam como se existisse
vários processos internos ao processo pai, o qual gera outros processos. Aos
processos gerados pelo pai, denominam-se processos filhos. Estes podem
rodar ao mesmo tempo seguindo algumas regras para que não ocorram
conflitos internos. Estes conflitos internos podem variar desde uma paralisação
total ou parcial do sistema até inconsistência de valiosas informações.
      No universo dos modelos de processos existem dois conceitos
independentes de como enxergá-los, são eles o agrupamento de recursos e a
execução.
      O primeiro é um modo de ver um processo, ele apresenta um espaço de
endereçamento que possui um código e os dados de programa, e talvez alguns
outros recursos alocados, como alguns arquivos abertos, informações entre
contabilidades, processos filhos, enfim, o que interessa é que agrupar todos
eles em forma de processos facilitará o gerenciamento destes recursos.
      O segundo é denominado thread de execução, que normalmente é
abreviado simplesmente para thread. Ele possui um contador de programa, o
qual manterá o controle de qual instrução da thread deverá ser executada em
seguida pelo núcleo, possui registradores com suas variáveis de trabalho
atuais, possui uma pilha estruturada pelo conjunto de procedimentos
chamados, mas ainda não concluídos, a qual informa o histórico da execução.
      Os dois conceitos citados acima são importantes, pois delimitam os
conceitos entre threads e processos, que apesar de semelhantes, são
conceitos diferentes. A característica que os threads acrescentam ao conceito
de processos é a permissão de múltiplas execuções ocorrerem em um mesmo
processo de forma independente uma das outras.
      Existem sistemas que suportam apenas uma única thread em execução
por vez e os que suportam mais de uma por vez, denominados
de monothread e multithread respectivamente.Então, qual seria a diferença de
ter várias threads sendo executadas em um único processo (multithread), e
vários processos sendo executados em um computador?
      No primeiro caso (multithread), os várias threads estão compartilhando
um mesmo espaço de endereçamento na memória, assim como os recursos
alocados pelo processo criador da thread. No segundo caso, os processos
compartilham um espaço físico de memória.
      É importante citar que a existência de recursos compartilhados necessita
de um controle para que não haja nenhum tipo de conflito que possa
embaralhar tanto a vida do usuário como a integridade das informações
processadas.


2.1. TIPOS DE THREADS


      Existem dois tipos de implementações de threads: thread usuário e
thread núcleo.
      O primeiro, thread de usuário, como o próprio nome já diz, tem por
principal característica o fato de deixar todos os pacotes e controles de threads
no espaço do usuário, de forma que o núcleo não seja informado sobre eles,
logo as threads serão tratadas de forma simples (monothread). Mesmo que
existam vários núcleos, ou seja, vários processos sendo executados ao mesmo
tempo (multiprocessamento), onde somente os processos é que serão
executados paralelamente e não as threads, pois estas estão alocados dentro
dos processos.
      Uma vantagem das threads de usuário está na sua versatilidade, pois
elas funcionam tanto em sistemas que suportem ou não o uso de threads. Uma
vez que sua implementação estará interna ao processo criador da thread, o
sistema operacional não poderá interferir nesta criação, desta forma o sistema
executará a thread como se fosse apenas mais uma linha de execução do
processo. De fato, o processo criador deverá possuir todas as características
de gerenciamento e confiabilidade de threads, que estão presentes nas tabelas
dos núcleos dos sistemas que ofereçam o suporte aos threads.
      Um thread de usuário possui desempenho melhor, mas existem alguns
problemas, como por exemplo, uma chamada ao sistema de bloqueio. Se a
thread executar esta chamada, ela para todas as outras threads, porém com o
uso de threads chamadas desse tipo são muito comuns, pois elas permitem o
controle das threads. Seria contraditório realizar uma chamada de bloqueio,
que pare todos as threads para permitir que uma outra delas possa ser
executada. De fato nenhuma thread jamais seria executada, até que fosse
desbloqueada.
      Outro problema com a utilização de thread é a posse do núcleo. Uma
vez que se inicie uma thread do tipo usuário, ela ficará sendo executada até
que, por uma linha de comando própria ela libere o núcleo para outras threads
do processo.
      No entanto, existem soluções para os problemas mencionados acima,
porém são complicadas de serem implementadas, o que torna o código
bastante confuso.
      O segundo tipo, thread de núcleo, é perceptível logo de início que o
núcleo sabe da existência das threads e que ele será o gerenciador das
mesmas. Neste caso, o processo não precisará de nenhuma tabela para
gerenciar as threads, o núcleo se encarregará de tudo, sendo necessário ao
processo apenas a realização das chamadas que quiser ao núcleo para a
manipulação de suas threads.
      Estas chamadas ao sistema possuem um custo maior se comparadas
com as chamadas que um sistema de threads de usuário realiza. Para
amenizar este custo, os sistemas utilizam-se da ‘reciclagem’ de threads, desta
forma, quando uma thread é destruída, ela é apenas marcada como não
executável, sem afetar sua estrutura. Desta forma a criação de uma nova
thread será mais rápida, visto que sua estrutura já esta montada, bastando
apenas a atualização de suas informações.
      Uma vantagem da thread de núcleo é que se uma thread de um
processo for bloqueado, as outras threads que forem gerados por este mesmo
processo poderão dar continuidade às suas linhas de execução, sem a
necessidade da primeira thread concluir suas linhas de execução.
Existem também as implementações de threads híbridas, neste caso,
tenta-se combinar as vantagens das threads de usuário e com as de núcleo.


2.2. COMUNICAÇÃO ENTRE THREADS


      Com freqüência os processos precisam trocar informações entre si para
continuar suas linhas de execução. Quando se trata de threads isso é um
pouco mais fácil, pois elas compartilham um espaço de endereçamento
comum, entretanto ainda é necessário um controle para evitar embaraços entre
elas. Esses embaraços geralmente ocorrem quando elas acessam uma
variável compartilhada ao mesmo tempo, ou seja, dentro de uma região crítica.


2.2.1. Região crítica


       Em poucas palavras, região crítica é uma região de memória
compartilhada que acessa um recurso que está compartilhado e que não possa
ser acessado concorrentemente por mais de uma linha de execução. A região
crítica, como o próprio nome já diz, por ser uma área crítica necessita de
cuidados para que não hajam problemas futuros devido à má utilização da
mesma.
       Para entender melhor região crítica é bom ter em mente o que seria
Condição de disputa. Esta consiste em um conjunto de recursos que deve ser
compartilhado entre processos no qual, em um mesmo intervalo tempo, dois ou
mais processos tentem alocar uma mesma parte de um mesmo recurso para
poder utilizá-lo. Nesta hora que ocorre o problema do controle de disputa.
       Para melhor entendimento deste problema, imagine duas threads, A e
B, e um recurso compartilhado que permita o acesso das duas threads ao
mesmo tempo, de forma que sempre que este recurso for utilizado seja emitido
um aviso informando que ocorreu tudo bem. Agora vamos supor que as
threads A e B entrem na região compartilhada para utilizá-la quase que ao
mesmo tempo. Então, a thread A chega primeiro e marca na região
compartilhada para ser a próxima a utilizá-la, mas antes que ela a utilize, o
sistema operacional tire a sua posse de núcleo e a passa para a thread B.
Então a thread B marca o recurso compartilhado como sendo ele o próximo a
utilizá-lo, o utiliza e recebe sua confirmação do recurso informando que ocorreu
tudo bem. Após isso a thread B libera o núcleo e o sistema o passa para a
thread A, que pensa ser a próximo a utilizar o recurso compartilhado e fica
aguardando a mensagem do recurso informando que ocorreu tudo bem.
Entretanto o processo A ficará eternamente esperando pela resposta do
recurso, mas ela nunca chegará.
         Com base nisto é possível perceber o problema de condição de disputa
e também é caracterizar a região crítica. Que no caso do exemplo anterior seria
a área do recurso que aloca o próximo processo ou thread a utilizá-lo.
         Para resolver estes tipos de problemas e muitos outros tipos que
envolvam regiões compartilhadas é preciso encontrar uma forma de bloquear
que outros processos usem uma área compartilhada que esteja sendo usada
até que ela seja liberada pelo processo que a esteja utilizando. A essa solução
denomina-se exclusão mútua e será o assunto abordado no próximo tópico.


2.2.2. Exclusão mútua


        Como dito anteriormente, exclusão mútua é uma solução encontrada
para evitar que dois ou mais processos ou threads tenham acesso
simultaneamente a alguma região crítica de algum recurso que esteja
compartilhado.
        Existem quatro condições que devem ser satisfeitas para que os
processos e threads concorrentes à mesma região crítica sejam executados de
forma eficiente e corretamente. São elas:
        1)    Nunca dois processos podem estar simultaneamente em suas
regiões críticas;
        2)    Nada pode ser afirmado sobre a velocidade ou sobre o número de
CPUs;
        3)    Nenhum processo executando fora de sua região crítica pode
bloquear outros processos;
        4)    Nenhum processo deve esperar eternamente para entrar em sua
região crítica;
Existem várias formas para se realizar a exclusão mútua de forma que
se um processo estiver utilizando a região crítica, nenhum outro processo
poderá utilizar esta região para que não ocorram problemas.


2.3. PROBLEMAS COM O USO DE THREADS


        Como dito anteriormente, as threads apresentam alguns problemas.
Alguns deles já foram passados implicitamente com as abordagens anteriores,
como os da região crítica e os da dificuldade de implementação, por exemplo.
Entretanto existem mais um leque de problemas relacionados a threads e são
deles que este artigo tratará de explicar quais são eles agora.


                 Lock //Variável compartilhada. Indica se a região crítica está
                 //liberada.

                 While (true) do {

                         If (Lock = 0) {

                               Lock = 1;

                               //Região_Crítica

                               Lock = 0;

                        }

                 }

               Quadro 1. Pseudocódigo do funcionamento das variáveis de
                                     travamento


      Existem várias tentativas de contornar os problemas com os threads, um
deles é utilização de variáveis de impedimento (Lock), a qual está representada
no Quadro 1. Esta é uma solução via software do usuário e não pelo sistema
operacional.
      De acordo com o Quadro 1, a variável Lock inicialmente contem o valor
0. Sempre que algum processo ou thread tenta entrar na região crítica ele testa
se lock é 0. Caso afirmativo, o processo ou thread altera esta variável para 1 e
então entra na região crítica. Caso negativo o processo ou thread entrará em
um laço sem fazer nada até que a variável esteja contenha o valor 0.
       Apesar de parecer uma boa solução, ela não consegue satisfazer
sempre as quadro condições da exclusão mútua. Para provar isso, suponha
duas threads, A e B, que queiram acessar uma mesma região crítica. A thread
A testa se a variável Lock é 0. Como é o estado inicial, Lock é 0. Mas
justamente neste ponto, o sistema operacional toma a posse do núcleo de A e
o entrega para B. Logo, B também verifica que a variável Lock permanece 0,
uma vez que A ainda não entrou e alterou para 1. Então como as duas threads
verificaram que Lock é 0, podem entrar na região crítica, e neste caso ocorrerá
problemas.
       Outra suposição com os mesmos parâmetros de entrada é supor que a
thread A entre da região e antes que saia e modifique a variável Lock para 0,
de um erro e trave. Desta forma, como o thread B ainda não entrou na região
crítica, ele nunca entrará, pois a variável Lock permanecerá sempre como 1.
Este mesmo caso ocorre com outra tentativa de solução que se da através da
utilização de semáforos.
       Existem vários outros problemas relacionados às threads. Este foi
apenas um deles e serviu como exemplo para que se possa entender a
complexidade da programação com a utilização de threads, pois apesar do
pseudocódigo do Quadro 1 ser uma solução ser simples, verifica-se que ele
não satisfaz as quatro condições da exclusão mútua, e continua sem resolver
os problemas complexos das threads.
       Outros problemas de importante citação são o deadlock e o starvation.
       O primeiro ocorre quando uma thread está aguardando a liberação de
um recurso compartilhado, que por sua vez, possui uma outra thread
aguardando a liberação de outro recurso compartilhado da primeira thread.
Desta forma elas ficarão eternamente paradas, uma esperando pela outra.
Uma analogia a este problema é imaginar uma rua estreita, na qual só entra
um carro por vez. Supondo que entrem dos dois lados duas fileiras enormes de
carros, um atrás do outro, eles ficarão travados, pois não conseguirão ir para
frente ou para trás.
O segundo, conhecido como inanição, ocorre quando um processo ou
thread nunca é executado, pois processos ou threads de maior importância
sempre tomam a posse do núcleo fazendo com que os de menores prioridades
nunca sejam executados.
      A diferença entre o deadlock e o starvation é que o starvation ocorre
quando os programas rodam indefinidamente, ao contrário do deadlock, que
ocorre quando os processos permanecem bloqueados, dependendo da
liberação dos recursos por eles alocados.
      Como já mencionado, é importante salientar a dificuldade de se
programar ao utilizar-se de thread devido a sua complexidade, uma vez que
uma simples desatenção do programador pode ocasionar vários problemas.
Ainda há a desvantagem do debug com threads, que é mais complicado, pois
como pode existir mais de uma thread em execução pelo programa, o
programador não saberá qual é a thread mostrada pelo compilador no modo
debug.


3. APRESENTANDO O PROBLEMA DOS LEITORES E ESCRITORES

      As dependências de dados na execução de processos ou threads
caracterizaram diversos tipos de problemas. Um deles é o problema conhecido
como problema dos leitores e escritores.
      O problema dos Leitores e Escritores modela o acesso a uma base de
dados, onde basicamente alguns processos ou threads estão lendo os dados
da região crítica, somente querendo obter a informação da região crítica, que é
o caso dos leitores, e outros processos ou threads tentando alterar a
informação da região crítica, que é o caso dos escritores.
      Analisando uma situação de um banco de dados localizado em um
servidor, por exemplo, temos situações relacionadas ao caso do problema dos
leitores e escritores. Supondo que temos usuários ligados a este servidor
querendo ler dados em uma tabela chamada Estoque, a princípio todos os
usuários terão acesso a esses dados. Supondo agora usuários querendo
atualizar na mesma tabela de Estoque, informações de vendas realizadas, de
fato esses dados serão atualizados. Mas para organizar esses acessos tanto
de atualização, quanto leitura no banco de dados algumas políticas são
seguidas, o mesmo acontecerá no problema dos leitores e escritores.
      As políticas seguidas no caso dos leitores e escritores para acesso a
região critica são as seguintes: processos ou threads leitores somente lêem o
valor da variável compartilhada (não alteram o valor da variável compartilhada),
podendo ser de forma concorrente; processos ou threads escritores podem
modificar o valor da variável compartilhada, para isso necessita de exclusão
mutua sobre a variável compartilhada; durante escrita do valor da variável
compartilhada a operação deve ser restrita a um único escritor; para a
operação de escrita não se pode existir nenhuma leitura ocorrendo, ou seja,
nenhum leitor pode estar com a região critica bloqueada; em caso de escrita
acontecendo, nenhum leitor conseguirá ter acesso ao valor da variável.
      Continuando a análise do banco de dados e seguindo as políticas dos
leitores e escritores têm as seguintes situações: vários usuários consultando a
tabela Estoque sem alterá-la; para um usuário atualizar uma venda é
necessário que não se tenha nenhum usuário consultando a tabela de estoque;
quando um usuário estiver atualizando a venda, nenhum outro usuário pode
atualizar ao mesmo tempo; se o usuário iniciar uma consulta e estiver
ocorrendo uma atualização o mesmo irá esperar a liberação da atualização.
      Por estarmos falando de um problema computacional, então como
resolvermos isto computacionalmente? Segue abaixo um pseudocódigo nos
quadros 2, 3 e 4 para se ter uma noção da solução:


 “semaphore mutex = 1;      // controla acesso a região critica
 semaphore db = 1;          // controla acesso a base de dados
 int rc = 0;                // número de processos lendo ou querendo ler”
 Tanenbaum [10]

                         Quadro 2.Variáveis do pseudocódigo
“void reader(void)
 {
        while(TRUE) {                // repete para sempre
               down(&mutex);         // obtém acesso exclusivo a região critica
               rc = rc + 1;          // um leitor a mais agora
              if (rc == 1) down(&db); //se este for o primeiro leitor bloqueia a
 //base de dados
               up(&mutex)            // libera o acesso a região critica
               read_data_base(); //acesso aos dados
               down(&mutex);         // obtém acesso exclusivo a região critica
               rc = rc -1;           // menos um leitor
               if (rc == 0) up(&db); // se este for o último leitor libera a base de
 //dados
               up(&mutex)            // libera o acesso a região critica
               use_data_read();      // utiliza o dado
        }
 }”
 Tanenbaum [11]



                              Quadro 3. Procedimento do leitor


Procedimento do Escritor

 void writer(void)
 {
        while (TRUE) {              // repete para sempre
               think_up_data();     // região não critica
               down(&db);           // obtém acesso exclusivo
               write_data_base(); // atualiza os dados
               up(&db);             // libera o acesso exclusivo
        }
 }”
 Tanenbaum [11]
Quadro 4.Procedimento do escritor


         Fazendo uma análise relacionada ao problema, enfim levando em
consideração políticas, e a solução apresentada, conseguimos evitar a questão
da espera ocupada que é um dos maiores problemas na comunicação de
processos ou threads, tendo assim um bom processamento. Mas se pode notar
que pode ocorrer à situação de se ter um leitor e bloquear a região critica. Se
sempre chegarem leitores, aumentando assim o número de leitores, e existir
um escritor esperando para realizar sua operação de escrita, o escritor pode
chegar a não ser executado pelo grande número de leitores estarem sempre
com a região critica bloqueada levando a uma situação caracterizada como
starvation.


4. THREADS NO C#


         A linguagem de programação C#, assim como as atuais linguagens de
programação de alto nível, oferece recursos que possibilitam a criação de
programas com processamento paralelo com uso das threads. O C# provê
recursos como criação de threads, sincronização e exclusão mutua.


4.1. CRIANDO THREADS E AS INICIANDO


         Antes de começar a programar utilizando threads, é preciso adicionar o
namespace System.Threading. Feito isso, é possível dispor dos recursos
oferecidos pela linguagem C#.
         Para criar uma thread basta informar uma nova variável do tipo Thread
passando no construtor o delegado que informa qual método será executado
pela thread. Feito isso, a thread já está pronta para ser iniciada, sendo preciso
chamar o método Start para começar a execução como mostrado nos quadros
5 e 6.
ThreadStart delegado = new ThreadStart(metodo);

              Thread t = new Thread (delegado);

              t.Start();


                           Quadro 5.Criando e iniciando uma thread


              class Ola_Mundo {

                  static void Main()
                  {

                      Thread t = new Thread(new ThreadStart(Imprime));

                      t.Start();

                  }

                  static void Imprime()
                  {
                     Console.WriteLine("Ola Mundo!");
                  }
              }


                                   Quadro 6.Programa Olá Mundo




4.2. SINCRONIZAÇÃO ENTRE THREADS


      A forma mais fácil de sincronizar a execução das threads é utilizando o
método Join. Este método faz o bloqueio do programa até que a thread seja
executada por completo, sendo então liberado para prosseguir com as demais
instruções.
class Ola_Mundo {

                          static void Main()
                          {

                          Thread t = new Thread(new
                      ThreadStart(Imprime));

                              t.Start();
                              t.Join();
                              Console.WriteLine("Fim do programa.");

                          }

                          static void Imprime()
                          {
                             Console.WriteLine("Ola Mundo!");
                          }
                      }


                              Quadro 7.Uso do método Join


      O uso do método Join garante que só será informado o fim do programa
quando for impresso na tela a frase Ola Mundo pela thread. Este recurso é
muito útil quando o programa só pode continuar sua execução após o fim da
thread.
      Esta é uma forma bastante simples da manter a sincronização entre
threads, porém anula a execução paralela, principal motivo para o uso de
threads. Outra forma de manter a sincronização entre as threads é utilizar
bloqueios, como lock, mutex ou semáforos.
      A forma de bloqueio lock é a mais simples e permite bloquear um bloco
de código com exclusão mutua, evitando assim a condição de corrida. Por ser
a forma mais simples de realizar um bloqueio, também é mais rápida que as
demais. Caso outra thread tente realizar o bloqueio de um objeto que se
encontra bloqueado, a thread será bloqueada pelo sistema operacional e só
poderá continuar quando o objetivo for liberado, como mostrado no quadro 8.
lock (contador) {

                            contador++;

                             Console.WriteLine(contador);

                      }


                     Quadro 8.Bloqueando uma região do código


       A classe mutex, mostrada no quadro 9, funciona parecida com o
bloqueio lock, entretanto por não realizar o bloqueio por blocos de código,
permite que tanto o bloqueio como o desbloqueio seja realizado em diferentes
regiões do código com uso dos métodos WaitOne e ReleaseMutex. A tentativa
de bloqueio de um objeto já bloqueado é análoga a forma anterior.


                      mutex.WaitOne();

                      contador++;

                      Console.WriteLine(contador);

                      mutex.ReleaseMutex();


                    Quadro 9.Classe mutex para exclusão mútua


      Os semáforos são uma forma mais completa de realizar bloqueio. A
classe Semaphore é uma extensão da classe mutex. Como principais
características permitem que um ou mais processos entrem na região crítica e
que o bloqueio realizado por uma thread possa ser desfeito por outra thread,
recurso que pode ser útil em determinados problemas, como no problema dos
leitores e escritores. Abaixo é mostrado como usar a classe Semaphore.
semaforo.WaitOne();

                       contador++;

                       Console.WriteLine(contador);

                       semaforo.Release();


                   Quadro 10.Exemplo de uso da classe semáforo


      O C# oferece ainda a classe Monitor que provê outras funcionalidades
como sinalizar e esperar por uma sinalização de outra thread. Os comandos
são Wait e Pulse ou PulseAll. Outro recurso interessante é o TryEnter que
como o próprio nome diz, tenta obter o acesso ao objeto, caso não seja
possível retorna o valor false. O método Enter funciona de maneira semelhante
aos já mencionados. Vale mencionar que a classe Monitor, segundo Jeffrey
Richter, é 33 vezes mais rápida que a classe Mutex por ser implementada pela
Common Language Runtime (CLR) e não pelo sistema operacional, além disso
a classe Mutex permite sincronização entre processos.


4.3. OUTROS RECURSOS


      Ainda é possível escolher a prioridade da thread, informar se a mesma é
uma thread de plano de fundo, dar um nome, adormecer por um tempo
determinado, suspender, retomar, interromper e abortar uma thread.
      Assim como os processos do sistema operacional, as threads no C# têm
uma prioridade padrão, mas que pode facilmente ser alterada pelo
programador, bem como informar se a thread deve executar em segundo
plano, ou background. Os métodos são Priority e IsBackground. Vale lembrar
que os possíveis valores de prioridade de uma thread são Lowest,
BelowNormal, Normal, AboveNormal e Highest, sendo normal a prioridade
padrão.Esses valores são uma enumeração pertencentes ao namespace
System.Threading.ThreadPriority.
      Os métodos para suspender, interromper e abortar uma thread parecem
confusos, contudo têm suas diferenças. Quando suspensa (Suspend), a thread
é bloqueada, mas há a possibilidade da mesma ser retomada (Resume). Este
recurso deve ser usado com cautela, pois uma thread suspensa pode manter
bloqueado um objeto até que seja re-iniciada, em uma condição mais crítica
pode levar a um deadlock. Os métodos para interromper (Interrupt) e abortar
(Abort) a thread finalizam permanentemente a execução, lançando as
exceções        ThreadInterruptedException     e      ThreadAbortException,
respectivamente.
      A diferença básica entre interromper e abortar uma thread está no
momento em que a thread será finalizada. Interrompendo, a thread só será
finalizada quando for bloqueada pelo sistema operacional, já abortando será
finalizada imediatamente.
      O problema que pode acontecer ao suspender uma thread, ou seja,
bloquear e não desbloquear, também pode ocorrer quando ela é abortada ou
interrompida. Isso acontece porque a thread é finalizada por meio de uma
exceção lançada que ocasiona o fim da thread. No quadro 11, caso a thread
seja finalizada dentro da região crítica, o objeto permanecerá bloqueado
mesmo após a execução da thread.


                 mutexWrite.WaitOne();
                 //...
                 //Região Crítica
                 //Thread abortada, interrompida ou suspensa
                 //...
                 mutexWrite.Release();


                    Quadro 11.O problema com threads abortadas


5. ESTUDO DE CASO: O PROBLEMA DOS LEITORES E ESCRITORES


       Para um melhor entendimento de como usar threads utilizando a
linguagem de programação C#, é mostrado e comentado o código fonte do
programa desenvolvido para resolver o problema dos leitores e escritores
utilizando processamento paralelo com concorrência, contudo, devidamente
sincronizado.
Este clássico problema pode ser visto como diversos usuários, threads,
utilizando um banco de dados. É claro que nos diversos sistemas que utilizam
banco de dados, vários usuários fazem consultas (ver o histórico escolar,
consultar o saldo bancário, etc.), alguns outros usuários precisam escrever na
base de dados, seja para atualizar o endereço de e-mail ou até mesmo se
cadastrar na locadora perto de casa.
       Visando facilitar o entendimento do programa, ele será dividido por
métodos, sendo comentados separadamente.




                             Figura 1. Tela do programa
//variáveis utilizadas para mutex de leitura e escrita
                object mutexRead = new object();
                Semaphore mutexWrite = new Semaphore(1, 1);

                //variável utilizada para evitar a condição de corrida ao
                //ListBox
                Semaphore mutexListBox = new Semaphore(1, 1);

                //vetores de threads utilizados para adicionar o recurso de
                //vários leitores e escritores simultâneios
                Thread[] threadReader, threadWriter;

                //dado compartilhado por leitores e escritores
                StringBuilder dado = new StringBuilder(16, 150);

                //contador de leitores ativos
                int readerCounter = 0;

                //constantes para uso no sleep das threads
                const int minRandom = 500;
                const int maxRandom = 5000;

                //variável usada para oferecer um número aleatório
                Random sorteio = new Random(minRandom);

                //informa o momento de parada das threads
                bool continuaLeitor, continuaEscritor;


                   Quadro 12.Declaração de variáveis do programa


      São mostradas todas as variáveis globais do programa no quadro 12. Os
objetos mutexRead, mutexWrite e mutexListBox controlam o acesso as regiões
dos leitores, escritores e do listbox usado, respectivamente.
      Os vetores threadReader e threadWriter armazenam todas as threads
manipuladas pelo programa, de tal modo que tenha controle sobre todas as
threads criadas caso seja preciso.
      A variável dado é responsável por armazenar a ultima informação
armazenada por um leitor. Ela é do tipo StringBuilder pelo fato de strings no C#
serem estáticas.
      Os objetos continuaLeitor e continuaEscritor são úteis para informar ate
quando cada as threads devem permanecer ativas. No momento desejado da
parada, as threads terminam as tarefas pendentes e depois chegam ao fim da
execução quando no comando while é testado o valor da variável
continuaLeitor no caso de uma thread leitora ou continuaEscritor caso seja uma
thread escritora.


         private void buttonLeitor_Click(object sender,
         System.EventArgs e) {

                //para evitar que novas thread sejam instanciadas
                buttonLeitor.Enabled = false;

                    continuaLeitor = true;

               //alocando o vetor
               threadReader = new
         Thread[Int32.Parse(textBoxValorLeitor.Text)];

                //criando e iniciando a quantidade desejada de threads
                //e definindo um nome para cada thread
                for (int i = 0; i < threadReader.Length; i++) {
                        threadReader[i] = new Thread(new
                        ThreadStart(Reader));
                        threadReader[i].Name = "Leitor " + (i +
                        1).ToString("D3");
                        threadReader[i].Start();
                }

         }


                           Quadro 13.Código que inicia os leitores


      O método buttonLeitor_Click cria o vetor de leitores com o tamanho
passado pelo usuário, então cria, nomeia e inicia cada thread.
private void buttonEscritor_Click(object sender, System.EventArgs
        e) {

              //para evitar que novas thread sejam instanciadas
              buttonEscritor.Enabled = false;

              continuaEscritor = true;

              //alocando vetor
              threadWriter = new
        Thread[Int32.Parse(textBoxValorEscritor.Text)];

              //criando e iniciando a quantidade desejada de threads
              //e definindo um nome para cada thread
              for (int i = 0; i < threadWriter.Length; i++) {
                      threadWriter[i] = new Thread(new
                      ThreadStart(Writer));
                      threadWriter[i].Name = "Escritor " + (i +
                      1).ToString("D3");
                      threadWriter[i].Start();
              }

        }


                         Quadro 14.Criando os escritores


      Da mesma forma como o método anterior, o método buttonEscritor_Click
cria o vetor que conterá as threads e as inicia dando um nome, seguindo a
mesma lógica já apresentada.
private void Writer() {
                int iteracoes = 1;
                while (continuaEscritor)

                    Thread.Sleep(sorteio.Next(minRandom,
               maxRandom + minRandom));

                      //bloqueando os escritores
                      mutexWrite.WaitOne();

                     mutexListBox.WaitOne();
                     listBoxInforma.Items.Add(Thread.CurrentThread.Na
               me + " bloqueou os escritores");
                     mutexListBox.Release();

                      //somente será posto na variavel dado o nome do
               escritor e sua iteração
                      dado.Remove(0, dado.Length);
                      dado.Insert(0, Thread.CurrentThread.Name + " na
               iteração "+ iteracoes.ToString("D3") );
                      //fim do write_data();

                     mutexListBox.WaitOne();
                     listBoxInforma.Items.Add(dado.ToString() + "
               escreveu.");
                     mutexListBox.Release();

                     mutexListBox.WaitOne();
                     listBoxInforma.Items.Add(Thread.CurrentThread.Na
               me + " liberou os escritores");
                     mutexListBox.Release();

                      //liberando os escritores
                      mutexWrite.Release();

                      iteracoes++;
               }

         }


                    Quadro 15.Método executado pelos escritores


      Por questão de simplicidade, o método para criar os escritores, quadro
15, é mostrado antes. Basicamente consiste em bloquear o semáforo de
escrita, escrever o dado e então liberar o semáforo. Usou-se do procedimento
Sleep para melhor intercalar as threads. Assim que bloqueia o acesso à escrita,
a thread informa que obteve o bloqueio, atualiza o valor da variável dado,
informa qual o novo valor e então libera o acesso.


         private void Reader() {
                int iteracores = 1;

                //dado lido pela thread
                string dadoLido;

                while (continuaLeitor) {
                       //Adormecer por um tempo randômico para melhor
                       //intercalar as threads
                       Thread.Sleep(sorteio.Next(minRandom,
                maxRandom));

                       //bloqueando leitores
                       lock (mutexRead) {

                              //incrementando o contador de leitores ativos
                              readerCounter ++;

                              //caso seja o primeiro leitor, deve bloquear os
                       escritores
                              if (readerCounter == 1) {
                                      //bloqueando escritores
                                      mutexWrite.WaitOne();

                                      mutexListBox.WaitOne();

                                      listBoxInforma.Items.Add(Thread.Current
                                      Thread.Name + " bloqueou os
                                      escritores");
                                      mutexListBox.Release();
                              }

                       }
                       //liberando leitores


                      Quadro 16.Método executado pelos leitores
dadoLido = dado.ToString();

                      //bloqueia leitores na RC e decrementa o valor do
                      //contador de leitores
                      lock (mutexRead) {

                              readerCounter --;

                            //caso seja o ultimo leitor, deve desbloquear
                      os escritores
                            if (readerCounter == 0) {

                                    mutexWrite.Release();

                                    mutexListBox.WaitOne();

                                    listBoxInforma.Items.Add(Thread.Curre
                                    ntThread.Name + " liberou os
                                    escritores");

                                    mutexListBox.Release();
                              }

                      }

                      //liberando leitores

                      //informar que a thread leitor leu um dado

                      mutexListBox.WaitOne();
                      listBoxInforma.Items.Add(Thread.CurrentThread.Na
                me + " leu o dado: " +
                      dadoLido);
                      mutexListBox.Release();


                       //incrementa a quantidade de iteraçoes realizadas
                pela thread
                       iteracores++;
                }
         }

                          Quadro 17.Continuação do quadro 16


      O procedimento Reader foi implementado de forma diferente do Writer
propositalmente, para mostrar as várias formas de controlar a concorrência
entre threads. Sucintamente, o primeiro leitor bloqueia o acesso à escrita, lê o
dado, verifica se é o ultimo leitor, pois caso seja deve liberar o acesso à escrita,
e então usa a informação lida. Ao entrar no laço while, o leitor é adormecido
por um tempo randômico pelo motivo supracitado, posteriormente bloqueia o
acesso a região crítica que incrementa a quantidade de leitores, verifica se há
necessidade de bloquear o acesso à escrita e então libera a região. Feito isso,
o dado é lido, novamente a região crítica é bloqueada, é decrementada a
quantidade de leitores, caso não exista leitores, o semáforo de escrita é
liberado e esta ação é informada. Por fim o leitor usa a informação lida, que
neste caso é simplesmente informar que o dado foi lido.
       Vale salientar que existe a necessidade de controlar o acesso à variável
listBoxInforma, pois é perfeitamente possível que 2 threads tentem acessar o
objeto ao mesmo tempo.


CONCLUSÃO


       Ao fazer uma análise de todo o assunto abordado neste artigo,
encontramos recursos que auxiliaram de forma positiva na implementação do
problema dos Leitores e Escritores usando a linguagem de programação C#.
       A linguagem C# contempla diversas ferramentas que auxiliam a
implementação de threads, tornando menos complexa a resolução do
problema. Estas ferramentas incluem desde simples bloqueios de regiões
específicas até a utilização de monitores e semáforos, dando ao programador
várias maneiras diferentes de implementações com o uso de threads.
       Por ser uma linguagem moderna e que vem crescendo nos últimos anos,
há uma grande facilidade de encontrar material relacionado a este e a qualquer
outro tópico relacionado a C#.
       Apesar de todas as facilidades oferecidas por esta e outras linguagens
de alto nível há uma maior complexidade na programação e depuração de
programas que utilizam threads, pois o programador precisa se preocupar com
questões relacionadas à utilização de threads. E sua depuração torna-se
menos intuitiva pelo fato do escalonamento realizado pelo sistema operacional
intercalar a execução das threads.


SOBRE OS AUTORES:
Daniel Ramon Silva Pinheiro é Graduando em Ciência da Computação pela
Universidade Tiradentes.
Danilo Santos Souza é Graduando em Ciência da Computação pela
Universidade Tiradentes.
Maria de Fátima A. S. Colaço é Mestre em Informática pela Universidade
Federal de Campina Grande
Rafael Oliveira Vasconcelos é Graduando em Ciência da Computação pela
Universidade Tiradentes.


Referência


1. ALBAHARI, Joseph; Threading in C#; Disponível em <
   http://www.albahari.com/threading/>; Acessado em 13.09.2008
2. BIRRELL, Andrew D.; An Introduction to Programming with C# Threads;
   Disponível em
   <http://research.microsoft.com/~birrell/papers/ThreadsCSharp.pdf>;
   Acessado em 13.09.2008
3. LEE, Edward A.; The Problem with Threads; Disponível em <
   http://www.computer.org/portal/site/computer/menuitem.5d61c1d591162e4b
   0ef1bd108bcd45f3/index.jsp?path=computer/homepage/0506&file=cover.x
   ml&xsl=article.xsl>; Acessado em 13.09.2008
4. Luiz Lima Jr.; Processos e Threads; Disponível em
   <http://www.ppgia.pucpr.br/~laplima/aulas/so/materia/processos.html>;
   Acessado em 19.09.2008
5. MAILLARD, Nicolas; Threads; Disponível em <
   http://www.inf.ufrgs.br/~nmaillard/sisop/PDFs/aula-sisop10-threads.pdf>;
   Acessado em 13.09.2008
6. MSDN Library; Threading (C# Programming Guide); Disponível em <
   http://msdn.microsoft.com/en-us/library/ms173178.aspx>; Acessado em
   13.09.2008
7. OUSTERHOUT, John; Why Threads Are A Bad Idea (for most purposes);
   Disponível em < http://home.pacbell.net/ouster/threads.pdf>; Acessado em
   13.09.2008
8. RICHTER, Jeffrey. CLR via C#, Segunda Edição. Microsoft Press, Março de
   2006
9. SANTOS, Giovane A.; Programação Concorrente; Disponível em
   <http://www.ucb.br/prg/professores/giovanni/disciplinas/2004-
   1/pc/material/giovanni/threads.html>; Acessado em 18.09.2008
10. SAUVÉ, Jacques Philippe; O que é um thread?; Disponível em
   <http://www.dsc.ufcg.edu.br/~jacques/cursos/map/html/threads/threads1.ht
   ml>; Acessado em 13.09.2008
11. TANENBAUM, Andrew. Sistemas operacionais modernos, 2ª Edição. Rio
   de Janeiro: LTC. 1999
12. Wikipedia; Thread (ciência da computação); Disponível em
   <http://pt.wikipedia.org/wiki/Thread_(ci%C3%AAncia_da_computa%C3%A7
   %C3%A3o)>; Acessado em 13.09.2008

Mais conteúdo relacionado

Mais procurados

Matemática Discreta - Introdução à Disciplina
Matemática Discreta - Introdução à DisciplinaMatemática Discreta - Introdução à Disciplina
Matemática Discreta - Introdução à DisciplinaRanilson Paiva
 
Redes Neurais Artificiais
Redes Neurais ArtificiaisRedes Neurais Artificiais
Redes Neurais ArtificiaisMarcos Castro
 
Respostas exercício 1 bdi
Respostas exercício 1   bdiRespostas exercício 1   bdi
Respostas exercício 1 bdiPatty Muniz
 
Banco de Dados I - Aula 06 - Banco de Dados Relacional (Modelo Lógico)
Banco de Dados I - Aula 06 - Banco de Dados Relacional (Modelo Lógico)Banco de Dados I - Aula 06 - Banco de Dados Relacional (Modelo Lógico)
Banco de Dados I - Aula 06 - Banco de Dados Relacional (Modelo Lógico)Leinylson Fontinele
 
Sistemas Distribuídos - Aula 09 - Tempos, Relogios e Sincronizacao de Tempo
Sistemas Distribuídos -  Aula 09 - Tempos, Relogios e Sincronizacao de TempoSistemas Distribuídos -  Aula 09 - Tempos, Relogios e Sincronizacao de Tempo
Sistemas Distribuídos - Aula 09 - Tempos, Relogios e Sincronizacao de TempoArthur Emanuel
 
Aula 2 - Introdução à programação de computadores - parte1
Aula 2 - Introdução à programação de computadores - parte1Aula 2 - Introdução à programação de computadores - parte1
Aula 2 - Introdução à programação de computadores - parte1Pacc UAB
 
Banco de dados - Mapeamento MER - Relacional
Banco de dados - Mapeamento MER - RelacionalBanco de dados - Mapeamento MER - Relacional
Banco de dados - Mapeamento MER - RelacionalDaniel Brandão
 
Resolução Parcial - Redes de Computadores - Kurose 6ª Edição
Resolução Parcial - Redes de Computadores - Kurose 6ª EdiçãoResolução Parcial - Redes de Computadores - Kurose 6ª Edição
Resolução Parcial - Redes de Computadores - Kurose 6ª EdiçãoRonildo Oliveira
 
Banco de Dados - Transações e Controle de Concorrência
Banco de Dados - Transações e Controle de ConcorrênciaBanco de Dados - Transações e Controle de Concorrência
Banco de Dados - Transações e Controle de ConcorrênciaJuliano Padilha
 
Problema da Mochila 0-1 (Knapsack problem)
Problema da Mochila 0-1 (Knapsack problem)Problema da Mochila 0-1 (Knapsack problem)
Problema da Mochila 0-1 (Knapsack problem)Marcos Castro
 
Banco de Dados II Aula Dinâmica 1 (Perguntas e Respostas)
Banco de Dados II Aula Dinâmica 1 (Perguntas e Respostas)Banco de Dados II Aula Dinâmica 1 (Perguntas e Respostas)
Banco de Dados II Aula Dinâmica 1 (Perguntas e Respostas)Leinylson Fontinele
 
Almanaque - Pensamento Computacional
Almanaque - Pensamento ComputacionalAlmanaque - Pensamento Computacional
Almanaque - Pensamento Computacionalprofjr
 
O Scratch no ensino da programação
O Scratch no ensino da programaçãoO Scratch no ensino da programação
O Scratch no ensino da programaçãoJoão Sá
 
Banco de dados exercícios resolvidos
Banco de dados exercícios resolvidosBanco de dados exercícios resolvidos
Banco de dados exercícios resolvidosGleydson Sousa
 
Estrutura de Dados Aula 13 - Árvores (conceito, elementos, tipos e utilizações)
Estrutura de Dados Aula 13 - Árvores (conceito, elementos, tipos e utilizações)Estrutura de Dados Aula 13 - Árvores (conceito, elementos, tipos e utilizações)
Estrutura de Dados Aula 13 - Árvores (conceito, elementos, tipos e utilizações)Leinylson Fontinele
 

Mais procurados (20)

Matemática Discreta - Introdução à Disciplina
Matemática Discreta - Introdução à DisciplinaMatemática Discreta - Introdução à Disciplina
Matemática Discreta - Introdução à Disciplina
 
Redes Neurais Artificiais
Redes Neurais ArtificiaisRedes Neurais Artificiais
Redes Neurais Artificiais
 
Respostas exercício 1 bdi
Respostas exercício 1   bdiRespostas exercício 1   bdi
Respostas exercício 1 bdi
 
Aula 4 banco de dados
Aula 4   banco de dados Aula 4   banco de dados
Aula 4 banco de dados
 
Banco de Dados I - Aula 06 - Banco de Dados Relacional (Modelo Lógico)
Banco de Dados I - Aula 06 - Banco de Dados Relacional (Modelo Lógico)Banco de Dados I - Aula 06 - Banco de Dados Relacional (Modelo Lógico)
Banco de Dados I - Aula 06 - Banco de Dados Relacional (Modelo Lógico)
 
Sistemas Distribuídos - Aula 09 - Tempos, Relogios e Sincronizacao de Tempo
Sistemas Distribuídos -  Aula 09 - Tempos, Relogios e Sincronizacao de TempoSistemas Distribuídos -  Aula 09 - Tempos, Relogios e Sincronizacao de Tempo
Sistemas Distribuídos - Aula 09 - Tempos, Relogios e Sincronizacao de Tempo
 
PostgreSQL
PostgreSQLPostgreSQL
PostgreSQL
 
Aula 2 - Introdução à programação de computadores - parte1
Aula 2 - Introdução à programação de computadores - parte1Aula 2 - Introdução à programação de computadores - parte1
Aula 2 - Introdução à programação de computadores - parte1
 
Banco de dados - Mapeamento MER - Relacional
Banco de dados - Mapeamento MER - RelacionalBanco de dados - Mapeamento MER - Relacional
Banco de dados - Mapeamento MER - Relacional
 
Resolução Parcial - Redes de Computadores - Kurose 6ª Edição
Resolução Parcial - Redes de Computadores - Kurose 6ª EdiçãoResolução Parcial - Redes de Computadores - Kurose 6ª Edição
Resolução Parcial - Redes de Computadores - Kurose 6ª Edição
 
Banco de Dados - Transações e Controle de Concorrência
Banco de Dados - Transações e Controle de ConcorrênciaBanco de Dados - Transações e Controle de Concorrência
Banco de Dados - Transações e Controle de Concorrência
 
Problema da Mochila 0-1 (Knapsack problem)
Problema da Mochila 0-1 (Knapsack problem)Problema da Mochila 0-1 (Knapsack problem)
Problema da Mochila 0-1 (Knapsack problem)
 
Banco de dados
Banco de dadosBanco de dados
Banco de dados
 
Banco de Dados II Aula Dinâmica 1 (Perguntas e Respostas)
Banco de Dados II Aula Dinâmica 1 (Perguntas e Respostas)Banco de Dados II Aula Dinâmica 1 (Perguntas e Respostas)
Banco de Dados II Aula Dinâmica 1 (Perguntas e Respostas)
 
Almanaque - Pensamento Computacional
Almanaque - Pensamento ComputacionalAlmanaque - Pensamento Computacional
Almanaque - Pensamento Computacional
 
Agilidade: Scrum e Xp
Agilidade: Scrum e XpAgilidade: Scrum e Xp
Agilidade: Scrum e Xp
 
O Scratch no ensino da programação
O Scratch no ensino da programaçãoO Scratch no ensino da programação
O Scratch no ensino da programação
 
Banco de dados exercícios resolvidos
Banco de dados exercícios resolvidosBanco de dados exercícios resolvidos
Banco de dados exercícios resolvidos
 
Linguagem assembly
Linguagem assemblyLinguagem assembly
Linguagem assembly
 
Estrutura de Dados Aula 13 - Árvores (conceito, elementos, tipos e utilizações)
Estrutura de Dados Aula 13 - Árvores (conceito, elementos, tipos e utilizações)Estrutura de Dados Aula 13 - Árvores (conceito, elementos, tipos e utilizações)
Estrutura de Dados Aula 13 - Árvores (conceito, elementos, tipos e utilizações)
 

Semelhante a Artigo Threads O Problema Dos Leitores E Escritores Implementado Em C# Rafael Oliveira Vasconcelos

Apresentação do artigo THREADS: O PROBLEMA DOS LEITORES E ESCRITORES IMPLEMEN...
Apresentação do artigo THREADS: O PROBLEMA DOS LEITORES E ESCRITORES IMPLEMEN...Apresentação do artigo THREADS: O PROBLEMA DOS LEITORES E ESCRITORES IMPLEMEN...
Apresentação do artigo THREADS: O PROBLEMA DOS LEITORES E ESCRITORES IMPLEMEN...rafaelov
 
SD_Aula_04_Introdução ao SD.pdf
SD_Aula_04_Introdução ao SD.pdfSD_Aula_04_Introdução ao SD.pdf
SD_Aula_04_Introdução ao SD.pdfFerro Gaita
 
Armazenamento, Indexação e Recuperação de Informação
Armazenamento, Indexação e Recuperação de InformaçãoArmazenamento, Indexação e Recuperação de Informação
Armazenamento, Indexação e Recuperação de InformaçãoMário Monteiro
 
Multithreaded tecnologia
Multithreaded tecnologia Multithreaded tecnologia
Multithreaded tecnologia J Chaves Silva
 
Apresentação final
Apresentação finalApresentação final
Apresentação finalvalmon
 
Relatório geral pi
Relatório geral piRelatório geral pi
Relatório geral piredesinforma
 
Threads - .Net Framework 4.0
Threads - .Net Framework 4.0Threads - .Net Framework 4.0
Threads - .Net Framework 4.0Charles Fortes
 
Gerências de Processos: Threads
Gerências de Processos: ThreadsGerências de Processos: Threads
Gerências de Processos: ThreadsAlexandre Duarte
 
Gerencia memoria simulador
Gerencia memoria simuladorGerencia memoria simulador
Gerencia memoria simuladormarcosfon
 
Emacs - Arquitetura E Design Com Foco No Desenv De Plugins
Emacs - Arquitetura E Design Com Foco No Desenv De PluginsEmacs - Arquitetura E Design Com Foco No Desenv De Plugins
Emacs - Arquitetura E Design Com Foco No Desenv De PluginsJosé Martins da Nobrega Filho
 

Semelhante a Artigo Threads O Problema Dos Leitores E Escritores Implementado Em C# Rafael Oliveira Vasconcelos (20)

Apresentação do artigo THREADS: O PROBLEMA DOS LEITORES E ESCRITORES IMPLEMEN...
Apresentação do artigo THREADS: O PROBLEMA DOS LEITORES E ESCRITORES IMPLEMEN...Apresentação do artigo THREADS: O PROBLEMA DOS LEITORES E ESCRITORES IMPLEMEN...
Apresentação do artigo THREADS: O PROBLEMA DOS LEITORES E ESCRITORES IMPLEMEN...
 
04 threads
04 threads04 threads
04 threads
 
Sistemas operacionais
Sistemas operacionaisSistemas operacionais
Sistemas operacionais
 
Sistemas operativos distribuidos
Sistemas operativos distribuidosSistemas operativos distribuidos
Sistemas operativos distribuidos
 
Curso openmp
Curso openmpCurso openmp
Curso openmp
 
Pilha de protocolos
Pilha de protocolosPilha de protocolos
Pilha de protocolos
 
SD_Aula_04_Introdução ao SD.pdf
SD_Aula_04_Introdução ao SD.pdfSD_Aula_04_Introdução ao SD.pdf
SD_Aula_04_Introdução ao SD.pdf
 
S.o aula 1920
S.o aula 1920S.o aula 1920
S.o aula 1920
 
Armazenamento, Indexação e Recuperação de Informação
Armazenamento, Indexação e Recuperação de InformaçãoArmazenamento, Indexação e Recuperação de Informação
Armazenamento, Indexação e Recuperação de Informação
 
Artigo c#
Artigo c#Artigo c#
Artigo c#
 
Multithreaded tecnologia
Multithreaded tecnologia Multithreaded tecnologia
Multithreaded tecnologia
 
Atividade2redes
Atividade2redesAtividade2redes
Atividade2redes
 
Resumo c#
Resumo c#Resumo c#
Resumo c#
 
Apresentação final
Apresentação finalApresentação final
Apresentação final
 
Relatório geral pi
Relatório geral piRelatório geral pi
Relatório geral pi
 
Threads - .Net Framework 4.0
Threads - .Net Framework 4.0Threads - .Net Framework 4.0
Threads - .Net Framework 4.0
 
Gerências de Processos: Threads
Gerências de Processos: ThreadsGerências de Processos: Threads
Gerências de Processos: Threads
 
Gerencia memoria simulador
Gerencia memoria simuladorGerencia memoria simulador
Gerencia memoria simulador
 
Emacs - Arquitetura E Design Com Foco No Desenv De Plugins
Emacs - Arquitetura E Design Com Foco No Desenv De PluginsEmacs - Arquitetura E Design Com Foco No Desenv De Plugins
Emacs - Arquitetura E Design Com Foco No Desenv De Plugins
 
Padrões de Projeto de Software
Padrões de Projeto de SoftwarePadrões de Projeto de Software
Padrões de Projeto de Software
 

Artigo Threads O Problema Dos Leitores E Escritores Implementado Em C# Rafael Oliveira Vasconcelos

  • 1. THREADS: O PROBLEMA DOS LEITORES E ESCRITORES IMPLEMENTADO EM C# Daniel Ramon Silva Pinheiro, Danilo Santos Souza, Maria de Fátima A. S. Colaço, Rafael Oliveira Vasconcelos RESUMO: Este artigo tem como objetivo apresentar a utilização de threads através da implementação de um problema que as utilizam. O problema utilizado é caso dos leitores e escritores, que modela o acesso a uma base de dados sendo requisitada para operações de leitura e escrita de forma a seguir alguns critérios visando a garantia da integridade dos dados da base. Aborda-se também alguns conceitos básicos de threads para um melhor entendimento da implementação e da solução do problema proposto. Além dos conceitos básicos, também é abordado neste artigo, exemplos de trechos de códigos programados na linguagem de programação C#, utilizando- se dos recursos disponíveis para a manipulação de threads como: criar e iniciar threads, sincronismo, prioridade, nomear, acordar e dormir, bloquear, interromper e resumir ou recomeçar. De forma prática e em conjunto com os recursos da linguagem já citada, mostra-se a implementação da resolução do problema dos leitores e escritores visando o estudo de threads não só na teoria. PALAVRAS-CHAVE: Processo, Região Crítica, Sistema Operacional, Thread. ABSTRACT: This article aims to present the use of threads through the implementation of a problem using them. The problem used is the readers and writers, which shapes access to a database being requested for operations of reading and writing in order to follow certain criteria aimed at ensuring the integrity of the data base. It also approached some basic concepts of threads to a better understanding of the implementation of the solution proposed. In addition to the basic concepts, is also approached in this article, examples of stretch of code programmed in the programming language C#, using the resources available for the manipulation of threads as: create and start threads, synchronization, priority, label, wake and sleep, block, interrupt and resume or start over. From a practical way and together with the resources of the language already quoted, it is shown in the implementation of the resolution of the problem of readers and writers seeking the study of threads not only in theory. KEYWORDS. Critical Area, Operating System, Process, Thread
  • 2. 1. INTRODUÇÃO Devido à evolução da tecnologia, principalmente no mundo computacional, ocorreu a necessidade de novas formas de executar processos nos sistemas operacionais, com o intuito de ganho em processamento. A partir dessas evoluções nos processos nasceu um conceito caracterizado como thread. Mas a principio o que seria processo? O que seria thread? O que thread tem haver com processamento? De forma básica um processo é um programa em execução e uma thread é a execução de parte de um processo. Pelo fato de thread ser parte de um processo, possui o mesmo espaço de endereçamento compartilhando uma mesma região de memória podendo assim um processo ter uma ou várias threads. É ai que entra o poder da thread com base no ganho de processamento, principalmente em ambientes multiprocessados. Os processos podem ser independentes, onde cada processo executa sem a necessidade de compartilhamento de variável, e concorrente que ao contrário dos independentes os processos compartilham uma ou mais variáveis, onde essa região compartilhada se caracteriza como região critica. Vale ressaltar também que vários processos podem tentar acessar a mesma região critica e o resultado depender da ordem em que eles são executados, definindo a idéia de condição de corrida. Exemplos e maiores detalhes das situações citadas anteriormente serão abordados ao decorrer do trabalho. O uso do compartilhamento de variável faz com que aconteçam alguns problemas no uso das threads pelo fato das mesmas herdarem algumas propriedades dos processos. Existem diversos problemas existentes no uso das threads. Demonstraremos um problema especifico que é o caso do problema dos leitores e escritores. O problema dos leitores e escritores de forma simples é um problema onde se tem uma região critica onde threads podem ler (somente querer saber o que consta na região critica) ou escrever (querer alterar valor na região critica), levando em considerações alguns critérios. O mesmo será descrito de forma mais detalhada no trabalho com o uso de exemplos e apresentando a solução do mesmo.
  • 3. Estamos falando de thread, computador, processamento, processos, mas como implementar threads no mundo computacional? Qual linguagem utilizar? Atualmente existem diversas linguagens com diversos recursos para criarmos a idéia de thread no mundo computacional. Com ênfase na linguagem C# alguns recursos serão demonstrados junto com exemplos de código e formas de como usar esse recursos. Demonstrado uma idéia prática de thread em conjunto com os recursos da linguagem C# o caso do problema dos leitores e escritores foi implementado na linguagem já citada, existindo assim um tópico exclusivo apresentando o código da implementação e para facilitar o entendimento o uso de comentários no código. Enfim tudo isso citado junto com mais alguns complementos serão apresentados neste artigo, com o objetivo de demonstrar o uso das threads desde a parte teórica até a parte prática, finalizando o artigo com uma conclusão retratando a opinião dos autores com relação ao tema abordado. 2. THREADS Literalmente thread significa, em português, linha de execução. Conceitualmente falando, thread é uma forma de um processo dividir-se em duas ou mais tarefas que podem ser executadas simultaneamente. Podem porque nos hardwares equipados com múltiplos núcleos, as linhas de execução de uma thread, podem ser executadas paralelamente, uma em cada núcleo, já nos hardwares com um único núcleo, cada linha de execução é processada de forma aparentemente simultânea, pois a mudança entre uma linha e outra é feita de forma tão rápida que para o usuário isso está acontecendo paralelamente. Para o progresso deste artigo que trata de threads é de fundamental importância uma breve diferenciação das threads e dos processos, uma vez que os dois são distintos, mas semelhantes. Então, o que seria um processo? Processo, na área da computação é um módulo executável único que é executado concorrentemente com outros módulos executáveis. Onde um
  • 4. módulo executável é um conjunto de instruções de um programa que devem ser seguidos para a conclusão do mesmo. Para exemplificar, existem os sistemas operacionais multitarefa (Windows ou Linux, por exemplo) que executam vários processos que rodam concorrentemente com os outros processos para que tenham suas linhas de código executadas pelo processador. Além de também poderem rodar simultaneamente com outros processos interagindo para que a aplicação ofereça um melhor desempenho e confiabilidade. Processos são módulos separados e carregáveis. Threads, não podem ser carregados, eles são iniciados dentro de um processo, onde um processo pode executar várias threads ao mesmo tempo. Funcionam como se existisse vários processos internos ao processo pai, o qual gera outros processos. Aos processos gerados pelo pai, denominam-se processos filhos. Estes podem rodar ao mesmo tempo seguindo algumas regras para que não ocorram conflitos internos. Estes conflitos internos podem variar desde uma paralisação total ou parcial do sistema até inconsistência de valiosas informações. No universo dos modelos de processos existem dois conceitos independentes de como enxergá-los, são eles o agrupamento de recursos e a execução. O primeiro é um modo de ver um processo, ele apresenta um espaço de endereçamento que possui um código e os dados de programa, e talvez alguns outros recursos alocados, como alguns arquivos abertos, informações entre contabilidades, processos filhos, enfim, o que interessa é que agrupar todos eles em forma de processos facilitará o gerenciamento destes recursos. O segundo é denominado thread de execução, que normalmente é abreviado simplesmente para thread. Ele possui um contador de programa, o qual manterá o controle de qual instrução da thread deverá ser executada em seguida pelo núcleo, possui registradores com suas variáveis de trabalho atuais, possui uma pilha estruturada pelo conjunto de procedimentos chamados, mas ainda não concluídos, a qual informa o histórico da execução. Os dois conceitos citados acima são importantes, pois delimitam os conceitos entre threads e processos, que apesar de semelhantes, são conceitos diferentes. A característica que os threads acrescentam ao conceito
  • 5. de processos é a permissão de múltiplas execuções ocorrerem em um mesmo processo de forma independente uma das outras. Existem sistemas que suportam apenas uma única thread em execução por vez e os que suportam mais de uma por vez, denominados de monothread e multithread respectivamente.Então, qual seria a diferença de ter várias threads sendo executadas em um único processo (multithread), e vários processos sendo executados em um computador? No primeiro caso (multithread), os várias threads estão compartilhando um mesmo espaço de endereçamento na memória, assim como os recursos alocados pelo processo criador da thread. No segundo caso, os processos compartilham um espaço físico de memória. É importante citar que a existência de recursos compartilhados necessita de um controle para que não haja nenhum tipo de conflito que possa embaralhar tanto a vida do usuário como a integridade das informações processadas. 2.1. TIPOS DE THREADS Existem dois tipos de implementações de threads: thread usuário e thread núcleo. O primeiro, thread de usuário, como o próprio nome já diz, tem por principal característica o fato de deixar todos os pacotes e controles de threads no espaço do usuário, de forma que o núcleo não seja informado sobre eles, logo as threads serão tratadas de forma simples (monothread). Mesmo que existam vários núcleos, ou seja, vários processos sendo executados ao mesmo tempo (multiprocessamento), onde somente os processos é que serão executados paralelamente e não as threads, pois estas estão alocados dentro dos processos. Uma vantagem das threads de usuário está na sua versatilidade, pois elas funcionam tanto em sistemas que suportem ou não o uso de threads. Uma vez que sua implementação estará interna ao processo criador da thread, o sistema operacional não poderá interferir nesta criação, desta forma o sistema executará a thread como se fosse apenas mais uma linha de execução do processo. De fato, o processo criador deverá possuir todas as características
  • 6. de gerenciamento e confiabilidade de threads, que estão presentes nas tabelas dos núcleos dos sistemas que ofereçam o suporte aos threads. Um thread de usuário possui desempenho melhor, mas existem alguns problemas, como por exemplo, uma chamada ao sistema de bloqueio. Se a thread executar esta chamada, ela para todas as outras threads, porém com o uso de threads chamadas desse tipo são muito comuns, pois elas permitem o controle das threads. Seria contraditório realizar uma chamada de bloqueio, que pare todos as threads para permitir que uma outra delas possa ser executada. De fato nenhuma thread jamais seria executada, até que fosse desbloqueada. Outro problema com a utilização de thread é a posse do núcleo. Uma vez que se inicie uma thread do tipo usuário, ela ficará sendo executada até que, por uma linha de comando própria ela libere o núcleo para outras threads do processo. No entanto, existem soluções para os problemas mencionados acima, porém são complicadas de serem implementadas, o que torna o código bastante confuso. O segundo tipo, thread de núcleo, é perceptível logo de início que o núcleo sabe da existência das threads e que ele será o gerenciador das mesmas. Neste caso, o processo não precisará de nenhuma tabela para gerenciar as threads, o núcleo se encarregará de tudo, sendo necessário ao processo apenas a realização das chamadas que quiser ao núcleo para a manipulação de suas threads. Estas chamadas ao sistema possuem um custo maior se comparadas com as chamadas que um sistema de threads de usuário realiza. Para amenizar este custo, os sistemas utilizam-se da ‘reciclagem’ de threads, desta forma, quando uma thread é destruída, ela é apenas marcada como não executável, sem afetar sua estrutura. Desta forma a criação de uma nova thread será mais rápida, visto que sua estrutura já esta montada, bastando apenas a atualização de suas informações. Uma vantagem da thread de núcleo é que se uma thread de um processo for bloqueado, as outras threads que forem gerados por este mesmo processo poderão dar continuidade às suas linhas de execução, sem a necessidade da primeira thread concluir suas linhas de execução.
  • 7. Existem também as implementações de threads híbridas, neste caso, tenta-se combinar as vantagens das threads de usuário e com as de núcleo. 2.2. COMUNICAÇÃO ENTRE THREADS Com freqüência os processos precisam trocar informações entre si para continuar suas linhas de execução. Quando se trata de threads isso é um pouco mais fácil, pois elas compartilham um espaço de endereçamento comum, entretanto ainda é necessário um controle para evitar embaraços entre elas. Esses embaraços geralmente ocorrem quando elas acessam uma variável compartilhada ao mesmo tempo, ou seja, dentro de uma região crítica. 2.2.1. Região crítica Em poucas palavras, região crítica é uma região de memória compartilhada que acessa um recurso que está compartilhado e que não possa ser acessado concorrentemente por mais de uma linha de execução. A região crítica, como o próprio nome já diz, por ser uma área crítica necessita de cuidados para que não hajam problemas futuros devido à má utilização da mesma. Para entender melhor região crítica é bom ter em mente o que seria Condição de disputa. Esta consiste em um conjunto de recursos que deve ser compartilhado entre processos no qual, em um mesmo intervalo tempo, dois ou mais processos tentem alocar uma mesma parte de um mesmo recurso para poder utilizá-lo. Nesta hora que ocorre o problema do controle de disputa. Para melhor entendimento deste problema, imagine duas threads, A e B, e um recurso compartilhado que permita o acesso das duas threads ao mesmo tempo, de forma que sempre que este recurso for utilizado seja emitido um aviso informando que ocorreu tudo bem. Agora vamos supor que as threads A e B entrem na região compartilhada para utilizá-la quase que ao mesmo tempo. Então, a thread A chega primeiro e marca na região compartilhada para ser a próxima a utilizá-la, mas antes que ela a utilize, o sistema operacional tire a sua posse de núcleo e a passa para a thread B. Então a thread B marca o recurso compartilhado como sendo ele o próximo a
  • 8. utilizá-lo, o utiliza e recebe sua confirmação do recurso informando que ocorreu tudo bem. Após isso a thread B libera o núcleo e o sistema o passa para a thread A, que pensa ser a próximo a utilizar o recurso compartilhado e fica aguardando a mensagem do recurso informando que ocorreu tudo bem. Entretanto o processo A ficará eternamente esperando pela resposta do recurso, mas ela nunca chegará. Com base nisto é possível perceber o problema de condição de disputa e também é caracterizar a região crítica. Que no caso do exemplo anterior seria a área do recurso que aloca o próximo processo ou thread a utilizá-lo. Para resolver estes tipos de problemas e muitos outros tipos que envolvam regiões compartilhadas é preciso encontrar uma forma de bloquear que outros processos usem uma área compartilhada que esteja sendo usada até que ela seja liberada pelo processo que a esteja utilizando. A essa solução denomina-se exclusão mútua e será o assunto abordado no próximo tópico. 2.2.2. Exclusão mútua Como dito anteriormente, exclusão mútua é uma solução encontrada para evitar que dois ou mais processos ou threads tenham acesso simultaneamente a alguma região crítica de algum recurso que esteja compartilhado. Existem quatro condições que devem ser satisfeitas para que os processos e threads concorrentes à mesma região crítica sejam executados de forma eficiente e corretamente. São elas: 1) Nunca dois processos podem estar simultaneamente em suas regiões críticas; 2) Nada pode ser afirmado sobre a velocidade ou sobre o número de CPUs; 3) Nenhum processo executando fora de sua região crítica pode bloquear outros processos; 4) Nenhum processo deve esperar eternamente para entrar em sua região crítica;
  • 9. Existem várias formas para se realizar a exclusão mútua de forma que se um processo estiver utilizando a região crítica, nenhum outro processo poderá utilizar esta região para que não ocorram problemas. 2.3. PROBLEMAS COM O USO DE THREADS Como dito anteriormente, as threads apresentam alguns problemas. Alguns deles já foram passados implicitamente com as abordagens anteriores, como os da região crítica e os da dificuldade de implementação, por exemplo. Entretanto existem mais um leque de problemas relacionados a threads e são deles que este artigo tratará de explicar quais são eles agora. Lock //Variável compartilhada. Indica se a região crítica está //liberada. While (true) do { If (Lock = 0) { Lock = 1; //Região_Crítica Lock = 0; } } Quadro 1. Pseudocódigo do funcionamento das variáveis de travamento Existem várias tentativas de contornar os problemas com os threads, um deles é utilização de variáveis de impedimento (Lock), a qual está representada no Quadro 1. Esta é uma solução via software do usuário e não pelo sistema operacional. De acordo com o Quadro 1, a variável Lock inicialmente contem o valor 0. Sempre que algum processo ou thread tenta entrar na região crítica ele testa
  • 10. se lock é 0. Caso afirmativo, o processo ou thread altera esta variável para 1 e então entra na região crítica. Caso negativo o processo ou thread entrará em um laço sem fazer nada até que a variável esteja contenha o valor 0. Apesar de parecer uma boa solução, ela não consegue satisfazer sempre as quadro condições da exclusão mútua. Para provar isso, suponha duas threads, A e B, que queiram acessar uma mesma região crítica. A thread A testa se a variável Lock é 0. Como é o estado inicial, Lock é 0. Mas justamente neste ponto, o sistema operacional toma a posse do núcleo de A e o entrega para B. Logo, B também verifica que a variável Lock permanece 0, uma vez que A ainda não entrou e alterou para 1. Então como as duas threads verificaram que Lock é 0, podem entrar na região crítica, e neste caso ocorrerá problemas. Outra suposição com os mesmos parâmetros de entrada é supor que a thread A entre da região e antes que saia e modifique a variável Lock para 0, de um erro e trave. Desta forma, como o thread B ainda não entrou na região crítica, ele nunca entrará, pois a variável Lock permanecerá sempre como 1. Este mesmo caso ocorre com outra tentativa de solução que se da através da utilização de semáforos. Existem vários outros problemas relacionados às threads. Este foi apenas um deles e serviu como exemplo para que se possa entender a complexidade da programação com a utilização de threads, pois apesar do pseudocódigo do Quadro 1 ser uma solução ser simples, verifica-se que ele não satisfaz as quatro condições da exclusão mútua, e continua sem resolver os problemas complexos das threads. Outros problemas de importante citação são o deadlock e o starvation. O primeiro ocorre quando uma thread está aguardando a liberação de um recurso compartilhado, que por sua vez, possui uma outra thread aguardando a liberação de outro recurso compartilhado da primeira thread. Desta forma elas ficarão eternamente paradas, uma esperando pela outra. Uma analogia a este problema é imaginar uma rua estreita, na qual só entra um carro por vez. Supondo que entrem dos dois lados duas fileiras enormes de carros, um atrás do outro, eles ficarão travados, pois não conseguirão ir para frente ou para trás.
  • 11. O segundo, conhecido como inanição, ocorre quando um processo ou thread nunca é executado, pois processos ou threads de maior importância sempre tomam a posse do núcleo fazendo com que os de menores prioridades nunca sejam executados. A diferença entre o deadlock e o starvation é que o starvation ocorre quando os programas rodam indefinidamente, ao contrário do deadlock, que ocorre quando os processos permanecem bloqueados, dependendo da liberação dos recursos por eles alocados. Como já mencionado, é importante salientar a dificuldade de se programar ao utilizar-se de thread devido a sua complexidade, uma vez que uma simples desatenção do programador pode ocasionar vários problemas. Ainda há a desvantagem do debug com threads, que é mais complicado, pois como pode existir mais de uma thread em execução pelo programa, o programador não saberá qual é a thread mostrada pelo compilador no modo debug. 3. APRESENTANDO O PROBLEMA DOS LEITORES E ESCRITORES As dependências de dados na execução de processos ou threads caracterizaram diversos tipos de problemas. Um deles é o problema conhecido como problema dos leitores e escritores. O problema dos Leitores e Escritores modela o acesso a uma base de dados, onde basicamente alguns processos ou threads estão lendo os dados da região crítica, somente querendo obter a informação da região crítica, que é o caso dos leitores, e outros processos ou threads tentando alterar a informação da região crítica, que é o caso dos escritores. Analisando uma situação de um banco de dados localizado em um servidor, por exemplo, temos situações relacionadas ao caso do problema dos leitores e escritores. Supondo que temos usuários ligados a este servidor querendo ler dados em uma tabela chamada Estoque, a princípio todos os usuários terão acesso a esses dados. Supondo agora usuários querendo atualizar na mesma tabela de Estoque, informações de vendas realizadas, de fato esses dados serão atualizados. Mas para organizar esses acessos tanto
  • 12. de atualização, quanto leitura no banco de dados algumas políticas são seguidas, o mesmo acontecerá no problema dos leitores e escritores. As políticas seguidas no caso dos leitores e escritores para acesso a região critica são as seguintes: processos ou threads leitores somente lêem o valor da variável compartilhada (não alteram o valor da variável compartilhada), podendo ser de forma concorrente; processos ou threads escritores podem modificar o valor da variável compartilhada, para isso necessita de exclusão mutua sobre a variável compartilhada; durante escrita do valor da variável compartilhada a operação deve ser restrita a um único escritor; para a operação de escrita não se pode existir nenhuma leitura ocorrendo, ou seja, nenhum leitor pode estar com a região critica bloqueada; em caso de escrita acontecendo, nenhum leitor conseguirá ter acesso ao valor da variável. Continuando a análise do banco de dados e seguindo as políticas dos leitores e escritores têm as seguintes situações: vários usuários consultando a tabela Estoque sem alterá-la; para um usuário atualizar uma venda é necessário que não se tenha nenhum usuário consultando a tabela de estoque; quando um usuário estiver atualizando a venda, nenhum outro usuário pode atualizar ao mesmo tempo; se o usuário iniciar uma consulta e estiver ocorrendo uma atualização o mesmo irá esperar a liberação da atualização. Por estarmos falando de um problema computacional, então como resolvermos isto computacionalmente? Segue abaixo um pseudocódigo nos quadros 2, 3 e 4 para se ter uma noção da solução: “semaphore mutex = 1; // controla acesso a região critica semaphore db = 1; // controla acesso a base de dados int rc = 0; // número de processos lendo ou querendo ler” Tanenbaum [10] Quadro 2.Variáveis do pseudocódigo
  • 13. “void reader(void) { while(TRUE) { // repete para sempre down(&mutex); // obtém acesso exclusivo a região critica rc = rc + 1; // um leitor a mais agora if (rc == 1) down(&db); //se este for o primeiro leitor bloqueia a //base de dados up(&mutex) // libera o acesso a região critica read_data_base(); //acesso aos dados down(&mutex); // obtém acesso exclusivo a região critica rc = rc -1; // menos um leitor if (rc == 0) up(&db); // se este for o último leitor libera a base de //dados up(&mutex) // libera o acesso a região critica use_data_read(); // utiliza o dado } }” Tanenbaum [11] Quadro 3. Procedimento do leitor Procedimento do Escritor void writer(void) { while (TRUE) { // repete para sempre think_up_data(); // região não critica down(&db); // obtém acesso exclusivo write_data_base(); // atualiza os dados up(&db); // libera o acesso exclusivo } }” Tanenbaum [11]
  • 14. Quadro 4.Procedimento do escritor Fazendo uma análise relacionada ao problema, enfim levando em consideração políticas, e a solução apresentada, conseguimos evitar a questão da espera ocupada que é um dos maiores problemas na comunicação de processos ou threads, tendo assim um bom processamento. Mas se pode notar que pode ocorrer à situação de se ter um leitor e bloquear a região critica. Se sempre chegarem leitores, aumentando assim o número de leitores, e existir um escritor esperando para realizar sua operação de escrita, o escritor pode chegar a não ser executado pelo grande número de leitores estarem sempre com a região critica bloqueada levando a uma situação caracterizada como starvation. 4. THREADS NO C# A linguagem de programação C#, assim como as atuais linguagens de programação de alto nível, oferece recursos que possibilitam a criação de programas com processamento paralelo com uso das threads. O C# provê recursos como criação de threads, sincronização e exclusão mutua. 4.1. CRIANDO THREADS E AS INICIANDO Antes de começar a programar utilizando threads, é preciso adicionar o namespace System.Threading. Feito isso, é possível dispor dos recursos oferecidos pela linguagem C#. Para criar uma thread basta informar uma nova variável do tipo Thread passando no construtor o delegado que informa qual método será executado pela thread. Feito isso, a thread já está pronta para ser iniciada, sendo preciso chamar o método Start para começar a execução como mostrado nos quadros 5 e 6.
  • 15. ThreadStart delegado = new ThreadStart(metodo); Thread t = new Thread (delegado); t.Start(); Quadro 5.Criando e iniciando uma thread class Ola_Mundo { static void Main() { Thread t = new Thread(new ThreadStart(Imprime)); t.Start(); } static void Imprime() { Console.WriteLine("Ola Mundo!"); } } Quadro 6.Programa Olá Mundo 4.2. SINCRONIZAÇÃO ENTRE THREADS A forma mais fácil de sincronizar a execução das threads é utilizando o método Join. Este método faz o bloqueio do programa até que a thread seja executada por completo, sendo então liberado para prosseguir com as demais instruções.
  • 16. class Ola_Mundo { static void Main() { Thread t = new Thread(new ThreadStart(Imprime)); t.Start(); t.Join(); Console.WriteLine("Fim do programa."); } static void Imprime() { Console.WriteLine("Ola Mundo!"); } } Quadro 7.Uso do método Join O uso do método Join garante que só será informado o fim do programa quando for impresso na tela a frase Ola Mundo pela thread. Este recurso é muito útil quando o programa só pode continuar sua execução após o fim da thread. Esta é uma forma bastante simples da manter a sincronização entre threads, porém anula a execução paralela, principal motivo para o uso de threads. Outra forma de manter a sincronização entre as threads é utilizar bloqueios, como lock, mutex ou semáforos. A forma de bloqueio lock é a mais simples e permite bloquear um bloco de código com exclusão mutua, evitando assim a condição de corrida. Por ser a forma mais simples de realizar um bloqueio, também é mais rápida que as demais. Caso outra thread tente realizar o bloqueio de um objeto que se encontra bloqueado, a thread será bloqueada pelo sistema operacional e só poderá continuar quando o objetivo for liberado, como mostrado no quadro 8.
  • 17. lock (contador) { contador++; Console.WriteLine(contador); } Quadro 8.Bloqueando uma região do código A classe mutex, mostrada no quadro 9, funciona parecida com o bloqueio lock, entretanto por não realizar o bloqueio por blocos de código, permite que tanto o bloqueio como o desbloqueio seja realizado em diferentes regiões do código com uso dos métodos WaitOne e ReleaseMutex. A tentativa de bloqueio de um objeto já bloqueado é análoga a forma anterior. mutex.WaitOne(); contador++; Console.WriteLine(contador); mutex.ReleaseMutex(); Quadro 9.Classe mutex para exclusão mútua Os semáforos são uma forma mais completa de realizar bloqueio. A classe Semaphore é uma extensão da classe mutex. Como principais características permitem que um ou mais processos entrem na região crítica e que o bloqueio realizado por uma thread possa ser desfeito por outra thread, recurso que pode ser útil em determinados problemas, como no problema dos leitores e escritores. Abaixo é mostrado como usar a classe Semaphore.
  • 18. semaforo.WaitOne(); contador++; Console.WriteLine(contador); semaforo.Release(); Quadro 10.Exemplo de uso da classe semáforo O C# oferece ainda a classe Monitor que provê outras funcionalidades como sinalizar e esperar por uma sinalização de outra thread. Os comandos são Wait e Pulse ou PulseAll. Outro recurso interessante é o TryEnter que como o próprio nome diz, tenta obter o acesso ao objeto, caso não seja possível retorna o valor false. O método Enter funciona de maneira semelhante aos já mencionados. Vale mencionar que a classe Monitor, segundo Jeffrey Richter, é 33 vezes mais rápida que a classe Mutex por ser implementada pela Common Language Runtime (CLR) e não pelo sistema operacional, além disso a classe Mutex permite sincronização entre processos. 4.3. OUTROS RECURSOS Ainda é possível escolher a prioridade da thread, informar se a mesma é uma thread de plano de fundo, dar um nome, adormecer por um tempo determinado, suspender, retomar, interromper e abortar uma thread. Assim como os processos do sistema operacional, as threads no C# têm uma prioridade padrão, mas que pode facilmente ser alterada pelo programador, bem como informar se a thread deve executar em segundo plano, ou background. Os métodos são Priority e IsBackground. Vale lembrar que os possíveis valores de prioridade de uma thread são Lowest, BelowNormal, Normal, AboveNormal e Highest, sendo normal a prioridade padrão.Esses valores são uma enumeração pertencentes ao namespace System.Threading.ThreadPriority. Os métodos para suspender, interromper e abortar uma thread parecem confusos, contudo têm suas diferenças. Quando suspensa (Suspend), a thread
  • 19. é bloqueada, mas há a possibilidade da mesma ser retomada (Resume). Este recurso deve ser usado com cautela, pois uma thread suspensa pode manter bloqueado um objeto até que seja re-iniciada, em uma condição mais crítica pode levar a um deadlock. Os métodos para interromper (Interrupt) e abortar (Abort) a thread finalizam permanentemente a execução, lançando as exceções ThreadInterruptedException e ThreadAbortException, respectivamente. A diferença básica entre interromper e abortar uma thread está no momento em que a thread será finalizada. Interrompendo, a thread só será finalizada quando for bloqueada pelo sistema operacional, já abortando será finalizada imediatamente. O problema que pode acontecer ao suspender uma thread, ou seja, bloquear e não desbloquear, também pode ocorrer quando ela é abortada ou interrompida. Isso acontece porque a thread é finalizada por meio de uma exceção lançada que ocasiona o fim da thread. No quadro 11, caso a thread seja finalizada dentro da região crítica, o objeto permanecerá bloqueado mesmo após a execução da thread. mutexWrite.WaitOne(); //... //Região Crítica //Thread abortada, interrompida ou suspensa //... mutexWrite.Release(); Quadro 11.O problema com threads abortadas 5. ESTUDO DE CASO: O PROBLEMA DOS LEITORES E ESCRITORES Para um melhor entendimento de como usar threads utilizando a linguagem de programação C#, é mostrado e comentado o código fonte do programa desenvolvido para resolver o problema dos leitores e escritores utilizando processamento paralelo com concorrência, contudo, devidamente sincronizado.
  • 20. Este clássico problema pode ser visto como diversos usuários, threads, utilizando um banco de dados. É claro que nos diversos sistemas que utilizam banco de dados, vários usuários fazem consultas (ver o histórico escolar, consultar o saldo bancário, etc.), alguns outros usuários precisam escrever na base de dados, seja para atualizar o endereço de e-mail ou até mesmo se cadastrar na locadora perto de casa. Visando facilitar o entendimento do programa, ele será dividido por métodos, sendo comentados separadamente. Figura 1. Tela do programa
  • 21. //variáveis utilizadas para mutex de leitura e escrita object mutexRead = new object(); Semaphore mutexWrite = new Semaphore(1, 1); //variável utilizada para evitar a condição de corrida ao //ListBox Semaphore mutexListBox = new Semaphore(1, 1); //vetores de threads utilizados para adicionar o recurso de //vários leitores e escritores simultâneios Thread[] threadReader, threadWriter; //dado compartilhado por leitores e escritores StringBuilder dado = new StringBuilder(16, 150); //contador de leitores ativos int readerCounter = 0; //constantes para uso no sleep das threads const int minRandom = 500; const int maxRandom = 5000; //variável usada para oferecer um número aleatório Random sorteio = new Random(minRandom); //informa o momento de parada das threads bool continuaLeitor, continuaEscritor; Quadro 12.Declaração de variáveis do programa São mostradas todas as variáveis globais do programa no quadro 12. Os objetos mutexRead, mutexWrite e mutexListBox controlam o acesso as regiões dos leitores, escritores e do listbox usado, respectivamente. Os vetores threadReader e threadWriter armazenam todas as threads manipuladas pelo programa, de tal modo que tenha controle sobre todas as threads criadas caso seja preciso. A variável dado é responsável por armazenar a ultima informação armazenada por um leitor. Ela é do tipo StringBuilder pelo fato de strings no C# serem estáticas. Os objetos continuaLeitor e continuaEscritor são úteis para informar ate quando cada as threads devem permanecer ativas. No momento desejado da parada, as threads terminam as tarefas pendentes e depois chegam ao fim da
  • 22. execução quando no comando while é testado o valor da variável continuaLeitor no caso de uma thread leitora ou continuaEscritor caso seja uma thread escritora. private void buttonLeitor_Click(object sender, System.EventArgs e) { //para evitar que novas thread sejam instanciadas buttonLeitor.Enabled = false; continuaLeitor = true; //alocando o vetor threadReader = new Thread[Int32.Parse(textBoxValorLeitor.Text)]; //criando e iniciando a quantidade desejada de threads //e definindo um nome para cada thread for (int i = 0; i < threadReader.Length; i++) { threadReader[i] = new Thread(new ThreadStart(Reader)); threadReader[i].Name = "Leitor " + (i + 1).ToString("D3"); threadReader[i].Start(); } } Quadro 13.Código que inicia os leitores O método buttonLeitor_Click cria o vetor de leitores com o tamanho passado pelo usuário, então cria, nomeia e inicia cada thread.
  • 23. private void buttonEscritor_Click(object sender, System.EventArgs e) { //para evitar que novas thread sejam instanciadas buttonEscritor.Enabled = false; continuaEscritor = true; //alocando vetor threadWriter = new Thread[Int32.Parse(textBoxValorEscritor.Text)]; //criando e iniciando a quantidade desejada de threads //e definindo um nome para cada thread for (int i = 0; i < threadWriter.Length; i++) { threadWriter[i] = new Thread(new ThreadStart(Writer)); threadWriter[i].Name = "Escritor " + (i + 1).ToString("D3"); threadWriter[i].Start(); } } Quadro 14.Criando os escritores Da mesma forma como o método anterior, o método buttonEscritor_Click cria o vetor que conterá as threads e as inicia dando um nome, seguindo a mesma lógica já apresentada.
  • 24. private void Writer() { int iteracoes = 1; while (continuaEscritor) Thread.Sleep(sorteio.Next(minRandom, maxRandom + minRandom)); //bloqueando os escritores mutexWrite.WaitOne(); mutexListBox.WaitOne(); listBoxInforma.Items.Add(Thread.CurrentThread.Na me + " bloqueou os escritores"); mutexListBox.Release(); //somente será posto na variavel dado o nome do escritor e sua iteração dado.Remove(0, dado.Length); dado.Insert(0, Thread.CurrentThread.Name + " na iteração "+ iteracoes.ToString("D3") ); //fim do write_data(); mutexListBox.WaitOne(); listBoxInforma.Items.Add(dado.ToString() + " escreveu."); mutexListBox.Release(); mutexListBox.WaitOne(); listBoxInforma.Items.Add(Thread.CurrentThread.Na me + " liberou os escritores"); mutexListBox.Release(); //liberando os escritores mutexWrite.Release(); iteracoes++; } } Quadro 15.Método executado pelos escritores Por questão de simplicidade, o método para criar os escritores, quadro 15, é mostrado antes. Basicamente consiste em bloquear o semáforo de escrita, escrever o dado e então liberar o semáforo. Usou-se do procedimento Sleep para melhor intercalar as threads. Assim que bloqueia o acesso à escrita,
  • 25. a thread informa que obteve o bloqueio, atualiza o valor da variável dado, informa qual o novo valor e então libera o acesso. private void Reader() { int iteracores = 1; //dado lido pela thread string dadoLido; while (continuaLeitor) { //Adormecer por um tempo randômico para melhor //intercalar as threads Thread.Sleep(sorteio.Next(minRandom, maxRandom)); //bloqueando leitores lock (mutexRead) { //incrementando o contador de leitores ativos readerCounter ++; //caso seja o primeiro leitor, deve bloquear os escritores if (readerCounter == 1) { //bloqueando escritores mutexWrite.WaitOne(); mutexListBox.WaitOne(); listBoxInforma.Items.Add(Thread.Current Thread.Name + " bloqueou os escritores"); mutexListBox.Release(); } } //liberando leitores Quadro 16.Método executado pelos leitores
  • 26. dadoLido = dado.ToString(); //bloqueia leitores na RC e decrementa o valor do //contador de leitores lock (mutexRead) { readerCounter --; //caso seja o ultimo leitor, deve desbloquear os escritores if (readerCounter == 0) { mutexWrite.Release(); mutexListBox.WaitOne(); listBoxInforma.Items.Add(Thread.Curre ntThread.Name + " liberou os escritores"); mutexListBox.Release(); } } //liberando leitores //informar que a thread leitor leu um dado mutexListBox.WaitOne(); listBoxInforma.Items.Add(Thread.CurrentThread.Na me + " leu o dado: " + dadoLido); mutexListBox.Release(); //incrementa a quantidade de iteraçoes realizadas pela thread iteracores++; } } Quadro 17.Continuação do quadro 16 O procedimento Reader foi implementado de forma diferente do Writer propositalmente, para mostrar as várias formas de controlar a concorrência entre threads. Sucintamente, o primeiro leitor bloqueia o acesso à escrita, lê o
  • 27. dado, verifica se é o ultimo leitor, pois caso seja deve liberar o acesso à escrita, e então usa a informação lida. Ao entrar no laço while, o leitor é adormecido por um tempo randômico pelo motivo supracitado, posteriormente bloqueia o acesso a região crítica que incrementa a quantidade de leitores, verifica se há necessidade de bloquear o acesso à escrita e então libera a região. Feito isso, o dado é lido, novamente a região crítica é bloqueada, é decrementada a quantidade de leitores, caso não exista leitores, o semáforo de escrita é liberado e esta ação é informada. Por fim o leitor usa a informação lida, que neste caso é simplesmente informar que o dado foi lido. Vale salientar que existe a necessidade de controlar o acesso à variável listBoxInforma, pois é perfeitamente possível que 2 threads tentem acessar o objeto ao mesmo tempo. CONCLUSÃO Ao fazer uma análise de todo o assunto abordado neste artigo, encontramos recursos que auxiliaram de forma positiva na implementação do problema dos Leitores e Escritores usando a linguagem de programação C#. A linguagem C# contempla diversas ferramentas que auxiliam a implementação de threads, tornando menos complexa a resolução do problema. Estas ferramentas incluem desde simples bloqueios de regiões específicas até a utilização de monitores e semáforos, dando ao programador várias maneiras diferentes de implementações com o uso de threads. Por ser uma linguagem moderna e que vem crescendo nos últimos anos, há uma grande facilidade de encontrar material relacionado a este e a qualquer outro tópico relacionado a C#. Apesar de todas as facilidades oferecidas por esta e outras linguagens de alto nível há uma maior complexidade na programação e depuração de programas que utilizam threads, pois o programador precisa se preocupar com questões relacionadas à utilização de threads. E sua depuração torna-se menos intuitiva pelo fato do escalonamento realizado pelo sistema operacional intercalar a execução das threads. SOBRE OS AUTORES:
  • 28. Daniel Ramon Silva Pinheiro é Graduando em Ciência da Computação pela Universidade Tiradentes. Danilo Santos Souza é Graduando em Ciência da Computação pela Universidade Tiradentes. Maria de Fátima A. S. Colaço é Mestre em Informática pela Universidade Federal de Campina Grande Rafael Oliveira Vasconcelos é Graduando em Ciência da Computação pela Universidade Tiradentes. Referência 1. ALBAHARI, Joseph; Threading in C#; Disponível em < http://www.albahari.com/threading/>; Acessado em 13.09.2008 2. BIRRELL, Andrew D.; An Introduction to Programming with C# Threads; Disponível em <http://research.microsoft.com/~birrell/papers/ThreadsCSharp.pdf>; Acessado em 13.09.2008 3. LEE, Edward A.; The Problem with Threads; Disponível em < http://www.computer.org/portal/site/computer/menuitem.5d61c1d591162e4b 0ef1bd108bcd45f3/index.jsp?path=computer/homepage/0506&file=cover.x ml&xsl=article.xsl>; Acessado em 13.09.2008 4. Luiz Lima Jr.; Processos e Threads; Disponível em <http://www.ppgia.pucpr.br/~laplima/aulas/so/materia/processos.html>; Acessado em 19.09.2008 5. MAILLARD, Nicolas; Threads; Disponível em < http://www.inf.ufrgs.br/~nmaillard/sisop/PDFs/aula-sisop10-threads.pdf>; Acessado em 13.09.2008 6. MSDN Library; Threading (C# Programming Guide); Disponível em < http://msdn.microsoft.com/en-us/library/ms173178.aspx>; Acessado em 13.09.2008 7. OUSTERHOUT, John; Why Threads Are A Bad Idea (for most purposes); Disponível em < http://home.pacbell.net/ouster/threads.pdf>; Acessado em 13.09.2008
  • 29. 8. RICHTER, Jeffrey. CLR via C#, Segunda Edição. Microsoft Press, Março de 2006 9. SANTOS, Giovane A.; Programação Concorrente; Disponível em <http://www.ucb.br/prg/professores/giovanni/disciplinas/2004- 1/pc/material/giovanni/threads.html>; Acessado em 18.09.2008 10. SAUVÉ, Jacques Philippe; O que é um thread?; Disponível em <http://www.dsc.ufcg.edu.br/~jacques/cursos/map/html/threads/threads1.ht ml>; Acessado em 13.09.2008 11. TANENBAUM, Andrew. Sistemas operacionais modernos, 2ª Edição. Rio de Janeiro: LTC. 1999 12. Wikipedia; Thread (ciência da computação); Disponível em <http://pt.wikipedia.org/wiki/Thread_(ci%C3%AAncia_da_computa%C3%A7 %C3%A3o)>; Acessado em 13.09.2008