29
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

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

  • Upload
    rafaelov

  • View
    10.000

  • Download
    3

Embed Size (px)

DESCRIPTION

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.

Citation preview

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

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

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

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.

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

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

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

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

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

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

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

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.

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

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

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

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;

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

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.

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

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;

}

}

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

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.

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

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

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

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:

Quadro 2.Variáveis do pseudocódigo

“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]

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

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]

“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]

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

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.

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

Quadro 5.Criando e iniciando uma thread

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(); } static void Imprime() { Console.WriteLine("Ola Mundo!"); } }

ThreadStart delegado = new ThreadStart(metodo);

Thread t = new Thread (delegado);

t.Start();

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

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.

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!"); } }

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

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.

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.

mutex.WaitOne();

contador++;

Console.WriteLine(contador);

mutex.ReleaseMutex();

lock (contador) {

contador++;

Console.WriteLine(contador);

}

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

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

semaforo.WaitOne();

contador++;

Console.WriteLine(contador);

semaforo.Release();

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

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

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.

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

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

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

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

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

//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;

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

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.

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 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(); } }

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

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 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();

} }

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

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,

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++;

} }

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

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.

Quadro 16.Método executado pelos leitores

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.CurrentThread.Name + " bloqueou os escritores"); mutexListBox.Release();

} }

//liberando leitores

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

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

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.CurrentThread.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++; }

}

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

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:

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

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

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

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