Programação Concorrente JAVA Prof. Alexandre Monteiro Recife ‹#› Contatos Prof. Guilherme Alexandre Monteiro Reinaldo Apelido: Alexandre Cordel E-mail/gtalk: [email protected] [email protected] Site: http://www.alexandrecordel.com.br/fbv Celular: (81) 9801-1878 Conteúdo O que é Motivação Conceitos Processos Threads Propriedades Safety Liveness Threads em JAVA Programação Concorrente [ O que é ] “Um programa concorrente é um conjunto de programas seqüenciais comuns que são executados em um paralelismo abstrato” (M.Bem-Ari) Programação Concorrente [ O que é ] “Um programa concorrente especifica 2 ou mais processos que cooperam para realizar uma tarefa. Processos cooperam através de comunicação; utilizam variáveis compartilhadas ou troca de mensagens“(G. R. Andrews) Programação Concorrente [ Motivação ] Aproveitar hardware com múltiplos processadores Atender a vários usuários simultaneamente Melhorar o desempenho das aplicações Aumentar a disponibilidade da CPU para o usuário Objetos ativos e controle de atividades Programas paralelos Programação Concorrente [ Conceitos ] Paralelismo • Processamento simultâneo físico Concorrência • Processamento simultâneo lógico (aparente) • Requer entrelaçamento (interleaving) de ações Processo • Execução de um programa (running) Programa Concorrente • Vários processos que cooperam para a realização de uma tarefa ou mais tarefas Programação Concorrente [ Conceitos ] Comunicação • Variáveis compartilhadas • Passagem de mensagens Sincronização • Exclusão mútua de seções críticas • Sincronização por condição Estado de um programa concorrente • Consiste dos valores das variáveis (explícitas e implícitas) • A execução de um comando muda o estado Ações atômicas • Transformação indivisível de estado Programação Concorrente [ Processos ] Um processo é um programa que está em algum estado de execução Tem espaço de endereçamento próprio, que é mapeado pelo S.O. para memória física Possui um fluxo de controle ou thread único Mantém um contador de programa (PC) que indica o endereço da próxima instrução A MMU (Memory Management Unit) traduz os endereços lógicos em endereços físicos, que normalmente não são contíguos (Memória Virtual) Programação Concorrente [ Processos ] Espaço de Endereçamento Lógico Pilha Espaço de Endereçamento Lógico de um Processo Heap Dados Globais Instruções Programação Concorrente [ Processos ] Tabela de Processos •Estado do processo •Valores dos registradores •Arquivos abertos •Alocação de memória •PID (Process ID) •UID (User ID) •GID (Owner’s Group ID) Programação Concorrente [ Processos ] Estados de um processo 1. Iniciado (Start): processo criado 2. Executando (Running): Utilizando a CPU 3. Executável ou Pronto (Runnable ou Ready): Esperando para ser escalonado para usar a CPU 4. Suspenso (Suspended): Recebeu um sinal para ser suspenso 5. Bloqueado (Blocked): Esperando pela conclusão de algum serviço solicitado ao S.O. 6. Encerrado (Dead): processo morto Programação Concorrente [ Processos ] Ativo Bloqueado Executável Executando Encerrado Iniciado Suspenso Programação Concorrente [ Threads ] Um processo pode ter mais de uma Thread (Linha) Cada Thread possui contador de programa e pilha próprios Quando um processo é escalonado para ser executado, uma das Threads entra em execução As Threads compartilham as variáveis globais do processo Programação Concorrente [ Threads ] Espaço de Endereçamento de um Processo Pilha Thread 1 Thread 2 Thread n Pilha Pilha Pilha Contador de Programa Contador de Programa Contador de Programa Heap Variáveis Globais Instruções Programação Concorrente [ Threads ] Vantagens sobre processos compartilhando memória •São muito mais leves de serem criadas •A troca de contexto é mais suave pois compartilha instruções, heap e variáveis globais •Facilitam o compartilhamento de memória Threads [ Ciclo de Vida ] Intervalo de tempo expirou 4.sleep() Dormindo 6.stop() 1.new Thread() escalonada Criada Encerrada Término 3.run() 2.start() Pronta Executando interrompida Operação de E/S concluída 5.notify() 5.notityAll() 4.wait() Esperando Bloqueada Operação de E/S iniciada Threads [ Sincronização ] Sincronizando Threads •Programação com múltiplas Threads requer bastante cuidado: - Acesso / Atualização de variáveis compartilhadas - Starvation (processo nunca é executado) - Deadlock (impasse e dois ou mais processos ficam impedidos de continuar suas execuções e ficam bloqueados) - Acesso a estados inválidos de outros objetos Threads [ Sincronização ] A sincronização baseia-se na idéia de que para acessar um método sincronizado ou um entrar em um bloco sincronizado, é preciso obter (ou já ter) o lock desse objeto. A Thread que conseguir esse lock é a única autorizada a acessar os recursos protegidos através de sincronização. Programação Concorrente [ Propriedades ] Safety: O programa nunca entra em um estado inconsistente Liveness: Em algum momento o programa entra em um estado consistente Correção Parcial: Se o programa terminar, o resultado está correto. Caso contrário, nunca pode dar o resultado correto Término: O programa termina eventualmente Ausência de Deadlock: Nunca todos os processos estarão bloqueados Correção Total: O programa sempre termina e produz o resultado correto Safety [ Introdução ] Objetos interagindo em múltiplas Threads normalmente provocam interferência Programas concorrentes devem possuir 2 propriedades: •Safety: Nada de mau acontecerá durante a execução do programa. •Liveness: Algo de bom acontecerá durante a execução do programa. Safety [ Imutabilidade ] Variáveis de instância constantes (final em Java) Encapsulamento Não requer sincronização Classes sem estado (stateless) •Como não há estado, não há interferência Safety [ Sincronização ] Um Objeto/variável sempre está pronto para sofrer modificações, mesmo quando ainda está no meio do processamento de alguma Acesso a estados inconsistentes devem ser evitados: •Conflitos de leitura/escrita: vários lêem enquanto um escreve •Conflitos de escrita/escrita: vários lêem e escrevem ao mesmo tempo Safety [ Sincronização ] No contexto de OO, podemos ter: •Sincronização Total - Objetos totalmente sincronizados. Podem fazer apenas uma operações por vez •Sincronização Parcial - Parte do objeto é sincronizado. Somente métodos sincronizados são travados Safety [ Contenção ] Baseia-se na idéia de manter referências únicas para objetos internos isolados dos demais. São acessados apenas através de métodos sincronizados do objeto no qual estão contidos, estando, portanto, protegidos. Safety [ Contenção ] •É preciso definir um protocolo que garanta a exclusividade do recurso null null serviço 1 vizinho vizinho serviço 0 serviço 2 vizinho vizinho serviço 3 null Recurso Liveness [ Falhas de Liveness ] São tão sérias quanto falhas de Safety São mais difíceis de identificar e evitar durante o projeto Tipos de falhas •Contenção: uma thread, apesar de estar pronta para executar, não executa porque outra tomou recursos (também conhecida como starvation ou adiamento infinito) Liveness [ Falhas de Liveness ] •Dormência: uma thread não pronta falha ao tentar passar para esse estado. •Deadlock: duas ou mais threads bloqueiamse em um ciclo vicioso enquanto tentam acessar travas sincronizadas necessárias para continuar suas atividades •Término prematuro: uma thread é parada ou encerrada prematuramente Threads JAVA ‹#› Definições Básicas Threads são sub-procesos no sistema operacional. É menos custoso gerenciar threads do que processos. As linguagens Java e Ada possuem funcionalidades MULTITHREADING na própria estrutura da linguagem. C e C++ necessitam de biblioteca especifica para processamento MULTITHREADING • Posix p_thread Diagrama de Estados Thread Java O diagrama mostra os estados de uma thread de Java pode estar e alguns métodos que podem ser usados para mudar de um estado para outro. New Thread Inicialização do thread - feita através do construtor Thread(). class MyThreadClass extends Thread{ ... } ... MyThreadClass myThread = new MyThreadClass(); Neste estado, nenhum recurso do sistema foi alocado para o thread ainda, assim, a partir daqui, tudo que você pode fazer é um start(), para ativar o thread, ou um stop(), para "matá-lo". A chamada de qualquer outro método não faz sentido e levantará a exceção IllegalThreadStateException. Runnable Este é o estado em que o thread está pronto para rodar. O método start() requisita os recursos do sistema necessários para rodar o thread e chama o seu método run(). O método run() é a "alma" de um thread; é neste método que definimos o que o thread vai executar. Thread myThread = new MyThreadClass(); myThread.start(); Falamos em "Runnable", ao invés de "Running", porque o thread pode não estar realmente sendo executado. Imagine um computador com um único processador - seria impossível executar todos os threads "Runnable" ao mesmo tempo. O que ocorre é que a CPU deve ser escalonada entre os vários threads. Quando um thread está "Running", ele está também "Runnable", e, as instruções do seu método run() é que estão sendo executadas pela CPU. Not Runnable O estado "Not Runnable" significa que o thread está impedido de executar por alguma razão. Existem 4 maneiras através das quais um thread ir para o estado "Not Runnable": •Alguém manda-lhe a mensagem suspend(). •Alguém manda-lhe a mensagem sleep(). •O thread bloqueia, esperando por I/O. •O thread usa seu método wait() para esperar por uma variável de condição. Not Runnable O exemplo abaixo coloca “myThread” para dormir por 10 segundos... Thread myThread = new MyThreadClass(); myThread.start(); try { myThread.sleep(10000); } catch (InterruptedException e){ } Not Runnable Cada uma destas maneiras tem a sua forma específica de sair do estado "Not Runnable". •Se o thread foi suspenso, alguém precisa mandar-lhe a mensagem resume(). •Se o thread foi posto para dormir, ele voltará a ser "Runnable" quando o número de milisegundos determinado passar. •Se o thread está bloqueado, esperando por I/O, a I/O precisa ser completada. •Se o thread está esperando por uma variável de condição, o objeto que a "segura" precisa liberá-la, através de um notify() ou de um notifyAll() Dead Um thread pode morrer de "causas naturais" (quando o seu método run() acaba normalmente) ou pode ser morto (AssAssINAdO pelo método stop()). Thread myThread = new MyThreadClass(); myThread.start(); public void run() { int i = 0; while (i < 100) { i++; System.out.println("i = " + i); } } Este thread vai morrer naturalmente quando o loop do run() acabar. Dead O que vai acontecer com este thread? Thread myThread = new MyThreadClass(); myThread.start(); try { Thread.currentThread().sleep(10000); } catch (InterruptedException e){ } myThread.stop(); Método yield() O método yield() Cede a CPU para outros threads Thread em Java Em Java, threads são implementadas como uma CLASSE •Pacote java.lang.Thread •É uma extensão da classe Thread •Contrutores: - public Thread (String nome_da_thread); - public Thread ( ); // o nome sera Thread-# • Thread-1, Thread-2,… Principais Métodos •run(): é o método que executa as atividades de uma THREAD. Quando este método finaliza, a THREAD também termina. •start(): método que dispara a execução de uma THREAD. Este método chama o método run( ) antes de terminar. •sleep(int x): método que coloca a THREAD para dormir por x milisegundos. Principais Métodos •join( ): método que espera o término da THREAD para qual foi enviada a mensagem para ser liberada. •interrupt( ): método que interrompe a execução de uma THREAD. •interrupted( ): método que testa se uma THREAD está ou não interrompida. Estados de uma Thread em Java nascimento Término do tempo de dormida start( ) Fim da E/S notify( ) notifyAll( ) pronta run( ) Alocar um processador executando E/S wait( ) sleep( ) esperando dormindo stop( ) morta Fim do Método run( ) bloqueada Prioridade de Thread Em Java, a prioridade é determinada com um inteiro entre 1 e 10. A prioridade padrão é o valor 5. 10 é a maior prioridade e 1 é a menor. A THREAD herda a prioridade da THREAD que acriou. void setPriority(int prioridade); int getPriority( ); Algoritmo de Escalonamento Prioridade 10 Prioridade 9 A B C Prioridade 8 . . . Prioridade 3 D E Prioridade 2 Prioridade 1 G F Exercício 01 O programa cria 04 threads e as coloca para dormir. ThreadBasica é uma extensão da classe Thread. Exercício 01 •Analise como se chama o método sleep(). •Crie n THREADs, onde n é definido pelo usuário. •Utilize o método join no main para esperar as THREADs terminarem. try { uma_thread.join( ); // uma_thread.join(tempo) … }catch (InterruptedException e) { … } Escalonamento de Threads Prioridade 10 Prioridade 9 A B C Prioridade 8 . . . Prioridade 3 D E Prioridade 2 Prioridade 1 G F Exercício 02 Prioridades de Threads Utilize o método setPriority(int) para mudar a prioridade de threads •Utilize 01 thread com prioridade 1, 01 com prioridade 09 e as outras com prioridade 05. •Faça com que uma das threads de alta prioridade durma por 10 ms antes de terminar. •Faça com que outra thread de alta prioridade faça uma entrada de dado. Exercício 03 Problema Produtor X Consumidor - Com buffer de tamanho 1. - Variáveis compartilhadas. - A solução do problema seria utilizar-se duas THREADS: 01 consumidor e 01 produtor. - O que ocorre se não houver sincronização entre a leitura e escrita? Exercício 04 Problema do Produtor X Consumidor com sincronização do Buffer. Em Java, a sincronização entre threads é feita através do conceito de monitores. Monitor é um agrupamento de funções, cujas execuções não podem se dar de forma concorrente. Exercício 04 Utilizar os métodos multuamente excludentes de um objeto como do tipo synchronized em Java. Utilizar os métodos wait( ) e notify( ) para bloquear e liberar, respectivamente, as threads. Resolver o problema de Produtor-Consumidor. A interface Runnable A solução encontrada em Java foi a utilização de uma interface: Runnable •No caso, tem-se de implementar esta interface, que possui o método run( ). public class Filho extends Pai implements Runnable • Ao implementar uma interface, a classe se capacita a ser tratada como se fosse um objeto do tipo da inteface implementada. - Se a classe Filho implementar a interface Runnable, ela pode ser tratada como tal. A interface Runnable Para utilizar multithreads em Java é necessário instanciar um objeto de uma classe que estende a classe básicaThread, certo? Uma vez que Java não possui herança múltipla, como eu posso utilizar um objeto, cuja classe já é derivada, como no caso da ClasseThread? public class Filho extends Pai extends Thread { ………………. } // isto nao eh possivel em Java A interface Runnable Cria-se uma thread (Classe Thread), passando para o seu construtor uma referência do objeto que implementa a interface Runnable. Thread uma_Thread = new Thread(Runnable obj_thread) Thread uma_Thread = new Thread(Runnable obj_thread, String nome_da_thread) Exercício 5 Crie um programa onde o usuário possa escolher a quantidade de processos a serem criados para execução em concorrência. Estes devem ser nomeados e atribuída prioridades valores de 1 até um valor máximo 10, passado para cada um deles. Quando o processo atingir o número correspondente a metade do valor máximo do número de processos, deve entrar em espera de um tempo calculado de maneira randômica entre 1 e 5 segundos e posteriormente ser liberado. O programa deve ter um menu com as seguintes opções: 1) Criação de processos (qtd e nomes) 2) Prioridades dos processos (int de 1 a 10) Exercício 6 Veja a classe a seguir É uma classe que implementa Runnable e, no método run(), apenas imprime dez mil números. Vamos usá-las duas vezes para criar duas threads e imprimir os números duas vezes simultaneamente: Exercício 6 Exercício 6 Se rodarmos esse programa, qual será a saída? De um a mil e depois de um a mil? Provavelmente não, senão seria sequencial. Ele imprimirá 0 de t1, 0 de t2, 1 de t1, 1 de t2, 2 de t1, 2 de t2 e etc? Exatamente intercalado? Na verdade, não sabemos exatamente qual é a saída. Rode o programa várias vezes e observe: em cada execução a saída é um pouco diferente. Depois mude a prioridade e veja como ocorre a execução. Exercício 6 O problema é que no computador existe apenas um processador capaz de executar coisas. E quando queremos executar várias coisas ao mesmo tempo, e o processador só consegue fazer uma coisa de cada vez? Entra em cena o escalonador de threads. O escalonador (scheduler), sabendo que apenas uma coisa pode ser executada de cada vez, pega todas as threads que precisam ser executadas e faz o processador ficar alternando a execução de cada uma delas. A ideia é executar um pouco de cada thread e fazer essa troca tão rapidamente que a impressão que fica é que as coisas estão sendo feitas ao mesmo tempo. Exercício 6 O escalonador é responsável por escolher qual a próxima thread a ser executada e fazer a troca de contexto (context switch). Ele primeiro salva o estado da execução da thread atual para depois poder retomar a execução da mesma. Aí ele restaura o estado da thread que vai ser executada e faz o processador continuar a execução desta. Depois de um certo tempo, esta thread é tirada do processador, seu estado (o contexto) é salvo e outra thread é colocada em execução. A troca de contexto é justamente as operações de salvar o contexto da thread atual e restaurar o da thread que vai ser executada em seguida. Quando fazer a troca de contexto, por quanto tempo a thread vai rodar e qual vai ser a próxima thread a ser executada, são escolhas do escalonador. Nós não controlamos essas escolhas (embora possamos dar "dicas" ao escalonador). Por isso que nunca sabemos ao certo a ordem em que programas paralelos são executados. Exercício 6 Todo esse processo é feito automaticamente pelo escalonador do Java (e, mais amplamente, pelo escalonador do sistema operacional). Para nós, programadores das threads, é como se as coisas estivessem sendo executadas ao mesmo tempo. Referências Concurrent Programming - Hartley (capítulo 1) Concurrent Programming in Java - Lea (capítulo 1, 2 e 3) Programação Concorrente JAVA Prof. Alexandre Monteiro Recife ‹#›