Na Seção 4.1, descrevemos um servidor web com múltiplos threads. Naquele caso, sempre que o servidor recebe uma solicitação, ele cria um thread separado para atendêla. Embora a criação de um thread separado seja, sem dúvida, melhor do que a criação de um processo separado, um servidor multithreaded apresenta, contudo, problemas potenciais. O primeiro problema está relacionado com o tempo necessário à criação do thread, além do fato de que esse thread será descartado, assim que concluir seu trabalho. O segundo problema é mais complicado. Se permitirmos que todas as solicitações concorrentes sejam atendidas em um novo thread, não teremos um limite para o número de threads ativos e em concorrência no sistema. Um número ilimitado de threads pode exaurir os recursos do sistema, como o tempo de CPU ou a memória. Uma solução para esse problema é usar um pool de threads. A ideia geral por trás do pool de threads é a criação de múltiplos threads na inicialização do processo, bem como a inserção dos threads em um pool, onde eles ficarão esperando para entrar em ação. Quando um servidor recebe uma solicitação, ele desperta um thread desse pool — se houver um disponível — e passa para ele a solicitação de serviço. Uma vez que o thread conclua seu serviço, ele retorna ao pool e espera por mais trabalho. Se o pool não tem um thread disponível, o servidor espera até que um seja liberado. Os pools de threads oferecem esses benefícios: O atendimento de uma solicitação com um thread existente é mais rápido do que esperar a criação de um thread.
Um pool de threads limita o número de threads existentes em determinado momento. Isso é particularmente importante em sistemas que não podem dar suporte a um grande número de threads concorrentes.
3.
• • •
4.5.2
A separação da tarefa a ser executada da mecânica de criação da tarefa permitenos usar diferentes estratégias para execução da tarefa. Por exemplo, ela poderia ser organizada no schedule tanto para execução após um tempo de espera como para execução periódica.
O número de threads no pool pode ser estabelecido heuristicamente com base em fatores, como o número de CPUs no sistema, o montante de memória física e o número esperado de solicitações de clientes concorrentes. Arquiteturas de pool de threads mais sofisticadas podem ajustar dinamicamente o número de threads no pool de acordo com padrões de uso. Essas arquiteturas oferecem o benefício adicional de possuírem um pool menor — consumindo menos memória — quando a carga no sistema é baixa. Discutimos uma arquitetura desse tipo, a Grand Central Dispatch da Apple, posteriormente, nesta seção.
A API Windows fornece várias funções relacionadas com os pools de threads. O uso da API de pools de threads é semelhante à criação de um thread com a função Thread_Create ( ), como descrito na Seção 4.4.2. Aqui, é definida uma função que deve ser executada como um thread separado. Essa função pode aparecer na forma a seguir:
DWORD WINAPI PoolFunction (AVOID Param) { /*
* essa função é executada como um thread separado. */
}
Um ponteiro para PoolFunction ( ) é passado para uma das funções na API de pool de threads, e um thread do pool executa a função. Um membro desse tipo, pertencente à API de pool de threads, é a função QueueUserWorkItem ( ), que recebe três parâmetros:
LPTHREAD_START_ROUTINE Function — um ponteiro para a função que deve ser executada como um thread separado PVOID Param — o parâmetro passado para Function ULONG Flags — flags indicando como o pool de threads deve criar e gerenciar a execução do thread Um exemplo de invocação de uma função seria o seguinte: QueueUserWorkItem(&PoolFunction, NULL, 0); Tal comando causa a invocação, por um thread do pool de threads, de PoolFuncion ( ) em nome do programador. Nesse caso, não são passados parâmetros para PoolFunction ( ). Já que especificamos 0 como flag, não fornecemos instruções especiais ao pool de threads para a criação de threads.
Outros membros da API Windows de pool de threads incluem utilitários que invocam funções em intervalos periódicos ou quando uma solicitação de I/O assíncrona é concluída. O pacote java.util.concurrent da API Java também fornece um utilitário de pool de threads.
OpenMP
O OpenMP é um conjunto de diretivas de compilador assim como uma API para programas escritos em C, C++ ou FORTRAN que dá suporte à programação paralela em ambientes de memória compartilhada. OpenMP identifica regiões paralelas como blocos de código que podem ser executados em paralelo. Desenvolvedores de aplicações inserem diretivas de compilador em seu código, em regiões paralelas, e essas diretivas instruem a biblioteca de tempo de execução do OpenMP a executar a região em paralelo. O programa em C, a seguir, ilustra uma diretiva de compilador acima da região paralela que contém o comando printf ( ):
#include <omp.h> #include <stdio.h>
int main(int argc, char *argv[]) {
4.5.3
/* código sequencial */ #pragma omp parallel {
printf(“I am a parallel region.”); }
/* código sequencial */ return 0;
}
Quando OpenMP encontra a diretiva #pragma omp parallel
ele cria um número de threads equivalente ao número de núcleos de processamento do sistema. Assim, para um sistema dualcore, dois threads são criados; para um sistema quadcore, quatro são criados; e assim por diante. Todos os threads, então, executam simultaneamente a região paralela. À medida que cada thread sai da região paralela, ele é encerrado.
O OpenMP fornece várias diretivas adicionais para a execução de regiões de código em paralelo, incluindo a paralelização de loops. Por exemplo, suponha que tenhamos dois arrays a e b de tamanho N. Queremos somar seu conteúdo e colocar o resultado no array c. Podemos ter essa tarefa executada em paralelo usando o segmento de código a seguir, que contém a diretiva de compilador para a paralelização de loops for:
#pragma omp parallel for for (i = 0; i < N; i++) { c[i] = a[i] + b[i]; }
O OpenMP divide o trabalho contido no loop for entre os threads que ele criou, em resposta à diretiva #pragma omp parallel for
Além de fornecer diretivas de paralelização, o OpenMP permite que os desenvolvedores escolham entre vários níveis de paralelismo. Por exemplo, eles podem definir o número de threads manualmente. Também permite que os desenvolvedores identifiquem se os dados são compartilhados entre threads ou se são privados de um thread. O OpenMP está disponível em vários compiladores comerciais e de códigofonte aberto para os sistemas Linux, Windows e MacOS X. Recomendamos que os leitores interessados em aprender mais sobre o OpenMP consultem a Bibliografia no fim do capítulo.