Threads
Um thread é uma sequência de instruções que
vão ser executadas num programa. No ambiente UNIX, os threads encontram-se
dentro de um processo, utilizando os recursos desse processo. Um processo pode
ter vários threads.
Pode-se dizer que um thread é um procedimento
que é executado dentro de um processo de uma forma independente. Para melhor
perceber a estrutura de um thread é útil entender o relacionamento entre um
processo e um thread. Um processo é criado pelo sistema operativo e podem
conter informações relacionadas com os recursos do programa e o estado de
execução do programa:
Os threads utilizam os recursos de um
processo, sendo também capazes de ser escalonados pelo sistema operativo e
serem executados como entidades independentes dentro de um processo.
Um thread pode conter um controlo de fluxo
independente e ser escalonável, porque mantêm o seu próprio:
Um processo pode ter vários threads,
partilhando cada um os recursos do processo e são executados no mesmo espaço de
endereçamento. Com vários threads, temos em qualquer instante vários pontos de
execução.
Como os threads partilham os recursos de um
processo:
Todos os threads estão no mesmo nível
hierárquico. Um thread não mantém uma lista dos threads criados nem tem
conhecimento do thread que o criou.
Vantagens:
- Sobrecarregar o CPU com operações de entrada/saída de dados. Por exemplo, um
programa tem situações onde necessita de efectuar operações de entrada/saída de
dados muito demoradas. Enquanto um thread espera que a operação de
entrada/saída de dados termine, o CPU pode ser utilizado para efectuar outras
operações através de vários threads;
- Tratamento de eventos assíncronos. Por exemplo, um servidor web pode
transmitir dados de pedidos anteriores e ao mesmo tempo fazer a gestão dos
novos pedidos.
Gestão de Threads
Inicialmente, o processo apenas tem um thread
(main()). Todos os outros threads são criados explicitamente pelo programador.
Criação de threads
pthread_create(thread,
attr, função, argumento)
Terminar a execução de um thread
Existem várias maneiras de um thread
terminar:
pthread_exit(estado)
Exemplo 1: Este exemplo cria três threads, cada um imprimindo a
mensagem "Sistemas Operativos II " e o número do thread, terminando
de seguida com a função pthread_exit()
/* Para compilar fazer: gcc -o fich fich.c
-lpthread */
#include <pthread.h>
#include <stdio.h>
void *escreve(void *numero)
{
int *n =(int *) numero;
printf("Sistemas Operativos II thread n. = %d\n",*n);
pthread_exit(NULL);
}
void main()
{
pthread_t threads[3];
int s,i;
for (i=0; i<3; i++)
{
printf("A
criar o thread n. %d\n",i);
s=pthread_create(&threads[i], NULL, escreve, (void *) &i);
if (s)
{
perror("Erro ao criar o thread");
exit(-1);
}
}
pthread_exit(NULL); /* se a função main não terminar com a função phtread_exit
o ultimo
thread
não chega a terminar a sua execução */
}
Nota: Neste
exemplo pode acontecer que apareça o mesmo número repetido (ex: 233). Isto
deve-se ao facto de quando o thread mostra o valor da variável i, este já foi
alterado. Para evitar esta situação deve-se utilizar um array de inteiros,
sendo cada posição utilizada apenas por um thread.
Funções para
identificação de threads
pthread_self()
pthread_equal()
Sincronização de threads
pthread_join(thread_id, estado)
Quando se cria um thread, um dos atributos
que pode ser alterado é se outros threads podem ou não chamar a função
pthread_join sobre ele. Para isso são possíveis dois valores:
Funções para manipulação de atributos
pthread_attr_init(attr)
pthread_attr_setdetachstate(attr, detachstate)
pthread_attr_destroy(attr)
pthread_detach(tread_id, estado)
Para utilizar
são necessários os seguintes passos:
Exemplo 2: Este exemplo espera que todos os threads terminem
através da utilização da função pthread_join.
#include <pthread.h>
#include <stdio.h>
void
*ocupa_tempo(void *null)
{
int i,j;
for (i=0;i<50000;i++)
j+=i;
pthread_exit(NULL);
}
void main()
{
pthread_t threads[5];
pthread_attr_t attr;
int s,i,estado;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
for(i=0; i<5; i++)
{
printf("A
criar o thread %d\n",i);
s=pthread_create(&threads[i], &attr,ocupa_tempo, NULL
);
if (s)
{
perror("Erro ao criar o thread");
exit(-1);
}
}
pthread_attr_destroy(&attr);
for(i=0; i<5; i++)
{
s=pthread_join(threads[i], (void **) &estado);
if (s)
{
perror("Erro no join");
exit(-1);
}
printf("O thread %d terminou com o estado %d\n",i,estado);
}
pthread_exit(NULL);
}
As variáveis de exclusão mútua (mutex) são uma das
formas de obter sincronização entre threads.
Actuam como um semáforo com um único recurso, fechando e protegendo o acesso a
um recurso partilhado. Num dado instante apenas um thread consegue “fechar” uma variável de exclusão mútua. Estas
variáveis podem ser usadas para evitar “race conditions”. Segue-se um exemplo
de uma “race condition” numa transacção bancária assumindo que o balanço
inicial é 10000.00:
Instante |
Thread 1 |
Thread 2 |
Balanço |
1 |
A ¬ balanço |
|
10000.00 |
2 |
|
B ¬ balanço |
10000.00 |
3 |
|
balanço¬ B – 5000.00 |
5000.00 |
4 |
balanço¬ A + 6000.00 |
|
16000.00 |
Neste exemplo deveria ser usada uma variável
mutex para “fechar” o “balanço” enquanto é utilizado por cada thread, de modo a
evitar situações de incoerência (neste caso o resultado correcto é 11000$00 !).
Funções
para a criação/destruição de variáveis mutex:
pthread_mutex_init(mutex,attr)
1.
cria e inicializa uma variável
mutex, definindo os seus atributos de acordo com attr;
2.
As variáveis mutex devem ser
do tipo pthread_mutex_t;
3.
attr é utilizado para definir as propriedades da
variável mutex, e deve ser do tipo pthread_mutexattr_t
se for usada (para utilizar os valores por defeito, pode-se usar NULL em
substituição de attr)
pthread_mutex_destroy(mutex)
4.
destrói a variável mutex.
pthread_mutexattr_init(attr)
1.
inicializa attr
pthread_mutexattr_destroy(attr)
2.
remove attr
Funções para a abrir/fechar
varáveis mutex:
pthread_mutex_lock(mutex)
3.
fecha a variável mutex. Se já
estiver “fechada” a função bloqueia até que seja possível fechar.
pthread_mutex_trylock(mutex)
4.
tenta fechar a variável mutex.
Se já estiver “fechada” esta função não bloqueia e devolve um valor de erro (EBUSY).
pthread_mutex_unlock(mutex)
5.
“abre” uma variável mutex.
Esta função dará erro se a variável mutex já estiver aberta ou se tentar abrir
uma variável mutex que foi “fechada” por outro thread.
Exemplo 3: Este exemplo utiliza variáveis de exclusão mútua para
garantir a coerência dos dados.
#include <pthread.h>
#include <stdio.h>
/* definição das
variáveis globais */
int balanco=10000;
pthread_t threads[2];
pthread_mutex_t mutex_balanco;
void *deposita(void
*valor)
{
int *a= (int *) valor;
/*fecha a variável mutex_balanco . Se já
estiver “fechada” espera até
que
seja possível fechar*/
pthread_mutex_lock(&mutex_balanco);
balanco
+= *a;
/*
“abre” a variável mutex_balanco */
pthread_mutex_unlock(&mutex_balanco);
pthread_exit(NULL);
}
void *levanta(void
*valor)
{
int *b= (int *) valor;
/*fecha a variável mutex_balanco . Se já
estiver “fechada” espera até
que
seja possível fechar*/
pthread_mutex_lock(&mutex_balanco);
balanco
-= *b;
/*
“abre” a variável mutex_balanco */
pthread_mutex_unlock(&mutex_balanco);
pthread_exit(NULL);
}
void main()
{
int
i,s,estado,depositar=6000,levantar=5000;
/* inicializa a variável mutex_balanco
utilizando os valores por defeito */
pthread_mutex_init(&mutex_balanco,
NULL);
pthread_create(&threads[0],NULL,deposita,(void *)&depositar);
pthread_create(&threads[1],NULL,levanta,(void *)&levantar);
for(i=0; i<2;
i++)
{
s=pthread_join(threads[i], (void **) &estado);
if (s)
{
perror("Erro no join");
exit(-1);
}
printf("O thread %d terminou com o estado %d\n",i,estado);
}
printf("O
balanço é = %d\n",balanco);
pthread_mutex_destroy(&mutex_balanco);
pthread_exit(NULL);
}
As variáveis de condição possibilitam outro modo de sincronização de
threads. Enquanto as variáveis de exclusão mútua implementam a sincronização
através do controlo do acesso aos dados, as variáveis de condição permitem a
sincronização de trheads através do valor desses dados. Sem a utilização destas
variáveis, os threads têm que estar sempre a verificar se uma dada variável tem
um valor específico, ocupando assim tempo de processamento, pois os threads
estão constantemente ocupados com esta actividade. Uma variável de condição
obtém os mesmos resultados sem a necessidade de estar sempre a verificar o
valor.
Nota: Uma variável de condição é sempre usada em conjunto com o fecho de
uma variável de exclusão mútua.
Funções
para a criação/destruição de variáveis de condição:
pthread_cond_init(variável_de_condição,attr)
6.
cria e inicializa uma variável
de condição, definindo os seus atributos de acordo com attr;
7.
As variáveis de condição devem
ser do tipo pthread_cond_t;
8.
attr é utilizado para definir as propriedades da
variável de condição, e deve ser do tipo pthread_mutexattr_t
se for usada (para utilizar os valores por defeito, pode-se usar NULL em
substituição de attr)
pthread_cond_destroy(variável_de_condição)
9.
destrói a variável de
condição.
pthread_condattr_init(attr)
10.
inicializa attr
pthread_condattr_destroy(attr)
11.
remove attr
Funções para a utilização das varáveis de condição:
pthread_cond_wait(variável_de_condição,mutex)
12.
bloqueia o thread até que a
variável de condição seja “sinalizada”. Esta função deve ser chamada quando a
variável de exclusão mútua se encontra “fechada”. Esta função “abre”
automaticamente a variável mutex de modo a ser utilizada por outros threads,
“adormecendo” de seguida o thread.
13.
Quando chega o “sinal”, fecha
a variável mutex.
pthread_cond_signal(variável_de_condição)
14.
É utilizada para “sinalizar”
(ou acordar) um thread que está à espera numa variável de condição. Deve ser
chamada quando a variável de exclusão mútua se encontra “fechada” e deve ser
“aberta” a variável de exclusão mútua para permitir que a função
pthread_cond_wait do outro thread prossiga.
pthread_cond_broadcast(variável_de_condição)
15. Deve ser utilizada em substituição
da função pthread_cond_signal, quando temos mais que um thread à espera.
Exemplo 4: Este exemplo
demonstra a utilização de variáveis de
condição. São utilizados dois threads para incrementar a variável
“contador”
#include
<pthread.h>
#include
<stdio.h>
/* definição das
variáveis globais */
int contador=0;
pthread_cond_t condicao_contador;
pthread_t threads[3];
pthread_mutex_t mutex_contador;
void *incrementa(void *valor)
{
int i,j;
for(i=0;i<50;i++)
{
pthread_mutex_lock(&mutex_contador);
contador++;
printf(“O valor do contador e’
%d\n”,contador);
if (contador == 10)
{
pthread_cond_signal(&condicao_contador);
printf(“O valor do contador e’
10\n”);
}
pthread_mutex_unlock(&mutex_contador);
for(j=0;j<2000;j++) ;
}
pthread_exit(NULL);
}
void *espera(void *valor)
{
printf(“O
thread está à espera que o valor do contador seja 10\n”);
/*
Fecha a variável mutex e espera pelo “sinal”. Nota: a função pthread_cond_wait
“abre” automaticamente a variável mutex para que possa ser usada por outros
threads enquanto espera. Também é necessário salientar que se o valor de
contador já for 10, o ciclo não é feito, evitando assim uma situação em que o
thread ficaria eternamente à espera de um “sinal” */
pthread_mutex_lock(&mutex_contador);
while
(contador < 10)
{
pthread_cond_wait(&condicao_contador,&mutex_contador);
/* quando esta função receber o “sinal”,
fecha a variável mutex */
printf(“O thread recebeu o ‘sinal’\n”);
}
printf(“O
contador tem o valor 10\n”);
pthread_mutex_unlock(&mutex_contador)
pthread_exit(NULL);
}
void main()
{
int i,s,estado;
/* inicializa a variável mutex_balanco
utilizando os valores por defeito */
pthread_mutex_init(&mutex_contador,
NULL);
pthread_cond_init(&condicao_contador,NULL);
pthread_create(&threads[0],NULL,incrementa,NULL);
pthread_create(&threads[1],NULL,incrementa,NULL);
pthread_create(&threads[2],NULL,espera,NULL);
for(i=0; i<3;
i++)
{
s=pthread_join(threads[i], (void **) &estado);
if (s)
{
perror("Erro no join");
exit(-1);
}
printf("O thread %d terminou com o estado %d\n",i,estado);
}
pthread_mutex_destroy(&mutex_contador);
pthread_cond_destroy(&condicao_contador);
pthread_exit(NULL);
}
Exercícios
1-
Considere que tem definido na função main duas strings em que a
primeira tem o seu primeiro nome e a segunda o último. Faça uma função que
receba como parâmetros uma string e escreva essa string no monitor.
Implemente um programa em que cada string é apresentada por um thread
diferente.
2-
Considere que tem um array com cinco posições, sendo cada posição constituída
pelo número, nome e morada. Faça uma função que recebe como parâmetros uma
estrutura desse tipo e imprima o conteúdo dessa estrutura. Implemente um
programa que crie 5 threads, "passando" como argumentos uma estrutura
desse array (Ex: 0 primeiro thread fica com a primeira posição do array, etc.)
3-
Tendo um array de inteiros com 1200 posições, pretende-se calcular resultado[i]
= dados[i]*4 + 20. Crie dois threads :
Depois dos cálculos terminarem, devem ser apresentados os
valores no monitor (pode ser feito na função main).
4-
Dado um array de inteiros com mil posições, crie 5 threads:
Nota: O array não tem números repetidos.
5-
Para verificar se os montantes depositados pelos seus clientes continuam os
mesmos depois de efectuar alterações à base de dados, pretende-se que calcule a
soma total de todos os saldos dos clientes de um sistema bancário. Assuma que
esses valores estão num array de 1000 posições e os cálculos devem ser feitos
por 5 threads. Cada thread faz a soma de 200 posições somando depois esse valor
ao saldo total que deve ser apresentado na função main.
Nota: tenha em atenção a coerência
dos dados.
6-
Uma empresa de estatística pretende obter quantas vezes cada número foi
sorteado no totoloto. Para isso dispõe de uma base de dados com 10000 chaves.
Utilize dez threads para calcular quantas vezes cada número foi sorteado. O
thread principal deve depois imprimir os resultados.
Nota: tenha em atenção que em cada instante apenas um thread
deve incrementar o contador de um determinado número (ex: se dois threads
encontraram o número 5, apenas um deles deverá incrementar o contador
relacionado com o número 5, estando o outro à espera)
Sugestão: Utilize um array de variáveis de exclusão mútua para
garantir a coerência dos dados.
7- Uma empresa pretende simular através da utilização de threads
o funcionamento das seguintes viagens : cidadeA-cidadeD e cidadeC-cidadeA.
Para a simulação tenha em atenção os seguintes aspectos:
-
Em cada instante, apenas um comboio pode
usar uma das linhas (cidadeA-cidadeB, cidadeB-cidadeC, cidadeB-cidadeD);
-
Quando um comboio ocupa uma linha, deve
apresentar no monitor o número do comboio, a sua origem/destino e a linha onde
se encontra;
-
Quando um comboio termina a viagem deve
apresentar no monitor a hora de partida e a hora de chegada. Para isso, considere
que já existe a função hora() – que devolve uma string com a hora actual
(hh:mm:ss);
Considere que se pretendem simular 10 comboios a efectuar a
viagem cidadeA-cidadeD e 7 a fazer a viagem cidadeC-cidadeA.
Para cada “comboio”, utilize um thread para simular a
viagem.
8-
Considere um sistema de gestão de stocks armazenado num array com 1000 posições
(cada uma tem o código do produto, o preço de custo, o preço de venda,
quantidade em stock). Utilize dois threads (cada um responsável por 500 posições)
para armazenar num outro array o código e preço dos produtos com valor superior
a 5 euros). Use outro thread para escrever as primeiras 10 posições desse array
(esse thread deve esperar que estejam ocupadas as primeiras dez posições do
array).
9-
Faça um programa que crie 5 threads. Cada thread é responsável por calcular 200
posições do array resultado (resultado[i] = dados[i]*2+10). Os threads devem
imprimir os resultados segundo a ordem do array.
10 - Um jornal desportivo encomendou à sua empresa uma sondagem para aferir a popularidade dos jogadores da selecção nacional. Foram efectuadas 10000 entrevistas, em que cada entrevistado escolhia o seu jogador preferido (através do número : 1 a 23), sendo esse resultado armazenado no array FAVORITO. Você foi encarregado de fazer um programa com os seguintes requisitos:
- crie 10 threads para processar os dados, isto é, cada thread é responsável por processar 1000 posições;
- Crie 23 threads para apresentar o resultado relativo a cada jogador sempre que este tenha mais 10 “votos” (ex: apresenta o resultado quando o jogador tem 10, 20, 30, etc.);
- Depois das 10 threads terminarem o processamento, deve ser apresentado o resultado total de cada jogador (nome e votação) na thread principal, terminando o processo.
Nota 1: tenha em atenção que a utilização de apenas uma variável de condição e uma variável de exclusão mútua produz resultados pouco eficientes!
Nota 2: Os nomes dos jogadores encontram-se no array NOMES.
11 - A brigada de trânsito instalou 100 postos de controlo de
velocidade em todo o país. Com o objectivo de aumentar a eficácia nesse
controlo, pretende desenvolver um sistema que verifique em simultâneo os
100 locais. Num determinado local, cada medição é efectuada por uma função que
obtém a matrícula e a velocidade de um automóvel. Se a velocidade for superior
a 90 km/h, deve armazenar esses dados no array INFRACÇOES. Defina também uma thread, que sempre que
exista um infractor em qualquer dos locais, “passe” a respectiva multa.
Os nomes dos 100 locais estão armazenados no array LOCAIS.
Considere que já existem definidas as seguintes funções:
-
obtem_velocidade(dados
*registo, char *local) – obtém a velocidade e a matrícula de um automóvel
no local local, colocando esse
resultado na variável registo
-
multa(dados
registo) – emite uma multa para o proprietário do automóvel.
typedef
struct {
int velocidade;
char matricula[20];
char local[50];
}
dados;
Nota:
Tenha em atenção que cada posto de controlo está constantemente a efectuar medições de velocidade.
Utilize
as primitivas de threads para resolver o problema.
12-
Uma empresa dispõe de quatro funções de cálculo avançado para desenvolver um
estudo relativo aos seus produtos. Para cada produto necessita de usar as
quatro funções de uma modo encadeado, isto é, primeiro executa a função “int
f1(int codigo)”, depois executa a função “int f2(int res)”, em que res é o
resultado da execução da função f1, depois executa a função “int f3(int res1)”,
em que res1 é o resultado da função f2, depois executa a função “int f4(int
res2)”, em que res2 é o resultado da função f3,
depois executa a função “int f3(int res3)”, em que res3 é o resultado da
função f4, depois executa a função “int f2(int res4)”, em que res4 é o
resultado da função f3, depois executa a função “int f1(int res5)”, em que res5
é o resultado da função f2 e por fim armazena o resultado da execução da função
f1 no array DADOS.
Considere
que existem 10 produtos e os seus códigos estão armazenados no array
DADOS, em que cada posição é do seguinte tipo:
typedef struct {
int código;
int resultado;
}
Para
resolver o problema deve criar 4 threads, em que cada uma das threads
será responsável por usar cada uma das quatro funções.
Nota:
tenha em atenção que a segunda thread só chama a função f2, quando for
“informada” que já o pode fazer. O mesmo se passa para as restantes threads. A
primeira thread só inicia o cálculo de um novo produto quando a quarta thread a
“informar”…
Utilize
variáveis de condição e exclusão mútua para resolver o problema.
Sugestão:
utilize mais de uma variável de condição.
Implemente
o programa.