Introdução aos Sistemas Operacionais Threads Eleri Cardozo FEEC/Unicamp O que é uma Thread? Uma thread (linha de controle) é uma unidade de execução e de controle, alocação e compartilhamento de recursos interna ao processo. Processo Threads Contexto (threads) Contexto (processo)
Motivações para Threads Minimizar a troca de contexto causada por processos que bloqueiam. Facilitar a implementação de programas concorrentes: - Grau mais fino de concorrência. - Facilidade de comunicação e sincronização inter-threads. - Troca de contexto mais rápida que processos (e sem influênciar o processo de paginação). - Criação e finalização mais rápida que processos (idem). Modelos de Threads Modelos de threads ditam como as threads se relacionam com o núcleo do sistema operacional. Quando o núcleo do sistema operacional não provê suporte a threads, é possível implementar threads no espaço do usuário por meio de bibliotecas específicas (por exemplo, pth - GNU Portable Threads). Este cenário é raro na atualidade. Quando o núcleo do sistema operacional provê suporte a threads, é possível utilizar threads por meio de chamadas de sistema específicas (por exemplo, pthreads - POSIX Threads). Este cenário é o mais comum na atualidade.
Modelos de Threads Problemas com threads no espaço do usuário: Apenas uma thread executa por vez (não tira proveito de processadores multicore). Não é possível interromper a execução de uma thread (a biblioteca de threads não pode definir um manipulador de interrupção de relógio!) Todas as chamadas bloqueantes devem ser redefinidas pela biblioteca de threads (pth define apenas um subconjunto). Exemplo: POSIX Threads #include <pthread.h> // definicao de thread void* thread1(void *p) { printf("thread recebeu parametro: %s\n", (char *)p); dowork(p); } // programa principal main() { pthread_t pth; pthread_create(&pth, NULL, thread1, "Alo thread"); pthread_join(pth, NULL); // espera thread terminar }
Comunicação Inter-threads Como threads compartilham a área de dados do processo, a comunicação inter-threads por memória compartilhada é a mais natural. Este mecanismo não requer a intermediação do sistema operacional, exceto para fins de sincronização. Pilha Dados área compartilhada Texto Thread 1 Thread 2 Sincronização Inter-threads Mutexes: semáforos binários (inicializados em 1). Variáveis de condição: permitem o compartilhamento de mutexes com o objetivo de evitar espera ocupada (polling). Tal como os mecanismos de sincronização inter-processo, mutexes e variáveis de condição devem ser providos pelo núcleo do sistema operacional.
Sincronização Inter-threads Exemplo do uso de variáveis de condição: problema produtor-consumidor. Thread mestre (produtora) Buffer Mutex? Variável de condição Threads trabalhadoras (consumidoras) Sincronização Inter-threads Implementação (ineficiente!) das threads consumidoras: #include <pthread.h> pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER; while(1) { // espera ocupada pthread_mutex_lock(&m); if(buffer.size == 0) { pthread_mutex_unlock(&m); usleep(10000); } else break; } // do work pthread_mutex_unlock(&m);
Sincronização Inter-threads Implementação (eficiente!) das threads consumidoras: #include <pthread.h> pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t c = PTHREAD_COND_INITIALIZER; pthread_mutex_lock(&m); if(buffer.size == 0) pthread_cond_wait(&c, &m); // do work pthread_mutex_unlock(&m); Sincronização Inter-threads Implementação (eficiente!) da thread produtora: #include <pthread.h> pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t c = PTHREAD_COND_INITIALIZER; pthread_mutex_lock(&m); // do work pthread_cond_signal(&c, &m); pthread_mutex_unlock(&m);
Escalonamento de Threads O escalonamento de threads é idêntico ao de processos, ou seja, cada thread tem seu contexto salvo/restaurado quando a thread perde/recupera a CPU. Escalonamento de Threads Escalonamento não preemptivo (threads no nível do usuário): threads executam até bloquear ou terminar. Escalonamento preemptivo (threads no nível do núcleo): threads são escalonadas por time-sharing ou por prioridades. Tal como processos, Linux suporta escalonamento de threads por prioridades tipo FIFO ou RR (Round Robin). Os sistemas operacionais atuais escalonam threads, ou seja, o processo perde a CPU quando expira o quantum de CPU ou quando não existe nenhuma se suas threads no estado de pronto.
Escalonamento de Threads #include <pthread.h> main() { pthread_t pth; pthread_attr_t ta; // atributos de escalonamento struct sched_param sp; // pars. de escalonamento // obtem a maxima prioridade que uma thread pode ter sp.sched_priority = sched_get_priority_max(sched_fifo); // define parametros de escalonameento pthread_attr_init(&sp); pthread_attr_setinheritsched(&ta, PTHREAD_EXPLICIT_SCHED); pthread_attr_setschedpolicy(&ta, SCHED_FIFO); pthread_attr_setschedparam(&ta, &sp); // cria thread com prioridade explicita pthread_create(&pth, &ta, thread1, "Alo thread"); pthread_join(pth, NULL); // espera thread terminar } Threads e a Chamada fork O que acontece quando um programa que iniciou múltiplas threads executa a chamada fork? (a) O processo filho inicia com todas as threads em execução no processo pai. (b) O processo filho é criado apenas com a thread principal (função main). No Unix ocorre a alternativa (b). Pior ainda, os mutexes e as variáveis de condição no processo filho assumem um estado indefinido.
Threads em Sistemas de Tempo Real Em sistemas de tempo real deve-se evitar a criação e destruição frequente de threads durante a execução do sistema (o overhead é baixo mas não desprezível). É preferível a criação de um pool de threads na inicializaçao e, eventualmente, um ajuste do tamanho do pool durante a execução do sistema. As threads do pool são sincronizadas com mutexes e variáveis de condição. Exemplo: Servidor Multithreaded
Atividades Práticas A biblioteca pthread Criação de threads Mutexes Variáveis de condição