Thread (computação)

Origem: Wikipédia, a enciclopédia livre.
 Nota: Para outros significados de thread, veja thread (desambiguação).

Linha ou Encadeamento 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 concorrencialmente. O suporte à thread é fornecido pelo próprio sistema operacional, 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 thread permite, por exemplo, que o usuário de um programa 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 thread é processada de forma aparentemente simultânea, pois a mudança entre uma thread e outra é feita de forma tão rápida que para o utilizador, isso está acontecendo paralelamente. Em hardwares com múltiplos CPUs ou multi-cores, as threads são realizadas realmente de forma simultânea.

Os sistemas que suportam uma única thread (em real execução) são chamados de monothread enquanto que os sistemas que suportam múltiplas threads são chamados de multithread.

Exemplo

Um exemplo simples seria um jogo, que 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 alto-falantes; Porém, para sistemas com uma única CPU, cada linha de execução é processada por vez.

Particularidades

Cada thread 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 uma thread é muito menor do que o escalonamento de processos. Entretanto, algumas linguagens (C, por exemplo) não fornecem acesso protegido à memória nativa (sua implementação fica a cargo do programador ou de uma biblioteca externa) devido ao compartilhamento do espaço de memória.

Um dos benefícios do uso das threads advém do fato do processo poder ser dividido em várias threads; quando uma thread está à espera de determinado dispositivo de entrada/saída ou qualquer outro recurso do sistema, o processo como um todo não fica parado, pois quando uma thread entra no estado de 'bloqueio', uma outra thread aguarda na fila de prontos para executar.

Uma thread possui um conjunto de comportamentos padrão, normalmente encontrados em qualquer implementação ou sistema operativo.

Uma thread pode:

  • criar outra da mesma forma que um processo, através do método thread-create, onde a thread retorna um ID como primeiro argumento (resultado da função de criação);
  • esperar outra thread se sincronizar, através do método join;
  • voluntariamente "desistir" da CPU por não precisar mais do processamento proposto pela própria ou por vontade do utilizador. Feito através do método 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

Uma thread pode assumir os seguintes estados:

  • Unstarted: logo após ser criada (antes do Start());
  • Running: após ser ativada (Start()) ou após método Resume();
  • Suspended: após método Suspended();
  • Stopped: após método Abort().

ULT e KLT

Usualmente as threads são divididas em duas categorias: thread ao nível do utilizador (em inglês: User-Level Thread (ULT)), e thread ao nível do núcleo (em inglês: Kernel-Level Thread (KLT)).

Thread em modo usuário

Thread em modo kernel

As threads da primeira categoria (ULT) são suportadas pela aplicação, sem conhecimento do núcleo e geralmente são implementadas por pacotes de rotinas (códigos para criar, terminar, escalonamento e armazenar contexto) fornecidas por uma determinada biblioteca de uma linguagem, como é o caso da thread.h (biblioteca padrão da linguagem C). Estas threads suportam as mesmas operações que as threads KLT (criar, sincronizar, duplicar e abortar). Possuem como vantagens a possibilidade de implementação em sistemas operativos que não suportam nativamente este recurso, sendo geralmente mais rápidas e eficientes pois dispensam o acesso ao núcleo. Evita assim mudança no modo de acesso, e a estrutura de dados fica no espaço do utilizador, levando a uma significativa queda de overhead, além de poder escolher entre as diversas formas de escalonamento em que melhor se adequa.

A gestão da thread (KLT) não é realizada 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 facto 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.

Há quatro operações básicas na gestão de threads: criar, terminar, thread join e thread yield.

Criar (thread creation)

Basicamente uma thread pode criar outra(s), sendo que depois essas mesmas threads são executas 'simultaneamente'. A thread criadora é a thread-mãe e a thread criada é a thread-filho. Threads incluídas na função main quando executadas podem criar threads-filho. No diagrama a seguir há a thread A que 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 (por exemplo uma 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.

Terminar (thread termination)

Para maioria dos casos, as threads não são criadas e executadas eternamente. Depois de terminado o seu objectivo, a thread termina. No facto, a thread que criou estas duas threads-filho termina 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-mãe terminar, todas as threads filho terminam também. Porque isso é importante? Isso é importante porque as threads-filho compartilham recursos com a thread-mãe, incluindo variáveis. Quando a thread-mãe termina, todas as variáveis são perdidas e a thread-filho não poderá aceder aos recursos que a thread-mãe possuía. Assim, se a thread-mãe terminar 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
Sincronizar(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-filho. 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 joinaguardando o seu irmão (thread-filho) terminar. Em geral o thread join é utilizado para a thread-mãe se sincronizar com uma das threads-filho.

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 escalonar pouco a pouco outros ciclos da CPU, assim outros programas podem ser executados. Isso pode ser um problema de política de planeamento do Sistema Operativo. Entretanto, quando escrevemos programas com múltiplas threads, temos que fazer correctamente 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 têm a mesma necessidade. Quando vários processos são executados em uma CPU, eles dão a impressão que estão sendo executados simultaneamente. 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 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 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 mesma 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: multithreading preemptiva ou multithreadingcooperativa. A multithreading preemptiva é 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 threadspara ceder o controle, o que pode ser um problema caso uma tarefa “monopolize” o uso da CPU ou se houver espera pela disponibilidade de um recurso. A desvantagem da multithread preemptiva é 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 multithreadingcooperativa.

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ário e 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]

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.[1]

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.[1]

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; // Cria o thread.
  static Thread threadListen;
  long totalPrimesFound = 0;
    
  public static void main (String[] args)
  {
      Example e = new Example();
      
      threadCalculate = new Thread(e);
      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"))
      {
          try
          {
              line = input.readLine();
          }
          catch (IOException exception) {}
      }
      
      System.out.println("Found " + totalPrimesFound +
          " prime numbers before you said stop");
      System.exit(1);
  }
}

Java, exemplo simples em português

import java.util.logging.Level;
import java.util.logging.Logger;

class Threaded extends Thread {
    
    Synchronized1 base;
    
    public Threaded( Synchronized1 bse ) {
        this.base = bse;
    }
    
}

public class Synchronized1 {

    public Synchronized1() {
    }
    
    public void ini() {
        new Threaded( this ) {
            public void run() {
                while( true ) {
                    synchronized( base ) {
                        System.out.print( "Este é A, agora vai mostrar B.\n" );
                        try {
                        Thread.sleep(1000);
                    } catch (InterruptedException ex) {
                        Logger.getLogger(Synchronized1.class.getName()).log(Level.SEVERE, null, ex);
                    }
                        try {
                            base.notify();
                            base.wait();
                        } catch (InterruptedException ex) {
                            Logger.getLogger(Synchronized1.class.getName()).log(Level.SEVERE, null, ex);
                        }
                    }
                    
                }
            }
        }.start();
        
        
        new Threaded( this ) {
            public void run() {
                while( true ) {
                    synchronized( base ) {
                        System.out.print( "Este é B, então foi já mostrado A.\n" );
                          try {
                        Thread.sleep(1000);
                    } catch (InterruptedException ex) {
                        Logger.getLogger(Synchronized1.class.getName()).log(Level.SEVERE, null, ex);
                    }
                          try {
                            base.notify();
                            base.wait();
                        } catch (InterruptedException ex) {
                            Logger.getLogger(Synchronized1.class.getName()).log(Level.SEVERE, null, ex);
                        }
                    }
                  
                }
            }
        }.start();
    }
    
    public static void main(String[] args) {
        new Synchronized1().ini();
    }
}

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 c*param)
{
       int id = *((int *)param);
       int i, loops = 10; 
       for(i = 0; i < loo ps; i++)
       {cwegve
               printf("thread %d: loop %d\n", id, i);
       }bb
       pthread_exit(NULL);
}

int main(void)ef
       printf("pre-execution\n");
       for (i = 0bf; i < THREADS_MAX; i++)
       {
               pthread_create(&threads[i], NULL, function, &i);
       }be
       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;
};

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

Referências

  1. a b c d SILBERSCHATZ, Abraham; GALVIN, Peter Baer; GAGNE, Greg. Fundamentos de sistemas operacionais. 6. ed. Rio de Janeiro: LTC, 2004 580 p.