Sistemas Operacionais Programação Concorrente Introdução Edson Moreno edson.moreno@pucrs.br http://www.inf.pucrs.br/~emoreno
Introdução Programa Seqüencial Representado por apenas um processo Existe apenas um fluxo de execução Programas concorrentes Representado por dois ou mais processos que coexistem em um dado momento Se há cooperação, exige interação entre processos para sincronização Descrição reflete em múltiplos fluxos possíveis de execução Paralelismo Garantido em nível de hardware Paralelismo real Ocorre em plataformas que garantem mais de um elemento de processamento Paralelismo aparente Ocorrem em máquinas monoprocessadas, o que é emulado pelo sistema operacional
Programação Concorrente Um programa descrito de forma concorrente possui Um conjunto de processos de execução seqüencial Processos executados de forma concorrente Concorrência: Tradicionalmente significa: Competir por algo (recurso) Alternativamente significa: Cooperar por algo (fim comum)
Programação Concorrente Objetivos da exploração de paralelismo Reduzir o tempo total de processamento Execução em múltiplos processadores Aumentar confiabilidade e disponibilidade Emprego de processadores distribuídos Obter especialização de serviços Sistemas operacionais Implementar aplicações distribuídas Correio eletrônico
Desvantagens Programação Complexa Paradigma de codificação visando paralelismo deve ser levado em conta Erros de programação Adiciona-se erros intrínsecos ao modelo Velocidade distinto para os processos de um programa concorrente Aspectos não determinísticos Difícil depuração
Fluxo de Execução Execução seqüencial Controle de fluxo de execução Seqüencial Condicional Iterativo Requisição de execução explícita:chamada de métodos implícita: ativação de exceções Ordem de execução Controlado pela aplicação Execução concorrente Processos são unidades autônomas Exemplo: threads Processos podem ser independentes E.g. execução de mesmo método por dois objetos Processos podem necessitar comunicação Exigirá mecanismo de compartilhamento de dado Ordem de execução Programa não controla
Fluxo de Execução Fluxo único de execução Vários fluxos de execução tarefa 1 tarefa 2 tarefa 1 tarefa 2 tarefa 3 tarefa 3 cada fluxo possui uma pilha de execução
Criação de Processos Processos podem executar a criação de mais processos Cria o conceito de árvore de processos Criação realizada a partir de chamadas de sistemas Processos executam em paralelo A não ser que o processo pai queira (explicitamente esperar pelo filho) Comandos de criação de processos UNIX: Fork() Windows CreateProcess()
Criação de Processos Inter-process Communication IPC Comunicação entre processos Pipes, message queues, sockets
Controle de processos Alguns comandos Wait: Força que o processo pai aguarde o fim da execução do processo filho Exit: Força a finalização do processo que o chamou Sleep: Força um processo a ser suspenso por um tempo determinado Exemplo: #include <stdio.h> #include <unistd.h> int main (){ int pid, status; if(fork()) { printf( Processo pai: aguardando pelo processo filho ); pid = wait(&status); printf( Pai:n O PID do meu filho eh (%d) e seu status eh (%d)", pid, status); } else{ printf( Processo Filho -- dormindo"); sleep(1); printf( Processo filho -- finalizando"); exit(0); } printf( Fim de execucao (%d), getpid()); }
Controle de processos Alguns comandos Kill: Força a finalização de todos processos associados ao processo que chamou Ver exemplos de código Exemplo: #include <stdio.h> #include <sys/types.h> #include <signal.h> int main (){ int id; id = fork(); if (id!= 0){ printf ( Eu sou o pai\n ); sleep(5); kill(id,sigkill); }else{ printf ( Eu sou o filho\n ); while(1); } }
Single Thread MS-DOS suporta um único processo de usuário e uma única thread. Alguns UNIX, suportam múltiplos processos de usuário mas com apenas uma thread por processo
Multithreading JRE é um processo único com múltiplas threads Múltiplos processos E threads são encontrados em Windows, Solaris, e muitos sistemas modernos UNIX
Processos e Threads Pense nos processos como se englobassem duas características: Domínio de recurso Execução/Escalonamento Um processo inclui um espaço de endereçamento virtual para guardar a imagem do processo: uma coleção da pilha com os dados do programa e atributos Execução de um processo segue um caminho de execução intercalado com outros processos Alguns SOs suportam múltiplas threads em um único processo. Processos leves também são escalonados A unidade que contém o domínio de recurso é conhecida como processo/tarefa
Threads vs processos Independente do processo, cada thread tem: Um estado de execução (running, ready, etc.) Contexto de thread salvo quando não executando Uma pilha de execução Espaço para variáveis locais estáticas Acesso à memória e recursos do processo ao qual pertence (todas as threads do processo compartilham isso)
Threads vs processos
Threads vs processos Relação entre um processo e suas threads Ações aplicadas a um processo pai afetem sua threads Thread somente é swapped out se o processo pesado for O OS deve gerenciar isso no nível de processo Ex.: Suspender um processo suspende todas as suas threads Terminar um processo termina todas as suas threads
Processos leves (Threads) Threads Mais leves do que processos Tem menos informações para salvar e restaurar para troca de contexto Podem comunicar-se através de variáveis globais ao processo Principal diferença entre processos e threads Espaço de endereçamento Processos Espaço de endereçamento único por processo Variáveis distintas para cada processo Threads Trabalham no mesmo espaço de endereçamento de um processo pai Compartilham as mesmas variáveis de um processo
Processos leves (Threads) Vantagens quando comparada a processos Menor tempo de criação, término e chaveamento entre threads Contexto de software é o mesmo do processo original Diferença está no grupo de informações referentes ao hardware Comunicam-se sem interferência do kernel Exploram o mesmo espaço de endereçamento na memória
Implementação de threads User Level Thread (ULT) Kernel level Thread (KLT) ou: kernel-supported threads
User-Level Threads Núcleo do sistema trata somente processos pesados Gerenciamento das threads realizada pela aplicação do usuário Programador responsável por Definir o modo de escalonamento Tratar os estados da thread Requer uma biblioteca de apoio Ex. Biblioteca Pthreads (Linux) Código deve dar suporte Criação / eliminação Passagem de mensagem entre threads Escalonar threads Salvar / restaurar contexto
User-Level Threads Vantagens Chaveamento entre threads não envolve kernel Escalonamento pode ser dependente da aplicação Pode rodar em qualquer SO Desvantagens Maioria das chamadas de sistema são bloqueantes Ao bloquear o processo pesado, todas threads são bloqueadas O núcleo somente associa processos pesados a processadores Duas threads de um mesmo processo não podem rodar em processadores diferentes
Kernel-Level Threads Gerência de processos feita no nível de núcleo Não emprega bibliotecas de threads Núcleo mantém informações de contexto para processos pesados e threads Escalonamento e chaveamento Realizado no nível de threads
Kernel-Level Threads Vantagens O kernel pode simultaneamente escalonar múltiplas threads do mesmo processo em múltiplos processadores Se uma thread em um processo é bloqueada, o kernel pode escalonar outra thread do mesmo processo. Desvantagens A transferência de controle de uma thread para outra do mesmo processo requer uma troca de modo para o kernel ULT são mais rápidas por serem tratadas pelo programador
Programação com Threads POSIX Threads (PTHREADS) Bibliotecas de threads POSIX são um padrão API para threads baseado em C/C++ Permite gerar fluxo de processos concorrentes Mais eficaz em sistemas multi-processadores ou multi-core Fluxo do processo pode ser programado para executar em outro processador Permite obter ganho de velocidade através do processamento paralelo ou distribuído. Ganhos também são encontrados em sistemas com um único processador Exploração da latência de I/O entre outras que podem parar a execução do processo Uma thread pode executar enquanto a outra está bloqueada. Threads mais leves que processos pesados Exigem menos sobrecarga do que "bifurcação (fork)" ou geração de um processo novo, O sistema não inicializa um espaço de memória virtual novo para o processo Todos as threads dentro de um processo compartilham o mesmo espaço de endereço.
Programação com Threads Codificação PTHREAD_CREATE Cria uma thread associada a um processo Dispara sua execução PTHREAD_JOIN Threads Faz com que o processo criador aguarde uma sinalização de fim de execução da thread Criadas a partir de funções Variáveis locais acessíveis por uma única thread Variáveis globais acessíveis por todas as threads #include <stdio.h> #include <pthread.h> void *Thread0() { } int i; for(i=0;i<100;i++) printf( Thread0 = %d\n, i); void *Thread1() { } int i; for(i=100;i<200;i++) printf( Thread1 = %d\n, i); main(){ } pthread_t t0, t1; pthread_create( &t0, NULL, Thread0, NULL); pthread_create( &t1, NULL, Thread1, NULL); pthread_join( t0, NULL); pthread_join( t1, NULL); printf( Main \n );
Programação com Threads Criação de uma Thread int = pthread_create(pthread_t *thid, const pthread_attr_t *atrib, void *(*funcao), void *args); pthread_t *thid: Identificador thread const pthread_attr_t *atrib: Escalonamento (null = default), void *(*funcao): Função que a implementa, void *args: Ponteiro para os parâmetros a serem passados para a thread O processo de criação pode Falhar Thread não é criada Sinais e seus significados: EAGAIN: O sistema sem recursos necessários para criar a nova thread EFAULT: O nome da função ou attr não é um ponteiro válido EINVAL: attr (atrib) não é um atributo inicializado Ocorrer com sucesso Retorna zero (0)
Programação com Threads Vinculação entre o processo e suas threads pthread_join (pthread _thid, void **args); Bloqueia até que a thread _thid termine Thread pode retornar valores em args 0, sucesso < 0, caso de falha Valor de retorno, calculado pela thread Caso não desejar valor de retorno, passar NULL