Thread (ciência da computação) Origem: Wikipédia, a enciclopédia

Propaganda
Thread (ciência da computação)
Origem: Wikipédia, a enciclopédia livre.
Linha de execução (em inglês: Thread), é uma forma de um processo dividir a si mesmo em
duas ou mais tarefas que podem ser executadas concorrentemente. O suporte à thread é
fornecido pelo próprio sistema operacional (SO), no caso da linha de execução ao nível do
núcleo (em inglês: Kernel-Level Thread (KLT)), ou implementada através de uma biblioteca de
uma determinada linguagem, no caso de uma User-Level Thread (ULT).
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 autofalantes; Porém, para sistemas com uma única CPU, cada linha de execução é processada por
vez;
Particularidades
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
1
(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
entrada/saída 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 threadcreate, 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
I/O).
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 I/O;
término. Neste estado são desativados os contextos de hardware e a pilha é deslocada.
ULT e KLT
Usualmente as linhas de execução são divididas em duas categorias: ou linha de execução ao
nível do usuário (em inglês: User-Level Thread (ULT)), e linha de execução ao nível do núcleo
(em inglês: Kernel-Level Thread (KLT)).
Thread em modo kernel
2
Thread em modo usuário
As linhas de execução da primeira categoria são suportadas pela aplicação, sem conhecimento do
núcleo e geralmente são implementadas por pacotes de rotinas (códigos para criação, término,
escalonamento e armazenagem de contexto) fornecidas por uma determinada biblioteca de uma
3
linguagem, como é o caso da thread.h (biblioteca padrão da linguagem C). Estas linhas de
execução suportam as mesmas operações que as linha de execução KLT (criação, juntar, duplicar
e desistir). Possuem como vantagens a possibilidade de implementação em sistemas operacionais
que não suportam nativamente este recurso, geralmente são mais rápidas e eficientes pois
dispensam o acesso ao núcleo, evitando assim mudança no modo de acesso e sua estrutura de
dados fica no espaço do usuário, levando a uma significativa queda de overhead, além de poder
escolher entre as diversas formas de escalonamento em que melhor se adequa.
O gerenciamento de linha de execução (KLT) não é realizado através do código do próprio
programa; todo o processo é subsidiado pelo SO. Esse modelo tem a vantagem de permitir o
suporte a multiprocessamento e o fato do bloqueio de uma linha de execução não acarretar
bloqueio de todo processo, não obstante, temos a desvantagem de ter que mudar o tipo de acesso
sempre que o escalonamento for necessário aumentando assim o tão temido overhead.
São quatro operações básicas do gerenciamento de linha de execução: criação, término, thread
join e thread yield.
Criação (thread creation)
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 (thread termination)
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
4
Junção (Thread Join)
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.
Thread Yield (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.
Escalonamento
Da mesma forma que os processos sofrem escalonamento as threads também tem a mesma
necessidade. Quando vários processos são executados em uma CPU eles dão a impressão que
estão sendo executados simultâneamente, com as threads ocorre o mesmo, elas esperam até
serem executadas, como esta alternância é muito rápida há impressão de que todas as threads são
executadas paralelamente.
Linha de execução ao nível do usuário
As ULT são escalonadas pelo programador, tendo a grande vantagem de cada processo usar um
algoritmo de escalonamento que melhor se adapte a situação, o sistema operacional neste tipo de
thread não faz o escalonamento, em geral ele não sabe que elas existem. Neste modo o
programador é responsável por criar, executar, escalonar e destruir a thread. Um exemplo prático
de processo chamado P1 que contém tais threads: P1T1, P1T2 e P1T3, quando o sistema
operacinal da a CPU para o processo P1 cabe a ele destinar qual thread será executada, caso esta
thread use todo processo do quantum, o sistema operacional chamará outro processo, e quando o
processo P1 voltar a executar, P1T1 voltará a ser executada e continuará executando até seu
5
término ou intervenção de P1, este comportamento não afetará outros processos pois o sistema
continua escalonando os processos normalmente.
Linha de execução ao nível do núcleo
As KLT são escalonadas diretamente pelo sistema operacional, comumente são mais lentas que
as Threads ULT pois a cada chamada elas necessitam consultar o sistema, exigindo assim a
mudança total de contexto do processador, memória e outros níveis necessários para alternar um
processo. Um exemplo prático de processo chamado P2 que contém as threads P2T1, P2T2 e
P2T3 e um processo chamado P3 que contém as threads P3T1, P3T2 E P3T3. O Sistema
Operacional não entregará a CPU ao processo e sim a uma thread deste processo, note agora que
o sistema é responsável por escalonar as threads e este sistema tem que suportar threads, a cada
interrupção de thread é necessário mudar todo o contexto de CPU e memória, porém as threads
são independentes dos processos, podendo ser executadas P3T2, P2T1, P2T2, P2T1,
P3T1,P2T3,P3T3, ou seja a ordem em que o escalonador do sistema determinar. Já com as
threads em modo usuário não se consegue ter a mesma independência, pois quando passamos o
controle ao processo, enquanto seu quantum for válido ele irá decidir que thread irá rodar. Um
escalonamento típico do sistema é onde o escalonador sempre escolhe a thread de maior
prioridade, que são divididas em duas classes: Real Time e Normal. Cada thread ganha uma
prioridade ao ser criada que varia de 0 a 31(0 é a menor e 31 maior), processos com prioridade 0
a 15(Real Time) tem prioridade ajustada no tempo de execução como nos processos de E/S que
tem a prioridade aumentada variando o periférico, processos com prioridade 16 a 31 são
executados até terminar e não tem prioridade alterada, mas somente uma thread recebe a
prioridade zero que é a responsável por zerar páginas livres no sistema. Existe ainda uma outra
classe chamada de idle, uma classe mais baixa ainda, só é executada quando não existem threads
aptas, threads dessa classe não interferem na performance.
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
6
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.
Modelo de Geração de Multithreads
Modelo Muitos-Para-Um
O modelo muitos-para-um mapeia muitos threads de nível de usuário para threads do kernel. O
gerenciamento dos threads é realizado no espaço do usuárioe assim é eficiente, mas o processo
inteiro ficará bloqueado. Além disso, como somente um thread pode acessar o kernel de cada
vez, múltiplos threads são incapazes de executar em paralelo em multiprocessadores.[1]
7
Modelo Um-Para-Um
O modelo um-para-um mapeia cada thread de usuário para um thread de kernel, gera mais
concorrência do que o modelo muitos-para-um. Permite a um outro thread ser executado,
enquanto um thread realiza uma chamada de sistema de bloqueio, ele também permite que
múltiplos threads executem em paralelo em multiprocessadores. A única desvantagem deste
modelo é que a criação de um thread de usuário requer a criação do correspondente thread de
kernel.
Modelo Muitos-Para-Muitos
O modelo muitos-para-muitos multiplexa muitos threads de nível de usuário para um número
menor ou igual de threads de kernel. O número de threads de kernel pode ser específico tanto
para uma aplicação em particular quanto para uma máquina em particular. Os desenvolvedores
podem criar tantos threads de usuário quantos forem necessários, e os correspondentes threads de
kernel podem executar em paralelo em um multiprocessador. Além disso, quando um thread
realiza uma chamada de sistema de bloqueio, o kernel pode agendar um outro thread para
execução.
Cancelamento
O cancelamento de threads corresponde à tarefa de terminar um thread antes que se complete.
Por exemplo, se múltiplos threads estão pesquisando concorrentemente em um banco de dados e
um thread retorna o resultado, os threads que ainda estão sendo executados podem ser
cancelados. Uma outra situação pode ocorrer quando um usuário pressionar um botão em um
navegador da Web. Com frequência, uma página da Web é carregada em um thread separado.
Quando um usuário pressionar o botão stop, o thread que estava carregando a página é
cancelado. Um thread que está para ser cancelado é frequêntemente denominado thread-alvo.[1]
Exemplos
Java
import java.io.*;
public class Example implements Runnable
{
static Thread threadCalculate;
static Thread threadListen;
long totalPrimesFound = 0;
public static void main (String[] args)
{
Example e = new Example();
threadCalculate = new Thread(e);
8
threadListen = new Thread(e);
threadCalculate.start();
threadListen.start();
}
public void run()
{
Thread currentThread = Thread.currentThread();
if (currentThread == threadCalculate)
calculatePrimes();
else if (currentThread == threadListen)
listenForStop();
}
public void calculatePrimes()
{
int n = 1;
while (true)
{
n++;
boolean isPrime = true;
for (int i = 2; i < n; i++)
if ((n / i) * i == n)
{
isPrime = false;
break;
}
if (isPrime)
{
totalPrimesFound++;
System.out.println(n);
}
}
}
private void listenForStop()
{
BufferedReader input = new BufferedReader(new
InputStreamReader(System.in));
String line = "";
while (!line.equals("stop"))
9
{
try
{
line = input.readLine();
}
catch (IOException exception) {}
}
System.out.println("Found " + totalPrimesFound +
" prime numbers before you said stop");
System.exit(0);
}
}
C
Notas
Esta implementação depende do uso da biblioteca POSIX Threads.
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define THREADS_MAX
4
void *function(void *param)
{
int id = (int)param;
int i, loops = 10;
for(i = 0; i < loops; i++)
{
printf("thread %d: loop %d\n", id, i);
}
pthread_exit(NULL);
}
int main(void)
{
pthread_t threads[THREADS_MAX];
int i;
printf("pre-execution\n");
for (i = 0; i < THREADS_MAX; i++)
10
{
pthread_create(&threads[i], NULL, function, (void *)i);
}
printf("mid-execution\n");
for (i = 0; i < THREADS_MAX; i++)
{
pthread_join(threads[i], NULL);
}
printf("post-execution\n");
return EXIT_SUCCESS;
}
C++
Notas
Esta implementação depende do uso da biblioteca Boost.
#include <iostream>
#include <boost/thread/thread.hpp>
using namespace std;
const int THREADS_MAX = 4;
struct function
{
function( const int &_id )
: id( _id )
{ }
void operator()()
{
for( int i = 0; i < 10; ++i )
{
cout << "thread " << id << ": loop " << i << endl;
}
}
private:
const int id;
};
11
int main(void)
{
boost::thread_group threads;
cout << "pre-execution" << endl;
for( int i = 0; i < THREADS_MAX; ++i )
{
threads.create_thread( function( i ) );
}
cout << "mid-execution" << endl;
threads.join_all();
cout << "post-execution" << endl;
return 0;
}
Ruby
count = 0
a = Thread.new { loop { count += 1 } }
sleep(0.1)
Thread.kill(a)
puts count
#=> 93947
Delphi
unit UExemplo;
interface
uses
Classes, Generics.Collections;
type
TThreadExemplo = class(TThread)
private
FPrimeiroNumero: Integer;
FUltimoNumero: Integer;
FListaPrimos : TList<Integer>;
function IsPrimo(const pNumero : Integer) : Boolean;
protected
procedure Execute; override;
public
constructor Create(const pCreateSuspended: Boolean; const
pPrimeiroNumero, pUltimoNumero: Integer);
destructor Destroy; override;
12
property PrimeiroNumero: Integer read FPrimeiroNumero write
FPrimeiroNumero;
property UltimoNumero: Integer read FUltimoNumero write FUltimoNumero;
function GetListaAsString: String;
end;
implementation
uses
SysUtils;
{ TThreadExemplo }
constructor TThreadExemplo.Create(const pCreateSuspended: Boolean; const
pPrimeiroNumero, pUltimoNumero: Integer);
begin
inherited Create(pCreateSuspended);
FListaPrimos := TList<Integer>.Create;
FPrimeiroNumero := pPrimeiroNumero;
FUltimoNumero := pUltimoNumero;
end;
destructor TThreadExemplo.Destroy;
begin
FListaPrimos.Free;
inherited;
end;
procedure TThreadExemplo.Execute;
var
lNumero: Integer;
begin
lNumero := FPrimeiroNumero;
while not Terminated and (lNumero <= FUltimoNumero) do
begin
if IsPrimo(lNumero) then
begin
FListaPrimos.Add(lNumero);
end;
Inc(lNumero);
end;
13
end;
function TThreadExemplo.GetListaAsString: String;
var
lNum: Integer;
begin
Result := EmptyStr;
for lNum in FListaPrimos do
begin
Result := Result + IntToStr(lNum) + sLineBreak;
end;
end;
function TThreadExemplo.IsPrimo(const pNumero: Integer): Boolean;
var
lNum: Integer;
lMax: Integer;
begin
Result := True;
lNum := 2;
lMax := (pNumero div 2);
while Result and not Terminated and (lNum <= lMax) do
begin
Result := pNumero mod lNum <> 0;
Inc(lNum);
end;
end;
end.
14
Download