Threads Bauke A. Dijkstra Lucas Alves Allan F. Furukita Threads • Threads, como processos são mecanismos para permitir um programa fazer mais de uma coisa ao mesmo tempo • Como processo as threads aparentam correr simultaneamente • O Linux kernel interrompe cada thread de tempo em tempo para dar chance de outros executar • Conceitualmente uma threads está dentro do processo • Quando é executado um programa, é criado um processo e dentro desse é criado uma única thread que pode criar outras threads • Ao criar uma nova thread nada é copiado, pois eles já compartilham a mesma memória, espaço, descrição, e outros recursos do sistema • Se uma thread mudar o valor de uma variável a thread subsequente ira ver o valor alterado • GNU/Linux implementa o POSIX thread API, todas funções e tipo de data são declarados no cabeçalho do arquivo • Como a pthread não esta incluido na biblioteca padrao de C, para compilar você deve adicionar –lpthread pois ela esta na libpthread Criação de threads • Cada thread possui sua ID • Na criação em a thread executa a thread function, quando retorna algum valor é porque ela existe • Em C e C++ usam o tipo pthread_t • A função pthread_create possui 4 parametros, (id, atributo, ponteiro da função, argumento) • A chamada de pthread_create retorna imediatamente e a thread original continua a executar as instruções seguindo a chamada, enquanto a nova thread começa a função thread • Seu programa não pode depender da ordem de execuções das threads Como passar dados para Threads? • É utilizado o argumento para a passagem de dados • Normalmente é definido uma estrutura para cada função thread que possui os parametros que é esperado Juntando Threads • É feita a partir da função pthread_join, que possui dois parâmetros. O id da thread que tem que esperar e um ponteiro que irá receber o retorno da thread quando terminar • Caso não seja necessário o valor de retorno utiliza o NULL • Caso seja diferente de NULL o valor de retorno será armazenado no local apontado pelo argumento Mais sobre Thread’s Ids • A função pthread_self retorna o a ID da thread que foi chamada • A função pthread_equal serve para comparar os ID das threads Atributos de Threads • Providencia um mecanismo para melhorar o comportamento da thread • Você pode criar e customizar atributos de objeto da thread para especificar outros valores para os atributos • Para especificar uma thread customizada você deve, criar pthread_attr_t, chamar pthread_attr_init, modificar o atributos de objeto da thread para conter o que é desejado, passar um ponteiro para o objeto quando chamar pthread_create, chamar pthread_attr_destroy • Um unico atributos de objeto da thread pode ser usado para começar varias threads • Não é necessário deixar o atributos de objetos da thread ao redor depois que as threads foi criada • Uma Thread pode ser declarada como joinable thread ou como detached thread • Joinable thread não é limpada automaticamente quando terminada já a detached thread limpa automaticamente e por isso essa não pode ser sincronizada com outra thread • Para escolher o estade detach no atributo de objeto da thread utiliza pthread_attr_setdetachstate sendo o primeiro parametro um ponteiro para o atributo de objeto da thread, e o segundo o estado desejado de detach Cancelamento da Thread • A função utilizada é a pthread_cancel, passando a ID da thread • Ela pode ser juntada depois de cancelada • Para liberar seus recursos ela deve ser juntada • Temos três estados em relação ao cancelamento das threads • Cancelamento assíncrono • Cancelamento síncrono (padrão) • Incancelável Threads Síncrona e Assíncrona • Uma thread cancelável assíncrona pode ser cancelada em qualquer ponto da execução, já a síncrona pode ser cancelada em pontos específicos da execução • Para deixar uma thread cancelável assíncrona utilize pthread_setcanceltype, que afeta a thread que chamou a função. pthread_setcanceltype (PTHREAD_CANCEL_ASYNCHRONOUS, NULL); • Para criar pontos de cancelamento utiliza-se o pthread_testcancel Seções Críticas Incancelável • A thread pode desabilitar o cancelamento usando a função pthread_setcancelstate • pthread_setcancelstate (PTHREAD_CANCEL_DISABLE, NULL); • Usando a função acima habilita a implementação de seções críticas • Seções críticas é uma seqüência de códigos que não pode ser cancelado enquanto não estiver pronto Quando usar cancelamento da Thread • No geral não é aconselhável utilizar cancelamento de uma thread para finalizar a execução de uma thread, exceto em circunstancias incomuns. • A melhor estratégia é indicar a thread que deve sair e esperar ela terminar de maneira normal. Dados específicos da Thread • As threads pertencentes a um processo compartilham os dados do processo. • Esse compartilhamento de dados fornece um dos benefícios da programação multithreads. • Em certas circunstancias, cada thread poderia precisar de sua própria copa de certos dados. • Você pode criar quantos dados específicos da thread que quiser, sendo do tipo void* • Cada item é referenciado por uma chave • Para criar uma nova chave é usado a função pthread_key_create • O primeiro argumento é um ponteiro para a variavel pthread_key_t • O segundo argumento para pthread_key_t é uma função de limpeza, pode-se passar null caso não precise de uma função de limpeza. • Depois da chave ser criada, cada thread pode definir seu valor de thread-específico correspondendo a chave chamando pthread_setspecific. • O primeiro argumento é a chave • O segundo argumento é o valor do void* thread-específico para armazenar • Para recuperar os dados do thread-específico, se chama pthread_getspecific, passando a chave como seu argumento. Cleanup Handlers • São funções chamadas quando a thread é terminada ou cancelada. • Ela é útil por ser capaz de especificar funções de limpeza sem criar um novo dado de thread específico que esta duplicado pra cada thread. • O GNU/LINUX te da esse recurso com esse proposito. • O handler toma um parâmetro void* e o valor do argumento é dado quando o handler é registrado. • Um Cleanup Handler é uma medida temporária, usada para desalocar um recurso somente se a thread é fechada ou cancelada. • Caso a thread não sair ou for cancelada, o cleanup handler deve ser removido e o recurso desalocado explicitamente. • Para registrar um cleanup handler, deve-se chamar pthread_cleanup_push, passando um ponteiro para a função de limpeza e o valor do argumento void* • A chamada para pthread_cleanup_push deve ser equilibrada por uma chamada correspondente para pthread_cleanup_pop, que cancela o registro de limpeza handler. • Como uma conveniência pthread_cleanup_pop leva um argumento int flag • Se a flag é diferente de zero, a ação de limpeza é realizada, uma vez que não é registrada. Thread Cleanup em C++ • Quando os objetos saem do escopo, C++ garante que os destructors sejam chamados para essas variáveis automáticas, fornecendo um código de limpeza que é chamado não importando como o bloco foi encerrado. • Se uma thread chama pthread_exit, a linguagem C++ não garante que os destructors sejam chamados para todas variáveis automáticas • A melhor forma para recuperar essa funcionalidade é usar pthread_exit no nível superior da função thread jogando uma exceção especial Sincronização e Sessões Críticas • Programar com threads é muito complicado, por causa da sua concorrência. É impossível saber quando o sistema irá agendar e executar uma thread. • Para fazer um “debbuging” é complicado, pois você pode executar o programa e funcionar, mas na próxima ele pode falhar. Condições de Corrida • A causa definitiva da maioria dos problemas que envolvem threads é que estes estão acessando os mesmos dados. Se uma thread usa parcialmente uma estrutura de dados enquanto outra acessa a mesma estrutura, problemas são suscetíveis de acontecer. • Quando uma thread fica agendada mais ou menos frequentemente que outra pode ocorrer um erro conhecido como condição de corrida. A depuração de programas que contenham condições de corrida não é nada simples. Os resultados da maioria dos testes não apresentam problemas, mas em algum momento algo estranho pode acontecer. Sessões Críticas O problema de evitar condições de disputa pode ser formulado de um modo abstrato. Durante uma parte do tempo, um processo está ocupando fazendo coisas que não levam a condições de disputa. Porém, algumas vezes ele precisa ter acesso à memória ou outra coisas que pode ocasionar disputas. Na parte do programa onde há acesso à memória compartilhada é chama ou sessão critica ou região crítica Porém, mesmo que evite-se as condições de disputa, é necessário satisfazer 4 condições para chegar a uma boa solução: 1. 2 Processos nunca podem estar simultaneamente em suas regiões críticas. 2. Nada pode ser afirmado sobre a velocidade ou sobre o número de CPUs. 3. Nenhum processo executando fora de sua região crítica pode bloquear outros processos 4. Nenhum processo deve esperar eternamente para entrar em sua região crítica. Exclusão mútua • É um modo de assegurar que outros processos sejam impedidos de usar uma variável ou um arquivo compartilhado que já estiver em uso por um processo. Semáforos • É uma variável especial que tem como função o controle de acesso a recursos compartilhados. O valor de um semáforo indica quantas threads podem ter acesso a um recurso compartilhado. • Edsger Dijkstra, criador do semáforo propôs a existência de duas operações, down e up (ou sleep e wakeup, respectivamente). • Down: Verifica se o valor do semáforo é maior que zero. Se sim, decrementará o valor e prosseguirá. Se o valor for zero, o processo será posto para dormir, sem terminar o down, pelo menos por enquanto. • Up: Incrementa o valor de um dado semáforo. Se um ou mais processos estivessem dormindo naquele semáforo, um deles seria escolhido pelo sistema e seria dada a permissão para terminar seu down. Mutexes Um mutex (abreviação de mutual exclusion, exclusão mútua) é uma versão simplificada de semáforo e só é adequado para gerenciar a exclusão mútua de algum recurso ou parte de código compartilhada. • Mutexes são fáceis de implementar e eficientes, o que os torna especialmente úteis em pacotes de threads implementados totalmente no espaço do usuário. • Um mutex é uma variável que tem dois estados (desimpedido ou impedido) e consequentemente, somente 1 bit é necessário para representá-lo. Quando um thread precisa ter acesso a uma região crítica, ele chama mutex_lock. Se o mutex estive desimpedido, a chamada prosseguirá. Por outro lado, se o mutex já estiver impedido, o thread que chamou mutex_lock, permanecerá bloqueado até que o thread na região crítica termine e chame mutex_unlock. Se vários threads estiverem bloqueados sobre o mutex, um deles será escolhido e liberado para adquirir a trava. Barreiras Algumas aplicações são divididas em fases e têm como regra que nenhum processo pode avançar para a próxima fase até que todos os processos estejam prontos a fazê-lo. Isso pode ser conseguido por meio da colocação de uma barreira no final de cada fase. Quando alcança a barreira, um processo permanece bloqueado até que todos os processos alcancem a barreira. Implementação de thread GNU/Linux • A implementação de threads POSIX no GNU/Linux é diferente da implementação em outros sistemas UNIX. • No GNU/Linux threads são implementados como processos. • Quando se chama pthread_create para criar uma nova thread, o Linux cria um novo processo para executar a thread Manipulação de sinais • O comportamento de interação entre sinais e threads variam de um sistema tipo UNIX para outro. • No GNU/Linux o comportamento é ditado por um fato que as threads são implementadas como processos. • Uma vez que cada thread é um processo separado, e porque um sinal é emitido para determinado processo, não há ambiguidade sobre qual thread recebe o sinal. • Normalmente sinais são enviados de fora do programa ao processo correspondente ao thread principal. • Em um programa multithread, é possível para um thread enviar um sinal específico para outro thread, usando a função pthread_kill. Passando o ID da thread como seu primeiro parâmetro e um numero de sinais como seu segundo parâmetro. Chamada de Sistema clonada • A chamada de sistema clonada do Linux é uma forma generalizada de fork e pthread_create que permite especificar os recursos compartilhados entre o processo de chamada e o novo processo criado. • O clone requer que especifique a região da memória para a execução da pilha que o novo processo use. • Para criar novos processos use fork • Para criar novas threads use pthread_create Processos x Threads • Todos threads em um programa deve rodar o mesmo executável. Um processo filho pode rodar um executável diferente chamando uma função exec. • Um thread errante pode prejudicar outros threads no mesmo processo porque compartilham o mesmo espaço de memória virtual e outros recursos. Um processo errante não prejudica pois cada processo tem uma cópia do espaço de memória do programa. • Threads deve ser usada em sistema paralelo de grão fino, em que o problema pode ser dividido em várias tarefas semelhantes. Processo deve ser usado em sistema paralelo de grão grosso. • Compartilhamento de dados entre threads é trivial porque threads compartilham a mesma memória. O compartilhamento de dados entre os processos requer o uso de mecanismos de IPC.