Multithread com C e Win32

O compilador do Microsoft C/C++ (MSVC) fornece suporte para a criação de aplicativos multithread. Considere usar mais de um thread se seu aplicativo precisar executar operações caras que fariam com que a interface do usuário ficasse sem resposta.

Com o MSVC, há várias maneiras de programar com vários threads: você pode usar C++/WinRT e a biblioteca Windows Runtime, a biblioteca de Classe do Microsoft Foundation (MFC), C++/CLI e o runtime do .NET ou a biblioteca de tempo de execução C e a API Win32. Este artigo é sobre multithreading em C. Por exemplo, código, consulte Exemplo de programa multithread em C.

Programas multithread

Um thread é basicamente um caminho de execução por meio de um programa. É também a menor unidade de execução que o Win32 agenda. Um thread consiste em uma pilha, o estado dos registros de CPU e uma entrada na lista de execução do agendador do sistema. Cada thread compartilha todos os recursos do processo.

Um processo consiste em um ou mais threads e no código, dados e outros recursos de um programa na memória. Os recursos típicos do programa são arquivos abertos, semáforos e memória alocada dinamicamente. Um programa é executado quando o agendador do sistema fornece um de seus threads de controle de execução. O agendador determina quais threads devem ser executados e quando devem ser executados. Threads de prioridade mais baixa podem ter que esperar enquanto threads de prioridade mais alta completam suas tarefas. Em computadores multiprocessadores, o agendador pode mover threads individuais para processadores diferentes para equilibrar a carga da CPU.

Cada thread em um processo opera de forma independente. A menos que você os torne visíveis uns para os outros, os threads são executados individualmente e desconhecem os outros threads em um processo. Os threads que compartilham recursos comuns, no entanto, devem coordenar seu trabalho usando semáforos ou outro método de comunicação entre processos. Para mais informações sobre sincronizar tópicos, consulte Escrever um programa Win32 multithreaded.

Suporte de biblioteca para multithread

Todas as versões do CRT agora dão suporte ao multithreading, com exceção das versões sem bloqueio de algumas funções. Para obter mais informações, consulte Desempenho de bibliotecas multithread. Para obter informações sobre as versões do CRT disponíveis para vincular ao código, consulte os recursos da biblioteca CRT.

Incluir arquivos para multithread

O CRT Padrão inclui arquivos que declaram as funções de biblioteca de runtime C conforme elas são implementadas nas bibliotecas. Se as opções do compilador especificarem as convenções de chamada __fastcall ou __vectorcall, o compilador pressupõe que todas as funções devem ser chamadas usando a convenção de chamada de registro. As funções de biblioteca de runtime usam a convenção de chamada C e as declarações nos arquivos de inclusão padrão dizem ao compilador para gerar referências externas corretas a essas funções.

Funções CRT para controle de thread

Todos os programas Win32 têm pelo menos um thread. Qualquer thread pode criar threads adicionais. Um thread pode concluir seu trabalho rapidamente e, em seguida, terminar, ou ele pode permanecer ativo durante a vida útil do programa.

As bibliotecas CRT fornecem as seguintes funções para criação e encerramento de thread: _beginthread, _beginthreadex, _endthread e _endthreadex.

As funções _beginthread e as funções _beginthreadex criarão um novo thread e retornarão um identificador de thread se a operação for bem-sucedida. O thread será encerrado automaticamente se concluir a execução. Ou, ele pode terminar sozinho com uma chamada para _endthread ou _endthreadex.

Observação

Se você chamar rotinas de tempo de execução C de um programa criado com libcmt.lib, deverá iniciar seus threads com a função _beginthread ou _beginthreadex. Não use as funções Win32 ExitThread e CreateThread. O uso SuspendThread pode levar a um deadlock quando mais de um thread está bloqueado aguardando que o thread suspenso conclua seu acesso a uma estrutura de dados em tempo de execução C.

As funções _beginthread e _beginthreadex

As funções _beginthread e _beginthreadex criam um novo thread. Um thread compartilha os segmentos de código e dados de um processo com outros threads no processo, mas tem seus próprios valores de registro exclusivos, espaço de pilha e endereço de instrução atual. O sistema dá tempo de CPU para cada thread, de modo que todos os threads em um processo possam ser executados simultaneamente.

_beginthread e _beginthreadex são semelhantes à função CreateThread na API Win32, mas tem estas diferenças:

  • Eles inicializam determinadas variáveis de biblioteca de runtime C. Isso só será importante se você usar a biblioteca de tempo de execução C em seus threads.

  • CreateThread ajuda a fornecer controle sobre atributos de segurança. Você pode usar essa função para iniciar um thread em um estado suspenso.

_beginthread e _beginthreadex retorne um identificador para o novo thread se tiver êxito ou um código de erro se houver um erro.

As funções _endthread e _endthreadex

A função _endthread termina um thread criado por _beginthread (e, da mesma forma, _endthreadex encerra um thread criado por _beginthreadex). Os threads terminam automaticamente quando são concluídos. _endthread e _endthreadex são úteis para terminação condicional de dentro de um thread. Um thread dedicado ao processamento de comunicações, por exemplo, poderá ser encerrado se não conseguir obter o controle da porta de comunicações.

Escrevendo um programa Win32 multithread

Ao escrever um programa com vários threads, você deve coordenar o comportamento e o uso dos recursos do programa. Além disso, verifique se cada thread recebe sua própria pilha.

Compartilhar recursos comuns entre threads

Observação

Para uma discussão semelhante do ponto de vista do MFC, consulte Multithreading: dicas de programação e Multithreading: quando usar as classes de sincronização.

Cada thread tem sua própria pilha e sua própria cópia dos registros de CPU. Outros recursos, como arquivos, dados estáticos e memória de heap, são compartilhados por todos os threads no processo. Os threads que usam esses recursos comuns devem ser sincronizados. O Win32 fornece várias maneiras de sincronizar recursos, incluindo semáforos, seções críticas, eventos e mutexes.

Quando vários threads estão acessando dados estáticos, seu programa deve fornecer possíveis conflitos de recursos. Considere um programa em que um thread atualiza uma estrutura de dados estáticos contendo coordenadas x,y para que os itens sejam exibidos por outro thread. Se o thread de atualização alterar a coordenada x e admitir preempção antes de poder alterar a coordenada y, o thread de exibição poderá ser agendado antes que a coordenada y seja atualizada. O item seria exibido no local errado. Você pode evitar esse problema usando semáforos para controlar o acesso à estrutura.

Um mutex (abreviação de exclusãotua) é uma maneira de se comunicar entre threads ou processos que estão sendo executados de forma assíncrona uns dos outros. Essa comunicação pode ser usada para coordenar as atividades de vários threads ou processos, normalmente controlando o acesso a um recurso compartilhado bloqueando e desbloqueando o recurso. Para resolver esse problema de atualização das coordenadas x,y, o thread de atualização define um mutex indicando que a estrutura de dados está em uso antes de executar a atualização. Limparia o mutex depois que ambas as coordenadas tivessem sido processadas. O thread de exibição deve aguardar o mutex ser limpo antes de atualizar a exibição. Esse processo de espera por um mutex geralmente é chamado de bloqueio em um mutex, pois o processo é bloqueado e não pode continuar até que o mutex seja limpo.

O programa Bounce.c mostrado no Programa C multithread de exemplo usa um mutex nomeado ScreenMutex para coordenar atualizações de tela. Sempre que um dos threads de exibição estiver pronto para gravar na tela, ele chama WaitForSingleObject com o identificador ScreenMutex e a constante INFINITE para indicar que a chamada WaitForSingleObject deve ser bloqueada no mutex e não no tempo limite. Se ScreenMutex estiver clara, a função de espera definirá o mutex para que outros threads não possam interferir na exibição e continue executando o thread. Caso contrário, o thread será bloqueado até que o mutex seja limpo. Quando o thread conclui a atualização de exibição, ele libera o mutex chamando ReleaseMutex.

Exibições de tela e dados estáticos são apenas dois dos recursos que exigem um gerenciamento cuidadoso. Por exemplo, seu programa pode ter vários threads acessando o mesmo arquivo. Como outro thread pode ter movido o ponteiro do arquivo, cada thread deve redefinir o ponteiro do arquivo antes de ler ou gravar. Além disso, cada thread deve garantir que ele não admita preempção entre a hora em que posiciona o ponteiro e a hora em que ele acessa o arquivo. Esses threads devem usar um semáforo para coordenar o acesso ao arquivo, agrupando cada acesso de arquivo com chamadas WaitForSingleObject e ReleaseMutex. O exemplo de código a seguir ilustra esse padrão:

HANDLE    hIOMutex = CreateMutex (NULL, FALSE, NULL);

WaitForSingleObject( hIOMutex, INFINITE );
fseek( fp, desired_position, 0L );
fwrite( data, sizeof( data ), 1, fp );
ReleaseMutex( hIOMutex);

Pilhas de threads

Todo o espaço de pilha padrão de um aplicativo é alocado para o primeiro thread de execução, que é conhecido como thread 1. Como resultado, você deve especificar quanta memória alocar para uma pilha separada para cada thread adicional que seu programa precisa. O sistema operacional aloca espaço de pilha adicional para o thread, se necessário, mas você deve especificar um valor padrão.

O primeiro argumento na chamada _beginthread é um ponteiro para a função BounceProc, que executa os threads. O segundo argumento especifica o tamanho da pilha padrão para o thread. O último argumento é um número de ID que é passado para BounceProc. BounceProc usa o número de ID para propagar o gerador de número aleatório e selecionar o atributo de cor do thread e o caractere de exibição.

Os threads que fazem chamadas para a biblioteca de tempo de execução C ou para a API Win32 devem permitir espaço de pilha suficiente para a biblioteca e as funções de API que eles chamam. A função C printf requer mais de 500 bytes de espaço na pilha e você deve ter 2K bytes de espaço em pilha disponíveis ao chamar rotinas de API do Win32.

Como cada thread tem sua própria pilha, você pode evitar possíveis colisões sobre itens de dados usando o mínimo possível de dados estáticos. Crie seu programa para usar variáveis de pilha automáticas para todos os dados que podem ser privados para um thread. As únicas variáveis globais no programa Bounce.c são mutexes ou variáveis que nunca mudam após serem inicializadas.

O Win32 também fornece TLS (armazenamento local de thread) para armazenar dados por thread. Para obter mais informações, consulte TLS (Armazenamento local do Thread).

Evitando áreas de problema com programas multithread

Há vários problemas que você pode encontrar ao criar, vincular ou executar um programa C multithread. Alguns dos problemas mais comuns estão descritos na tabela abaixo. (Para uma discussão semelhante do ponto de vista do MFC, consulte Multithreading: dicas de programação.)

Problema Causa provável
Você recebe uma caixa de mensagem mostrando que seu programa causou uma violação de proteção. Muitos erros de programação do Win32 causam violações de proteção. Uma causa comum de violações de proteção é a atribuição indireta de dados para ponteiros nulos. Como isso resulta em seu programa tentando acessar a memória que não pertence a ele, uma violação de proteção é emitida.

Uma maneira fácil de detectar a causa de uma violação de proteção é compilar seu programa com informações de depuração e executá-lo por meio do depurador no ambiente do Visual Studio. Quando a falha de proteção ocorre, o Windows transfere o controle para o depurador e o cursor é posicionado na linha que causou o problema.
Seu programa gera vários erros de compilação e vinculação. Você pode eliminar muitos problemas potenciais definindo o nível de aviso do compilador como um de seus valores mais altos e anotando as mensagens de aviso. Usando as opções de nível 3 ou nível 4 de aviso, você pode detectar conversões de dados não intencionais, protótipos de função ausentes e uso de recursos não ANSI.

Confira também

Suporte de multithreading para código anterior (Visual C++)
Exemplo de programa multithread em C
Armazenamento local de thread (TLS)
Simultaneidade e operações assíncronas com C++/WinRT
Multithreading com C++ e MFC