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:

  1. Process ID, process group ID, group ID
  2. Ambiente
  3. Directoria de trabalho
  4. Instruções do programa
  5. Registos
  6. Pilha (Stack)
  7. Espaço de endereçamento comum, dados e memória
  8. Descritores de ficheiros
  9. Comunicação entre processos (pipes, semáforos, memória partilhada, filas de mensagens)

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:

  1. Pilha
  2. Propriedades de escalonamento
  3. Dados específicos do thread

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:

  1. as alterações feitas por um thread num recurso partilhado (ex: fechar um ficheiro) serão "vistaa" pelos outros threads.
  2. Dois apontadores com o mesmo valor, apontam para os mesmos dados.
  3. É possível ler e escrever nas mesmas posições de memória, sendo por isso necessária uma sincronização explícita que tem de ser feita pelo programador.

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:

  1. ganhos nas performances dos programas;
  2. quando comparados com a criação e gestão de um processo, os threads podem ser criados com muito menos sobrecarga para o sistema operativo. A gestão dos threads necessita de menos recursos que a gestão dos processos;
  3. todos os threads num processo partilham o mesmo espaço de endereçamento. A comunicação entre threads é mais eficiente e na maior parte das situações mais fácil do que a comunicação entre processos;
  4. As aplicações com threads são mais eficientes e têm mais vantagens práticas:

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

  1. Esta função cria um novo thread e torna-o executável. Os threads podem criar outros threads.
  2. A função retorna a identificação do thread através do parâmetro thread. Esta identificação pode ser utilizada para efectuar várias operações nesse thread.
  3. attr é usado para definir as propriedades do thread. Podem-se especificar os atributos ou o NULL para os valores por defeito.
  4. função é o nome da função que vai ser executada pelo thread.
  5. Pode-se passar um argumento para a função através de argumento. Deve-se fazer sempre o cast para o apontador de um void. Para passar mais do que um argumento, pode-se utilizar uma estrutura que contenha todos os argumentos e depois passar um apontador para essa estrutura.

 

 

Terminar a execução de um thread

Existem várias maneiras de um thread terminar:

  1. O thread retorna da sua função inicial (a função main para o thread inicial).
  2. O thread executa a função pthread_exit
  3. O thread foi cancelado por outro thread através da função pthread_cancel
  4. O thread recebeu um sinal que fez com que terminasse;
  5. Todo o processo foi terminado devido à utilização da função exec (que substitui a imagem do processo) ou através da função exit (que termina um processo!)

pthread_exit(estado)

  1. Esta função termina explicitamente um thread, Utiliza-se esta função depois de um thread já ter completado o seu trabalho.
  2. Se o main() termina antes dos threads que foram criados, e termina através da utilização de pthread_exit(), os outros threads continuarão a ser executados, caso contrário terminam automaticamente quando main() termina.

 

 

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

  1. retorna o identificador desse thread.

pthread_equal()

  1. compara dois identificadores de threads. Se são diferentes retorna 0, senão retorna um valor diferente de 0.

 

Sincronização de threads

pthread_join(thread_id, estado)

  1. bloqueia o thread que chamou esta função até que o thread com o identificador thread_id termine.
  2. também é possível obter o estado do thread que terminou, se esse terminou com a função pthread_exit(estado).
  3. Tem um conportamento semelante à função wait utilizadanos processos.

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:

  1. Detached – não pode ser feito o join (PTHREAD_CREATE_DETACHED);
  2. Undetachedpode ser feito o join (PTHREAD_CREATE_JOINABLE)

 

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:

  1. Declarar a variável com o tipo de dados pthread_attr_t ;
  2. Inicializar a variável com a função pthread_attr_init
  3. Definir o estado do atributo com a função pthread_attr_setdetachstate;
  4. Libertar os recursos utilizados por essa variável com a função pthread_attr_destroy

 

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

}

 

Variáveis de exclusão mútua

 

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

}

 

Variáveis de Condição

 

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.