Programação Concorrente Processos Processo = Programa em execução. Um processo é constituído basicamente de: Código executável; Pilha de execução; Dados; Estado; Registradores (PC, ...); Prioridade, arquivos abertos, ... Cada processo tem sua própria CPU virtual. A CPU física é compartilhada entre os vários processos: Multiprogramação/Compartilhamento de tempo Pseudoparalelismo Um escalonador seleciona qual processo deve usar a CPU a cada momento e por quanto tempo. Mudança de Contexto A troca de um processo por outro na CPU chama-se mudança de contexto. Durante uma mudança de contexto: Estado atual do processo e seus registradores devem ser salvos; O mapa de memória deve ser salvo; Um novo processo deve ser escolhido; O novo processo precisa ser iniciado; Necessidade de troca de processos (swapping). A mudança de contexto fornece transparência de concorrência para o usuário, contudo, é uma atividade dispendiosa! Implementação de Processos O sistema operacional controla os processos a serem executados por meio do uso de uma Tabela de Processos, a qual contém uma entrada para cada processo. Cada entrada contém também todas as informações necessárias para a realização de mudanças de contexto: Contador de Programa Registradores Ponteiro de pilha Estado Variáveis globais Arquivos abertos Processos filhos Alarmes pendentes Sinais e tratadores de sinais Uso de recursos Com isso, pode-se suspender a execução de um processo e, em um momento mais adiante, reiniciar a sua execução a partir daquele ponto de parada. Threads Em uma máquina multiprocessada ou em um cluster de computadores, podemos ter vários processos (inclusive de uma mesma aplicação) rodando ao mesmo tempo (vale salientar que em máquinas monoprocessadas o que ocorre é um pseudoparalelismo). Entretanto, na prática, precisamos de uma granularidade ainda menor, isto é, precisamos de paralelismo dentro de um mesmo processo. Esse paralelismo interno ao processo é obtido com o uso de Threads. Alguns autores usam o termo processo leve (lightweight process) ao invés de thread. Na realidade, uma thread é análoga a um processo tendo em vista que ambos são uma execução sequencial de um conjunto de instruções. Contudo, uma thread é considerada leve pelo fato de rodar dentro do contexto do processo, aproveitando-se do compartilhamento dos recursos alocados para o ambiente do programa. Portanto, uma thread é um subconjunto das informações pertinentes a um processo e as threads de um mesmo processo compartilham recursos entre si: Itens por Processo Itens por Thread Espaço de endereçamento Variáveis globais Arquivos abertos Processos filhos Alarmes pendentes Sinais e tratadores de sinais Uso de recursos Contador de Programa Registradores Pilha Estado Cada thread tem sua própria pilha de execução. Threads são bem mais fáceis de criar e destruir, o que implica em um ganho de performance considerável. Implementação de Threads Há dois tipos de threads: de usuário e de sistema (núcleo). Threads de usuário: O sistema (núcleo) não sabe da existência de threads Núcleo gerencia processos monothread Cada processo tem sua própria tabela de threads Um sistema supervisor, dentro do processo, gerencia os threads Salva estado Faz escalonamento Melhor desempenho Threads devem se comportar educadamente -> Ceder a vez quando não puderem continuar. Threads de sistema: O núcleo conhece as threads e as gerencia Núcleo tem tabela de threads e tabela de processos Bloqueio de threads = chamadas ao sistema Núcleo escolhe outro thread do mesmo processo ou um thread de outro processo Programação Multithreaded em Java Grande parte das aplicações existentes usam o conceito de multitarefa (multithread). Por exemplo, quando estamos usando um navegador WEB (Internet Explorer, Netscape, ...), ao mesmo tempo em que estamos lendo um texto, também podemos estar efetuando o download de arquivos e ouvindo música. Para cada uma dessas atividades, o navegador cria threads que executarão em (pseudo)paralelamente. Nas próximas seções, estaremos vendo detalhes da implementação de aplicações multitarefa usando a linguagem Java. Criando uma subclasse de java.lang.Thread e rescrevendo o método run() A linguagem Java fornece uma classe chamada Thread a partir da qual o programador pode criar suas próprias threads. Para isso, é preciso que o programador estenda essa classe e implemente as devidas customizações, reescrevendo o método run(). public class SimpleThread extends Thread { public SimpleThread(String str) { super(str); } public void run() { for (int i = 0; i < 10; i++) { System.out.println(i + " " + getName()); try { sleep((long)(Math.random() * 1000)); } catch (InterruptedException e) {} } System.out.println("DONE! " + getName()); } } O método run() é onde colocamos toda a lógica de execução da thread. Em outras palavras, esse método é o coração da thread. Particularmente no exemplo acima, o método run() da classe SimpleThread contém um laço que é executado dez vezes. Em cada iteração, o método exibe na saída-padrão do sistema o número correspondente àquela iteração e o nome que foi associado à thread. Então, a thread entra em modo de espera durante um intervalo de tempo que vai de 0 a 1 segundo. Quando o laço termina, o método run imprime DONE! seguido do nome da thread. O programinha a seguir instancia duas threads da classe acima e inicia a execução das mesmas: public class TwoThreadsDemo { public static void main (String[] args) { new SimpleThread("Jamaica").start(); new SimpleThread("Fiji").start(); } } Um exemplo de saída durante a execução desse programinha poderia ser: 0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 9 Jamaica Fiji Fiji Jamaica Jamaica Fiji Fiji Jamaica Jamaica Fiji Jamaica Fiji Fiji Jamaica Jamaica Fiji Fiji Fiji 8 Jamaica DONE! Fiji 9 Jamaica DONE! Jamaica Implementando a interface Runnable Uma outra técnica que pode ser utilizada para implementação de threads na linguagem Java é o uso da interface Runnable. Vejamos a classe a seguir: import import import import java.awt.Graphics; java.util.*; java.text.DateFormat; java.applet.Applet; public class Clock extends Applet implements Runnable { private Thread clockThread = null; public void start() { if (clockThread == null) { clockThread = new Thread(this, "Clock"); clockThread.start(); } } public void run() { Thread myThread = Thread.currentThread(); while (clockThread == myThread) { repaint(); try { Thread.sleep(1000); } catch (InterruptedException e){ // the VM doesn't want us to sleep anymore, // so get back to work } } } public void paint(Graphics g) { // get the time and convert it to a date Calendar cal = Calendar.getInstance(); Date date = cal.getTime(); // format it and display it DateFormat dateFormatter = DateFormat.getTimeInstance(); g.drawString(dateFormatter.format(date), 5, 10); } // overrides Applet's stop method, not Thread's public void stop() { clockThread = null; } } Como a linguagem Java não permite herança múltipla, então, nesse caso, para se poder construir uma thread é necessário a implementação da interface Runnable.