Por que a consistência quieta é composicional, mas a consistência sequencial não é

7

Estou com problemas para comparar esses dois modelos de consistência de memória.

Essencialmente, para consistência sequencial, penso em código real como este:

int x, y;

void ThreadA()
{
   x = 20; //Write
   int a = y; //Read
}

void ThreadB()
{
    y = 20;
    int b = x;
}

Em um ambiente de consistência sequencial, é impossível aou bnão ter 20 anos. Ou seja a = 20, b = 20ou a = 20 && b = 20.

Mas como a consistência quieta se encaixa nesse exemplo e por que é composicional?

William
fonte

Respostas:

10

Desculpe pela resposta tardia, mas acabei de encontrar a pergunta (perguntas, de fato). Também estou estudando concorrência e tentarei compartilhar algumas idéias com você.

Primeiro, vamos começar com consistência sequencial. Um modelo possui essa propriedade se as operações parecerem ter efeito na ordem do programa. Em outras palavras, a ordem na qual as linhas de código são executadas é a especificada no arquivo de origem. Esse pré-requisito é específico do encadeamento: operações que envolvem encadeamentos diferentes não são relacionadas à ordem do programa. Portanto, a propriedade concede o seguinte: operações emitidas pelo mesmo encadeamento são executadas na mesma ordem especificada pelo código-fonte do encadeamento. Operações emitidas por diferentes threads podem ocorrer em qualquer ordem. Esse pré-requisito pode parecer óbvio, mas nem sempre é o caso (otimizações do compilador podem alterar a ordem na qual as operações são emitidas, tornando a ordem do programa diferente da origem).

Seu exemplo está certo, mas deixe-me dar alguns outros. Considere este programa P1 (vou marcar cada linha para uma referência fácil):

   int x = 1;

   void ThreadA()
   {
A1:    x = x * 2;
A3:    int a = x;
   }

   void ThreadB()
   {
B2:    x = 20;
   }

Existe uma execução sequencial em que, no final, a = 40? Sim: B2, A1, A3.

B2 e A1 podem ser executados em qualquer ordem (eles pertencem a diferentes segmentos). A1 é executado antes de A3 (ordem do programa = ordem do código fonte).

Agora considere este programa P2:

   int y = 1;

   void ThreadA()
   {
A2:    y = 40;
   }

   void ThreadB()
   {
B1:    y = y / 2;
B3:    int b = y;
   }

Existe uma execução sequencial em que, no final, b = 20? Sim: A2, B1, B3.

E a composição? Vamos compor P1 e P2. Temos P1∘P2:

   int x = 1;
   int y = 1;

   void ThreadA()
   {
A1:    x = x * 2;
A2:    y = 40;
A3:    int a = x;
   }

   void ThreadB()
   {
B1:    y = y / 2;
B2:    x = 20;
B3:    int b = y;
   }

Se a consistência sequencial fosse composicional, deveria haver uma execução na qual, no final, a = 40 eb = 20. É esse o caso? Não. Vamos dar uma prova formal, oh, por que não pode ser o caso. Escreverei "o1 → o2" para dizer que a operação o1 deve ser executada antes da operação o2.

Em P1∘P2, devido à consistência sequencial, o seguinte deve ser válido: A1 -> A2 e B1 -> B2 (ordem do programa por thread). Para obter a = 40, também B2 -> A1 deve reter. Para obter b = 20 também A2 -> B1 deve se manter. Você pode ver a cadeia de precedência? A1 -> A2 -> B1 -> B2 -> A1 -> ...

P1 e P2 eram sequencialmente consistentes, mas sua composição P1∘P2 não era. A consistência sequencial não é composicional. O exemplo que você forneceu não foi suficientemente complicado para mostrar esse fato.

Agora, vamos considerar a consistência inativa. É difícil para mim explicar essa propriedade sem usar o paradigma orientado a objetos. De fato, a consistência inativa pode ser facilmente entendida em termos de chamadas de método pendentes aos objetos. No entanto, tentarei manter o paradigma imperativo (as chamadas de método pendentes tornam-se funções iniciadas, mas não concluídas, objetos tornam-se variáveis ​​envolvidas em funções).

Uma chamada de função é composta por uma chamada e uma resposta. Uma função está pendente se tiver sido chamada por um encadeamento, mas ainda não retornou uma resposta a esse encadeamento. Um período de inatividade para uma variável é um intervalo de tempo em que não há chamadas de função pendentes (por qualquer encadeamento) operando nela. Um modelo possui a propriedade de consistência inativa se chamadas de função operando na mesma variável e separadas por um período de inatividade parecem ter efeito na ordem em tempo real (a especificada pelo código-fonte). Do ponto de vista inverso, a definição afirma que as operações na mesma variável executada por funções chamadas simultaneamente por threads diferentes (não separadas por quiescência) podem ocorrer em qualquer ordem.

Tentei por muitas horas projetar um exemplo significativo usando apenas operações de leitura e gravação para mostrar a diferença entre consistência inativa e sequencial, mas não obtive sucesso. Deixe-me usar outro exemplo envolvendo conjuntos. Vou usar um vetor de bits para acompanhar quais números inteiros (de 0 a 4) estão no conjunto:

   int set[5] = {0, 0, 0, 0, 0};  // 0 if i-th item is absent, 1 otherwise

   void add(int number) {
L1:    set[number] = 1;
L2:    temp foo = set[0];
   }

   void remove(int number) {
       set[number] = 0;
   }

   int contains(int number) {
       return set[number] == 1;
   }

   void ThreadA()
   {
A1:    add(1);
   }

   void ThreadB()
   {
B1:    add(2);
B2:    remove(2);
B3:    int res = contains(2);
   }

Existe uma execução sequencial em que, no final, res = 1? Não: devido à consistência sequencial B1 -> B2 e B2 -> B3, então B1 -> B3. Portanto, remove (2) é sempre executado após add (2) e contém (2) sempre retornará 0.

Existe uma execução inativa em que, no final, res = 1? Sim! Considere esta execução:

O segmento A chama add (1) em A1.

O segmento A executa L1 (mas não L2). // como existe uma chamada pendente no conjunto, agora o Thread B está livre para executar B2 antes de B1 porque essas chamadas envolvem o conjunto também

O segmento B chama B2. // invocação + resposta

O segmento B chama B1. // invocação + resposta

O segmento A executa L2 e o add (1) responde. // agora há quiescência no set

O segmento B executa B3. // invocação + resposta

Agora res = 1.

Infelizmente, ainda não consigo responder à última pergunta sobre por que a consistência quieta é composicional: encontrei sua pergunta enquanto procurava essa resposta exata. Se eu inventar algo, eu o informarei.

Para uma boa leitura do tópico, dê uma olhada no capítulo 3.3 do livro "The Art of Multiprocessor Programming", de Herlihy e Shavit.

EDIT: Acabei de encontrar uma ótima página explicando por que a consistência quieta é composicional. E esta é outra leitura muito boa!

EDIT2: Corrigido pequeno erro no exemplo de composição sequencial de consistência.

Modelo de sombra
fonte
Ótima resposta para uma pergunta difícil! (Imaginei que era toque, ninguém respondeu por muitos meses: P).
William William
Posso entender a consistência quieta e a consistência sequencial individualmente (além da consistência estrita), mas acho extremamente difícil relacioná-las uma à outra. Embora no seu primeiro exemplo, se a execução fosse B2, A1, A3.em tempo real, algo diferente de a = 40 seria impossível sob rigorosa consistência.
William William
Mas acho que não entendo o que realmente se entende por consistência quincente sendo 'composicional'. Eu li o seu terceiro exemplo como 30 vezes (e a descrição do curso), mas apenas lutando para entender!
William William
Primeiro, corrigi um pequeno erro na descrição do exemplo P1∘P2. Mas acho que já ficou claro para você por que a consistência sequencial (SC) não é composicional. Em segundo lugar, a = 40 não é o único resultado possível no primeiro exemplo fornecido [que assume um modelo de SC], porque A1, A3, B2 é legítimo e leva a = 20. Meu último objetivo de exemplo foi destacar as diferenças entre SC e consistência quiescente (QC) (res = 1 é possível em um modelo e impossível no outro). Eu não cobri o tópico de composição de CQ: leia o link fornecido na primeira edição para obter informações sobre o assunto #
Shadow Template
@ William, eu não entendi por que você forçou o objeto a não ter um período de inatividade entre add (2) e remove (2) (assumindo que o segmento A tem uma chamada de método pendente no objeto). Como alternativa, seria um problema se eu permitisse ao Thread A concluir sua execução do método add () e, em seguida, o Thread B executasse B2 e, em seguida, B1. Essa sequência seria consistentemente quiescencial?
Saurabh Raje