Multithreading: como usar as classes de sincronização do MFC
Sincronizar o acesso a recursos entre threads é um problema comum ao escrever aplicativos multithread. Ter dois ou mais threads acessando simultaneamente os mesmos dados pode levar a resultados indesejáveis e imprevisíveis. Por exemplo, um thread pode estar atualizando o conteúdo de uma estrutura enquanto outro thread está lendo o conteúdo da mesma estrutura. Não se sabe quais dados o thread de leitura receberá: os dados antigos, os dados recém-gravados ou uma combinação de ambos. O MFC fornece várias classes de acesso de sincronização e sincronização para ajudar a resolver esse problema. Este tópico explica as classes disponíveis e como usá-las para criar classes thread-safe em um aplicativo multithread típico.
Um aplicativo multithread típico tem uma classe que representa um recurso a ser compartilhado entre threads. Uma classe totalmente projetada e totalmente thread-safe não exige que você chame nenhuma função de sincronização. Tudo é tratado internamente para a classe, permitindo que você se concentre em como usar melhor a classe, não sobre como ela pode ser corrompida. Uma técnica eficaz para criar uma classe totalmente thread-safe é mesclar a classe de sincronização na classe de recurso. Mesclar as classes de sincronização na classe compartilhada é um processo simples.
Como exemplo, use um aplicativo que mantém uma lista vinculada de contas. Esse aplicativo permite examinar até três contas em janelas separadas, mas apenas uma pode ser atualizada a qualquer momento específico. Quando uma conta é atualizada, os dados atualizados são enviados pela rede para um arquivo morto de dados.
Este aplicativo de exemplo usa todos os três tipos de classes de sincronização. Como ele permite examinar que até três contas ao mesmo tempo, ela usa CSemaphore para limitar o acesso a três objetos de exibição. Quando ocorre uma tentativa de exibir uma quarta conta, o aplicativo aguarda até que uma das três primeiras janelas seja fechada ou falha. Quando uma conta é atualizada, o aplicativo usa CCriticalSection para garantir que apenas uma conta seja atualizada por vez. Depois que a atualização for bem-sucedida, ela sinalizará CEvent, que libera um thread aguardando a sinalização do evento. Esse thread envia os novos dados para o arquivo morto de dados.
Como projetar uma classe thread-safe
Para tornar uma classe totalmente thread-safe, primeiro adicione a classe de sincronização apropriada às classes compartilhadas como um membro de dados. No exemplo anterior de gerenciamento de conta, um membro de dados CSemaphore
seria adicionado à classe de exibição, um membro de dados CCriticalSection
seria adicionado à classe de lista vinculada e um membro de dados CEvent
seria adicionado à classe de armazenamento de dados.
Em seguida, adicione chamadas de sincronização a todas as funções de membro que modificam os dados na classe ou acessam um recurso controlado. Em cada função, você deve criar um objeto CSingleLock ou CMultiLock e chamar a função desse objeto Lock
. Quando o objeto de bloqueio sai do escopo e é destruído, o destruidor do objeto chama Unlock
para você, liberando o recurso. Claro, você poderá chamar Unlock
diretamente, se desejar.
Projetar sua classe thread-safe dessa forma permite que ela seja usada em um aplicativo multithread com a mesma facilidade de uma classe não thread-safe, mas com um nível mais alto de segurança. Encapsular o objeto de sincronização e o objeto de acesso de sincronização na classe do recurso proporciona todos os benefícios da programação totalmente thread-safe sem a desvantagem de manter o código de sincronização.
O exemplo de código a seguir demonstra esse método usando um membro de dados, m_CritSection
(do tipo CCriticalSection
), declarado na classe de recurso compartilhado e em um objeto CSingleLock
. A sincronização do recurso compartilhado (derivado de CWinThread
) é tentada criando um objeto CSingleLock
usando o endereço do objeto m_CritSection
. É feita uma tentativa de bloquear o recurso e, quando obtido, o trabalho é feito no objeto compartilhado. Quando o trabalho é concluído, o recurso é desbloqueado com uma chamada para Unlock
.
CSingleLock singleLock(&m_CritSection);
singleLock.Lock();
// resource locked
//.usage of shared resource...
singleLock.Unlock();
Observação
CCriticalSection
, ao contrário de outras classes de sincronização MFC, não tem a opção de uma solicitação de bloqueio cronometrada. O período de espera para que um thread se torne livre é infinito.
A desvantagem dessa abordagem é que a classe será ligeiramente mais lenta que a mesma classe sem os objetos de sincronização adicionados. Além disso, se houver uma chance de que mais de um thread possa excluir o objeto, a abordagem mesclada pode nem sempre funcionar. Nessa situação, é melhor manter objetos de sincronização separados.
Para informações sobre como determinar qual classe de sincronização usar em diferentes situações, confira Multithreading: quando usar as classes de sincronização. Para mais informações sobre sincronização, confira Sincronização no SDK do Windows. Para mais informações sobre o suporte a multithreading no MFC, confira Multithreading com C++ e MFC.