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

11,918 views
11,747 views

Published on

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 available resources for the manipulation of threads as: create and start threads, synchronization, priority, label, wake and sleep, block, interrupt and resume or start over.


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.
Abordam-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 são abordados 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.

Published in: Technology
0 Comments
0 Likes
Statistics
Notes
  • Be the first to comment

  • Be the first to like this

No Downloads
Views
Total views
11,918
On SlideShare
0
From Embeds
0
Number of Embeds
24
Actions
Shares
0
Downloads
361
Comments
0
Likes
0
Embeds 0
No embeds

No notes for slide

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

  1. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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

×