Threads Walfredo Cirne [email protected] Memória Compartilhada Uma questão básica em computação paralela é como as tarefas se comunicam Uma alternativa é que as várias tarefas compartilhem a mesma memória Neste caso, as tarefas podem ser implementadas como processos ou threads Processos Threads Processos executam em espaço de endereçamento próprio, de modo a evitar interferência de outros processos Threads de um processo usam o mesmo espaço de endereçamento Threads são mais “leves” que processos Além do que, pode-se criar threads a nível de usuário (green threads) Processos [possível criação de seg. memória compartilhada] ppid = fork(); if (ppid < 0) { fork_error_function(); } else if (ppid == 0) { child_function(); } else { parent_function(); } Threads Threads de um processo usam o mesmo espaço de endereçamento O programador e a linguagem devem prover proteção contra interferências Algumas linguagens possuem modelo de concorrência próprio (ex: ADA e Java) Outras linguagens utilizam extensões para lidar com threads (ex: C tipicamente usa Posix) POSIX POSIX: portable operating system interface Objetivo: Portabilidade do código fonte quando serviços do sistema operacional se fazem necessários Padrão IEEE Aplicabilidade de Threads Threads são muito úteis para paralelismo de “pequena escala” I/O não blocado Interface com o usuário Construção de servidores Threads também são úteis em sistemas de tempo real, onde a concorrência é intrinseca ao problema Modelo de Programação Um modelo de programação paralelo deve implementar três primitivas básicas Criação de processos/threads Sincronização de processos Comunicação entre processos Em memória compartilhada, a comunicação é feita pela própria memória Necessidade de Sincronização Por exemplo, considere dois processos atualizando uma variável comum: x=x+1 Operações: Carregar x em um registro Incrementar registro Armazenar registro em x C + Posix: Criação de Threads Criação de thread via: int pthread_create ( pthread_t *thread_id, const pthread_attr_t *attributes, void *(*thread_function)(void *), void *arguments ); Termino quando função termina ou via: int pthread_exit (void *status); Um thread pode esperar por outro via: int pthread_join (pthread_t thread, void **status_ptr); Semáforo Semáforo S: variável não-negativa inteira que só pode se modificada pelos procedimentos up() e down() down(S) se S > 0: decremente S senão: bloqueia esperando up(S) up(S) se há alguém bloqueado: desbloqueie senão: incremente S Exemplo de Semáforo (* exclusão mútua *) var mutex: semaphore := 1; thread P1; statement X down(mutex); statement Y up(mutex); statement Z end P1; thread P2; statement A; down(mutex); statement B; up(mutex); statement C; end P2; Mutex Posix Mutex é um semáforo binário down() é chamado de lock() up() é chamado de unlock() Posix tem as seguintes operações: int pthread_mutex_init (pthread_mutex_t *mut, const pthread_mutexattr_t *attr); int pthread_mutex_lock (pthread_mutex_t *mut); int pthread_mutex_unlock (pthread_mutex_t *mut); int pthread_mutex_trylock (pthread_mutex_t *mut); int pthread_mutex_destroy (pthread_mutex_t *mut); Condições Mutex servem para proteção de regiões críticas Além disso, precisamos esperar que outro thread faça alguma coisa Resolvido em Posix através de condições Condições Posix int pthread_cond_init (pthread_cond_t *cond, pthread_condattr_t *attr); int pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mut); int pthread_cond_signal (pthread_cond_t *cond); int pthread_cond_broadcast (pthread_cond_t *cond); int pthread_cond_timedwait (pthread_cond_t *cond, pthread_mutex_t *mut, const struct timespec *abstime); int pthread_cond_destroy (pthread_cond_t *cond); Exemplo Posix Produtor/Consumidor retirado de www.uq.edu.au/~cmamuys/humbug/talks/ pthreads/pthreads.html Monitores Lidar com mutexes e condições é muito baixo nível e muito sujeito a erros Monitores encapsulam seções críticas como procedimentos de um modulo, sendo portanto muito mais fáceis de programar Todas as chamadas ao procedimentos são executadas sob exclusão mútua Estruturas de sincronização estão embutidas no monitor Threads Java Java tem monitores “orientados a objeto” Em Java, existe um lock associado a cada objeto que não pode ser acessado diretamente pela aplicação Quando um método é identificado como synchronized, o acesso e feito apenas quando o lock associado com o objeto é obtido Criando Threads em Java [1] public class SimpleThread extends Thread { public SimpleThread(String str) { super(str); } public void run() { [ código do thread ] } } Criando Threads em Java [2] public class C extends D implements Runnable { private Thread thread = null; public void start() { if (clockThread == null) { thread = new Thread(this, “CThread"); thread.start(); } } public void run() { [código do thread ] } } Exclusão Mútua em Java class SharedInteger { private int theData; public SharedInteger(int initialValue) { theData = initialValue; } public synchronized int read() { return theData; } public synchronized void write(int newValue) { theData = newValue; } } Sincronização em Java Java associa condições a cada objeto wait() espera pela condição do objeto notify() e notifyAll() acordam threads esperando pela condição Exemplo Java Produtor/Consumidor retirado de java.sun.com/docs/books/tutorial/essen tial/threads ProducerConsumerTest.java CubbyHole.java Producer.java Consumer.java Agradecimentos A Fabricio Silva que gentilmente nos forneceu seu material sobre tempo real, usados como base destas transparências