quarta-feira, 24 de junho de 2009

Threads

Thread

Thread é uma forma de um processo dividir a si mesmo em duas ou mais tarefas que podem ser executadas simultaneamente. O suporte à thread é fornecido pelo próprio sistema operacional (SO), no caso da Kernel-Level Thread, ou implementada através de uma biblioteca de uma determinada linguagem, no caso de um usuario nivel thread.
Uma linha de execução permite que o usuário de programa, por exemplo, utilize uma funcionalidade do ambiente enquanto outras linhas de execução realizam outros cálculos e operações.
Em hardwares equipados com uma única CPU, cada linha de execução(Thread) é 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. Em hardwares com multiplos CPUs ou multi-cores as linhas de execução(Threads) podem ser realizadas realmente de forma simultânea;
Os sistemas que suportam apenas uma única linha de execução são chamados de monothread e aqueles sistemas que suportam múltiplas linhas de execução são chamados de multithread.

Exemplo

Um exemplo simples pode ser expressado através de um jogo onde o mesmo pode ser modelado com linhas de execução diferentes, sendo uma para desenho de imagem e outra para áudio; Neste caso, há um thread para tratar rotinas de desenho e outro thread para tratar áudio; No ponto de vista do usuário, a imagem é desenhada ao mesmo tempo em que o áudio é emitido pelos auto-falantes; Porém, para sistemas com uma única CPU, cada linha de execução é processada por vez;
No navegador como por exemplo: internet explorer ou firefox é outro modo facil de perceber threads. Quando abre o navegador foi executado um processo mas dentro desse processo voce pode abrir varias abas que mesmo assim vai continuar com um processo,ou seja, as abas sao threads.

Peculiaridades

Cada linha de execução tem o mesmo contexto de software e compartilha o mesmo espaço de memória (endereçado a um mesmo processo pai), porém o contexto de hardware é diferente. Sendo assim o overhead causado pelo escalonamento de linha de execução é muito menor do que o escalonamento de processos, entretanto, não há acesso protegido a memória nativamente (sua implementação fica a cargo do programador) devido ao compartilhamento do espaço de memória.
Um benefício do uso das linhas de execução advém do fato do processo poder ser dividido em mais de uma linha de tarefas; quando uma linha está esperando determinado dispositivo de E/S ou qualquer outro recurso do sistema, o processo como um todo não fica parado, pois quando uma linha de execução entra no estado de bloqueio uma outra thread aguarda na fila de prontos para executar.
Uma linha de execução possui um conjunto de comportamentos padrão, normalmente encontrados em qualquer implementação ou sistema operacional.
Uma linha de execução pode:
· criar outra da mesma forma que um processo, tal advento é conhecido como thread-create, onde a thread retorna um ID ao primeiro como primeiro argumento, como resultado da função de criação.
· esperar outra para se "juntar" (sincronizar), tal advento é conhecido como join.
· voluntariamente "desistir" da CPU, por não ser preciso mais o processamento proposto por ela ou pela vontade do usuário, tal advento é conhecido como thread-yield.
· replicar-se sem a necessidade de duplicar todo o processo, economizando assim memória, processamento da CPU e aproveitando o contexto (variáveis, descritores, dispositivos de E/S).

Estados de uma linha de execução

Basicamente uma linha de execução pode assumir os seguintes estados:
· criação. Neste estado, o processo pai está criando a thread que é levada a fila de prontos;
· execução. Neste estado a linha de execução está usando a CPU;
· pronto. Neste estado a linha de execução avisa a CPU que pode entrar no estado de execução e entra na fila de prontos;
· bloqueado. Neste estado, por algum motivo, a CPU bloqueia a linha de execução, geralmente enquanto aguarda algum dispositivo de E/S;
· término. Neste estado são desativados os contextos de hardware e a pilha é deslocada.
multithread Vários fluxos em execuções(várias threads), associado a um unico processo. Processo pode se dividir em duas ou mais taréfas, que podem ser executadas simultaneamente (programação concorrente).

Criação

Basicamente uma linha de execução pode dividir uma linha de execução em duas, depois estas linhas(threads) executam simultaneamente, a thread criadora é a thread pai e a thread criada é a thread filho. Threads incluidas na função main quando executadas pode criar threads filho no diagrama a seguir a thread A executa inicialmente. Mais tarde é criada a thread B indicada no ponto amarelo. Depois de criadas, a thread A e thread B executam simultaneamente. Em seguida a thread A pode criar uma ou mais threads (thread C). Depois de criada a thread C, há três threads executando simultaneamente e todas disputam o uso da CPU. Entretanto, a thread que pode ser executada a qualquer momento não é de conhecimento da CPU.

Término

Para maioria dos casos as threads não são criadas e executadas eternamente. Depois de terminado seu trabalho, a thread termina. No fato, a thread que criou estas duas threads filho terminam também porque sua tarefa atribuída se completa. Na matrix de multiplicação (matrix multiplication), uma vez que o valor de C[i,j] é computado a thread correspondente termina. Em geral quando a tarefa atribuída a thread completa, a thread pode ser terminada. Além disso, se a thread pai terminar, todas as threads filho terminam também. Porque isso é importante? Isso é importante porque as threads filho compartilham recursos com a thread pai, incluindo variáveis. Quando a thread pai termina, todas as variáveis são perdidas e a thread filho não poderá acessar os recursos que a thread pai possui. Assim, se a thread pai termina mais cedo que a thread filho haverá um problema. Uma thread pode terminar das seguintes maneiras:
· Retornando da sua rotina mais externa, a thread criadora.
· Quando termina a rotina em que foi começada.
· Chamando pthread_exit, fornecendo um estado de saída.
· Terminando através da função pthread_cancel

Junção

Imagine a seguinte situação: Você está estudando para uma prova. Então você pede o seu irmão mais novo para comprar uma pizza. Neste caso você é a thread principal e seu irmão a thread filha. Uma vez que você deu a ordem você e seu irmão começam a "executar uma tarefa" simultaneamente. Agora há dois casos a se considerar: Primeiro: Seu irmão traz a pizza e termina enquanto você estuda. Nesse caso você pode parar de estudar e comer a pizza. Segundo: Você acaba de estudar mais cedo e dorme e depois a pizza chegará.
A junção de threads (thread join) é destinada para resolver este problema. A thread pode executar o thread join e aguardar até a outra thread terminar. No caso acima você é a thread principal (thread main) e deve executar o thread join aguardando o seu irmão (thread filho) terminar. Em geral o thread join é utilizado para a thread pai juntar com uma das threads filhas.

Rendimento da thread

Suponha que você executa um certo número de programas o tempo todo no computador. Isso é possível devido a CPU destruir pouco a pouco outros ciclos de CPU, assim outros programas podem ser executados. Isso pode ser um problema de política de planejamento do sistema operacional. Entretanto, quando nós escrevemos nossos programas com múltiplas threads, nós temos que fazer certo para que algumas threads não ocupem a CPU eternamente, ou por um tempo muito longo sem abandoná-lo. Senão terminará na situação acima quando uma ou duas threads executam enquanto outras simplesmente esperam para retornar. Liberamos espaço na memória graças a thread yield. Quando a thread executa o thread yield, a execução da thread é suspensa e a CPU passa para uma outra thread em execução. Essa thread aguardará até a CPU tornar-se disponível novamente.

Comparação entre linha de execução e Processo

Um sistema baseado em linha de execução é diferente de um sistema operacional multi-tarefa tradicional, em que processos são tipicamente independentes, carregam considerável estado da informação, tem endereço de memória separado e interagem somente através de mecanismos de inter-processos de comunicação. As threads, por outro lado, compartilham o estado da informação de processos únicos, e compartilham memória e outros recursos diretamente.
A troca de contexto através de linha de execução num mesmo processo é tipicamente mais rápida que a troca de contexto entre processos diferentes. Sistemas como o Windows NT e o OS/2 são feitos para ter linha de execução "baratas" e processos "caros", enquanto em outros sistemas operacionais não há grandes diferenças.
O multithreading é um modelo de programação popular que permite a execução de múltiplas linha de execução dentro de um contexto simples, compartilhando recursos do processo, e capazes de executar de forma independente. O modelo de programação em linha de execução fornece ao desenvolvedor uma execução simultânea. Entretanto, a aplicação mais interessante da tecnologia ocorre quando ela é utilizada em um processo simples permitindo uma execução paralela em sistemas multi-processados.
Um sistema multi-threaded possui um melhor desempenho que um sistema de computadores com múltiplas CPUs e com múltiplos núcleos, ou que um cluster de máquinas. Isto acontece porque a linha de execução empresta a ela mesmo uma execução simultânea. Em alguns casos, o programador precisa ter cuidado em evitar condições de concorrência e outros comportamentos inesperados.
Para um dado ser manipulado corretamente, as linhas de execução freqüentemente precisarão ser sincronizadas, para que os dados sejam processados na ordem correta. As linha de execução podem também executar operações atômicas (freqüentemente implementadas usando semáforos) com intuito de prevenir que dados comuns sejam simultaneamente modificados ou lidos enquanto o processo esta sendo modificado.
Os sistemas operacionais implementam as linhas de execução de duas formas: preempção multithreading ou multithreading cooperativa. A preempção multithreading é geralmente considerada uma implementação superior, porque permite ao sistema determinar quando uma troca de contexto pode acontecer. A multithreading cooperativa, por outro lado, confia nas threads para ceder o controle, uma vez que elas estão paradas em um ponto. Isto pode criar um problema se a linha de execução estiver esperando um recurso tornar-se disponível. A desvantagem da preempção multithread é que o sistema pode fazer uma troca em um tempo inapropriado, causando uma inversão de prioridade ou outros efeitos ruins que podem ser evitados por uma multithreading cooperativa.
Em geral:
· Criar um processo pode ser caro em termos de tempo, memória, e sincronização entre processos.
· As linhas de execução podem ser criadas sem a replicação do processo inteiro.
· O trabalho de criar uma linha de execução pode ser feito no espaço do usuário.
· Como as linhas de execução partilham o espaço de endereçamento a comunicação entre elas é mais rápida.
· O tempo gasto para troca de linha de execução é menor, em parte por que não há necessidade de troca de espaço de endereçamento.

Beneficios.

Os beneficios da programaçao multithread podem ser divididos em quatro categorias pricipais:
1. Reponsabilidade: O uso de multithreads em uma aplicaçao interativa pode pemitir que um programa continue funcionando mesmo que parte dele esteja bloqueado ou realizando uma operaçao longa, aumentando assim a responsabilidade ao usuario.
2. Compatilhamento de recursos: Como padrao, as threads compartilham memoria e o recursos do processo ao qual pertencem.

3. Economia: A alocaçao de memoria e recursos para a criaçao de processos é dispediosa. Como as threads compartilham recursos do processos ao qual pertencem, é mais eonomico criar e trocar o contexto das threads.

4. Utilizaçao de arquiteturas multiprocessadas: Os beneficios do uso de multithreads podem ser muito maiores em uma arquitetura multiprocessada, na qual as threads podem ser executadas em paralelo nos diferentes processadores. Um processo dotado de unica thread so pode ser executado em uma cpu, não importa quantos estejam a disposiçao. O uso de multiplas threads em uma maquina de multiplas CPUs aumenta a concorrencia.

Modelos de múltiplas threads

1. Modelo muitos para um: Associa muitas threads no nivel do usuaio a uma thread de kernel. O gerenciamento de thread é feito pela biblioteca threads no espaço do usuario, de modo que é eficiente; mas o processo inteiro sera bloqueado se uma thread fizer uma chamada de sistema bloqueante. Alem disso, como somente uma thread pode acessar o kernel por vez, varias threads nao podem ser executadas em paralelo em multiprocessadores.

2. Modelo um para um: Associa a thread de cada usuario a uma thread de kernel. Ele prove maior concorrencia do que o modelo muitos para um, permitindo que outra thread seja executada quando uma thread faz uma chamada de sistema bloqueante; eles tambem permite que varias threads sejam executadas em paralelo em multiprocessadores. A unica desvantegem desse modelo é que a criaçao de uma thread de usuario requer a criaçao de uma thread de kernel correspondente. Como o custo adicional da criaçao de thread do kernel pode prejudicar o desempenho de uma aplicaçao a maioria das implementaçoes desse modelo restringe o numero de threads admitidos pelo sistema.

3. Modelo muito para muitos: Multiplexa muitas threads no nivel do usuario para um numero menor ou igual de threads de kernel. O numero de threads de kernel pode ser especifico a determinada aplicaçao ou a determinada maquina. Enquanto o modelo muitos para um permite que o desenvolvedor crie quantas threads de usuario desejar, a verdadeira concorrencia nao é obtida poruqe o kernel so pode escalonar uma thread de cada vez. P modelo um para um permite maior concorrencia, mas o desenvolvedor presisa ter cuidado de nao criar muitas threads dentro de uma aplicaçao. O modelo muitos para muitos nao sofre de nenhuma desssas limitaçoes: os desenvolvedores podem criar quantas threads forem necessarias, e as threads de kernel correspondente podem ser executadas paralelamente em um sistema multiprocessado. Alem disso, quando uma thread realiza uma chamada de sistema bloqueante, o kernel pode escalonar outra thread.


Nenhum comentário:

Postar um comentário

Bibliográfia utilizada

SILBERSCHATZ, Abraham - Sistemas Operacionais com Java, 7ª ed. rev. atual. - Rio de Janeiro: Elsevier Campus, 2008



TANENBAUM, Andrew S. - Sistemas Operacionais Modernos, 2ª ed. - São Paulo: Prentice Hall, 2003




STALLINGS, william - Operating Systems: Internals and Design Principles, Sixth Edition, 2009.