Anotando o comportamento de bloqueio

Para evitar bugs de simultaneidade em seu programa multithreaded, sempre siga uma disciplina de bloqueio apropriada e use anotações SAL.

Os bugs de simultaneidade são notoriamente difíceis de reproduzir, diagnosticar e depurar porque não são determinísticos. O raciocínio sobre a intercalação de threads é difícil, na melhor das hipóteses, e torna-se impraticável quando você está projetando um corpo de código que tem mais de alguns threads. Portanto, é uma boa prática seguir uma disciplina de bloqueio em seus programas multithreaded. Por exemplo, obedecer a uma ordem de bloqueio ao adquirir vários bloqueios ajuda a evitar deadlocks e adquirir o bloqueio de proteção adequado antes de acessar um recurso compartilhado ajuda a evitar condições de corrida.

Infelizmente, regras de bloqueio aparentemente simples podem ser surpreendentemente difíceis de seguir na prática. Uma limitação fundamental nas linguagens de programação e compiladores atuais é que eles não dão suporte diretamente a especificação e análise de requisitos de simultaneidade. Os programadores precisam confiar em comentários informais de código para expressar as intenções deles sobre como eles usam bloqueios.

As anotações SAL simultâneas foram projetadas para ajudar você a especificar efeitos colaterais de bloqueio, responsabilidade de bloqueio, tutela de dados, hierarquia de ordem de bloqueio e outro comportamento de bloqueio esperado. Tornando as regras implícitas explícitas, as anotações de simultaneidade SAL fornecem uma maneira consistente de documentar como seu código usa regras de bloqueio. As anotações de simultaneidade também aumentam a capacidade das ferramentas de análise de código para encontrar condições de corrida, deadlocks, operações de sincronização incompatíveis e outros erros de simultaneidade sutis.

Diretrizes gerais

Usando anotações, você pode declarar os contratos implícitos por definições de função entre implementações (computadores chamados) e clientes (chamadores). Você também pode expressar invariáveis e outras propriedades do programa que podem melhorar ainda mais a análise.

O SAL dá suporte a muitos tipos diferentes de primitivos de bloqueio, por exemplo, seções críticas, mutexes, bloqueios de rotação e outros objetos de recurso. Muitas anotações de simultaneidade tomam uma expressão de bloqueio como um parâmetro. Por convenção, um bloqueio é indicado pela expressão de caminho do objeto de bloqueio subjacente.

Algumas regras de propriedade de thread a serem consideradas:

  • Os bloqueios de rotação são bloqueios não contados que têm a propriedade de thread clara.

  • Mutexes e seções críticas são bloqueios contados que têm a propriedade de thread clara.

  • Semáforos e eventos são bloqueios contados que não têm uma propriedade de thread clara.

Anotações de bloqueio

A tabela a seguir lista as anotações de bloqueio.

Anotação Descrição
_Acquires_exclusive_lock_(expr) Anota uma função e indica que, no estado posterior, a função incrementa em um a contagem de bloqueio exclusiva do objeto de bloqueio chamado expr.
_Acquires_lock_(expr) Anota uma função e indica que, no estado posterior, a função incrementa em um a contagem de bloqueio do objeto de bloqueio chamado expr.
_Acquires_nonreentrant_lock_(expr) O bloqueio chamado expr é adquirido. Um erro será relatado se o bloqueio já estiver mantido.
_Acquires_shared_lock_(expr) Anota uma função e indica que, no estado posterior, a função incrementa em um a contagem de bloqueio compartilhado do objeto de bloqueio chamado expr.
_Create_lock_level_(name) Uma instrução que declara o símbolo name como um nível de bloqueio para que ele possa ser usado nas anotações _Has_Lock_level_ e _Lock_level_order_.
_Has_lock_kind_(kind) Anota qualquer objeto para refinar as informações de tipo de um objeto de recurso. Às vezes, um tipo comum é usado para diferentes tipos de recursos e o tipo sobrecarregado não é suficiente para distinguir os requisitos semânticos entre vários recursos. Aqui está uma lista de parâmetros predefinidos kind :

_Lock_kind_mutex_
Bloquear a ID do tipo para mutexes.

_Lock_kind_event_
Bloquear a ID do tipo para eventos.

_Lock_kind_semaphore_
Bloquear a ID do tipo para semáforos.

_Lock_kind_spin_lock_
Bloquear a ID do tipo para bloqueios de rotação.

_Lock_kind_critical_section_
Bloquear a ID do tipo para seções críticas.
_Has_lock_level_(name) Anota um objeto de bloqueio e fornece o nível de bloqueio de name.
_Lock_level_order_(name1, name2) Uma instrução que fornece a ordenação de bloqueio entre name1 e name2. Os bloqueios que têm nível name1 devem ser adquiridos antes dos bloqueios que têm nível name2.
_Post_same_lock_(expr1, expr2) Anota uma função e indica que, no estado posterior, os dois bloqueios, expr1 e expr2, são tratados como se fossem o mesmo objeto de bloqueio.
_Releases_exclusive_lock_(expr) Anota uma função e indica que, no estado posterior, a função diminui em um a contagem de bloqueio exclusiva do objeto de bloqueio chamado expr.
_Releases_lock_(expr) Anota uma função e indica que, no estado posterior, a função diminui em um a contagem de bloqueio do objeto de bloqueio chamado expr.
_Releases_nonreentrant_lock_(expr) O bloqueio chamado expr é liberado. Um erro será relatado se o bloqueio não estiver mantido no momento.
_Releases_shared_lock_(expr) Anota uma função e indica que, no estado posterior, a função diminui em um a contagem de bloqueio compartilhado do objeto de bloqueio chamado expr.
_Requires_lock_held_(expr) Anota uma função e indica que, no estado anterior, a contagem de bloqueio do objeto chamado expr é pelo menos uma.
_Requires_lock_not_held_(expr) Anota uma função e indica que, no estado anterior, a contagem de bloqueio do objeto chamado expr é zero.
_Requires_no_locks_held_ Anota uma função e indica que as contagens de bloqueio de todos os bloqueios conhecidos pelo verificador são zero.
_Requires_shared_lock_held_(expr) Anota uma função e indica que, no estado anterior, a contagem de bloqueio compartilhado do objeto chamado expr é pelo menos uma.
_Requires_exclusive_lock_held_(expr) Anota uma função e indica que, no estado anterior, a contagem de bloqueio exclusiva do objeto chamado expr é pelo menos uma.

Intrínsecos SAL para objetos de bloqueio não expostos

Determinados objetos de bloqueio não são expostos pela implementação das funções de bloqueio associadas. A tabela a seguir lista variáveis intrínsecas SAL que permitem anotações em funções que operam nesses objetos de bloqueio não expostos.

Anotação Descrição
_Global_cancel_spin_lock_ Descreve o bloqueio de rotação de cancelamento.
_Global_critical_region_ Descreve a região crítica.
_Global_interlock_ Descreve operações interconectadas.
_Global_priority_region_ Descreve a região de prioridade.

Anotações de acesso a dados compartilhados

A tabela a seguir lista as anotações para acesso a dados compartilhados.

Anotação Descrição
_Guarded_by_(expr) Anota uma variável e indica que sempre que a variável é acessada, a contagem de bloqueio do objeto de bloqueio chamado expr é pelo menos uma.
_Interlocked_ Anota uma variável e é equivalente a _Guarded_by_(_Global_interlock_).
_Interlocked_operand_ O parâmetro de função anotado é o operando de destino de uma das várias funções interconectadas. Esses operandos devem ter propriedades adicionais específicas.
_Write_guarded_by_(expr) Anota uma variável e indica que sempre que a variável é modificada, a contagem de bloqueio do objeto de bloqueio chamado expr é pelo menos uma.

Anotações RAII e de bloqueio inteligente

Bloqueios inteligentes normalmente encapsulam bloqueios nativos e gerenciam seu tempo de vida. A tabela a seguir lista anotações que podem ser usadas com bloqueios inteligentes e padrões de codificação RAII com suporte para semântica move.

Anotação Descrição
_Analysis_assume_smart_lock_acquired_(lock) Informa ao analisador para assumir que um bloqueio inteligente foi adquirido. Essa anotação espera um tipo de bloqueio de referência como parâmetro.
_Analysis_assume_smart_lock_released_(lock) Informa ao analisador para assumir que um bloqueio inteligente foi liberado. Essa anotação espera um tipo de bloqueio de referência como parâmetro.
_Moves_lock_(target, source) Descreve uma operação move constructor que transfere o estado de bloqueio do objeto source para o target. O target é considerado recém-construído, portanto, qualquer estado que ele tinha antes é perdido e substituído pelo estado source. O source também é redefinido para um estado limpo sem contagens de bloqueio ou destino de nome alternativo, mas os aliases que apontam para ele permanecem inalterados.
_Replaces_lock_(target, source) Descreve a semântica move assignment operator em que o bloqueio de destino é liberado antes de transferir o estado da origem. Você pode considerá-la como uma combinação de _Moves_lock_(target, source) precedida por um _Releases_lock_(target).
_Swaps_locks_(left, right) Descreve o comportamento padrão swap que pressupõe que os objetos left e right troquem estado deles. O estado trocado inclui a contagem de bloqueios e o destino de nome alternativo, se presente. Os aliases que apontam para os objetos left e right permanecem inalterados.
_Detaches_lock_(detached, lock) Descreve um cenário no qual um tipo de wrapper de bloqueio permite a dissociação com seu recurso contido. É semelhante a como std::unique_ptr funciona com seu ponteiro interno: ele permite que os programadores extraiam o ponteiro e deixem seu contêiner de ponteiro inteligente em um estado limpo. std::unique_lock dá suporte à lógica semelhante e pode ser implementada em wrappers de bloqueio personalizados. O bloqueio desanexado mantém seu estado (contagem de bloqueios e destino de nome alternativo, se houver), enquanto o wrapper é redefinido para conter a contagem de bloqueio zero e nenhum destino de nome alternativo, mantendo seus próprios aliases. Não há operação em contagens de bloqueio (liberação e aquisição). Essa anotação se comporta exatamente como _Moves_lock_, exceto que o argumento desanexado deve ser return em vez de this.

Confira também