São "corridas de dados" e "condição de corrida" na verdade a mesma coisa no contexto da programação simultânea

Respostas:

138

Não, eles não são a mesma coisa. Eles não são um subconjunto um do outro. Eles também não são nem o necessário, nem a condição suficiente um para o outro.

A definição de uma corrida de dados é bastante clara e, portanto, sua descoberta pode ser automatizada. Uma corrida de dados ocorre quando 2 instruções de threads diferentes acessam o mesmo local de memória, pelo menos um desses acessos é uma gravação e não há sincronização que está obrigando qualquer ordem particular entre esses acessos.

Uma condição de corrida é um erro semântico. É uma falha que ocorre no tempo ou na ordem dos eventos que leva a um comportamento incorreto do programa. Muitas condições de corrida podem ser causadas por data races, mas isso não é necessário.

Considere o seguinte exemplo simples, onde x é uma variável compartilhada:

Thread 1    Thread 2

 lock(l)     lock(l)
 x=1         x=2
 unlock(l)   unlock(l)

Neste exemplo, as gravações em x do encadeamento 1 e 2 são protegidas por bloqueios, portanto, estão sempre ocorrendo em alguma ordem imposta pela ordem com a qual os bloqueios são adquiridos no tempo de execução. Ou seja, a atomicidade das gravações não pode ser quebrada; sempre há um acontece antes do relacionamento entre as duas gravações em qualquer execução. Simplesmente não podemos saber qual escrita acontece antes da outra a priori.

Não há uma ordem fixa entre as gravações, porque os bloqueios não podem fornecer isso. Se a correção dos programas for comprometida, digamos, quando a gravação em x pelo encadeamento 2 é seguida pela gravação em x no encadeamento 1, dizemos que há uma condição de corrida, embora tecnicamente não haja disputa de dados.

É muito mais útil detectar as condições da corrida do que as corridas de dados; no entanto, isso também é muito difícil de conseguir.

Construir o exemplo reverso também é trivial. Esta postagem do blog também explica muito bem a diferença, com um exemplo simples de transação bancária.

Baris Kasikci
fonte
"data race (...) nenhuma sincronização que obrigue qualquer ordem particular entre estes acessos." Eu estou um pouco confuso. No seu exemplo, as operações podem ocorrer em ambas as ordens (= 1 e = 2 ou vice-versa). Por que não é uma corrida de dados?
josinalvo
5
@josinalvo: é um artefato da definição técnica de uma corrida de dados. O ponto chave é que entre os dois acessos, haverá um desbloqueio e uma aquisição de bloqueio (para qualquer um dos pedidos possíveis). Por definição, uma liberação de bloqueio e uma aquisição de bloqueio estabelecem uma ordem entre os dois acessos e, portanto, não há disputa de dados.
Baris Kasikci
A sincronização nunca exige nenhuma ordem particular entre as operações, portanto, esta não é uma maneira muito feliz de expressá-la. Por outro lado, o JMM especifica que para cada operação de leitura deve haver uma operação de gravação definida que ele observa, mesmo em uma corrida de dados. É difícil evitar mencionar explicitamente o que acontece antes e a ordem de sincronização, mas mesmo a definição JLS está errada ao mencionar apenas o que acontece antes : por sua definição, duas gravações voláteis simultâneas constituem uma disputa de dados.
Marko Topolnik
@BarisKasikci "estabelece uma ordem" não tem nenhum significado real, para mim. São apenas palavras furadas. Sinceramente, não acredito que "disputa de dados" seja um conceito remotamente útil, já que literalmente cada local de memória que é acessado por vários threads pode ser considerado uma "disputa de dados"
Noldorin
Os pares liberação-aquisição sempre estabelecem um pedido. Uma explicação geral é longa, mas um exemplo trivial é um par sinal-espera. @Noldorin "Estabelece uma ordem" refere-se a uma ordem acontece antes, que é um conceito chave da teoria da concorrência (veja o artigo seminal de Lamport sobre o relacionamento aconteceu antes) e sistemas distribuídos. Corridas de dados são um conceito útil, pois sua presença apresenta muitos problemas (por exemplo, semântica indefinida conforme o modelo de memória C ++, semântica muito complexa em Java, etc.). Sua detecção e eliminação constituem uma vasta literatura em pesquisa e prática.
Baris Kasikci
20

De acordo com a Wikipedia, o termo "condição de corrida" tem sido usado desde os dias das primeiras portas lógicas eletrônicas. No contexto do Java, uma condição de corrida pode pertencer a qualquer recurso, como um arquivo, conexão de rede, um encadeamento de um pool de encadeamentos, etc.

O termo "corrida de dados" é melhor reservado para seu significado específico definido pelo JLS .

O caso mais interessante é uma condição de corrida que é muito semelhante a uma corrida de dados, mas ainda não é, como neste exemplo simples:

class Race {
  static volatile int i;
  static int uniqueInt() { return i++; }
}

Como ié volátil, não há disputa de dados; entretanto, do ponto de vista da correção do programa, há uma condição de corrida devido à não atomicidade das duas operações: leitura i, gravação i+1. Vários threads podem receber o mesmo valor de uniqueInt.

Marko Topolnik
fonte
1
você pode deixar uma linha em sua resposta descrevendo o que data racerealmente significa em JLS?
Geek de
@geek A palavra "JLS" é um hiperlink para a seção relevante do JLS.
Marko Topolnik
@MarkoTopolnik Estou um pouco confuso com o exemplo. Você poderia explicar: "Como i é volátil, não há disputa de dados"? Voltility apenas garantiu que seja visível, mas ainda: 1) não está sincronizado e vários threads podem ler / gravar ao mesmo tempo e 2) É um campo não final compartilhado. Portanto, de acordo com Java Concurrency in Practice (citado abaixo também) , é corrida de dados e não condição de corrida, não é?
aniliitb10
@ aniliitb10 Em vez de confiar em citações de segunda mão arrancadas de seu contexto, você deve revisar a seção JLS 17.4 que indiquei em minha resposta. O acesso a uma variável volátil é uma ação de sincronização conforme definido em §17.4.2.
Marko Topolnik
@ aniliitb10 Votaltiles não causam disputa de dados, porque seus acessos podem ser solicitados. Ou seja, você pode raciocinar sua ordem desta ou daquela maneira, levando a resultados diferentes. Com a corrida de dados, você não tem como raciocinar o pedido. Por exemplo, a operação i ++ de cada thread pode ocorrer apenas em seus respectivos valores i armazenados em cache local. Globalmente, você não tem como ordenar essas operações (do ponto de vista do programador) - a menos que tenha um determinado modelo de memória de linguagem em vigor.
Xiao-Feng Li
3

Não, eles são diferentes e nenhum deles é um subconjunto de um ou vice-versa.

O termo condição de corrida é frequentemente confundido com o termo relacionado corrida de dados, que surge quando a sincronização não é usada para coordenar todo o acesso a um campo não final compartilhado. Você corre o risco de uma corrida de dados sempre que um encadeamento grava uma variável que pode ser lida por outro encadeamento ou lê uma variável que pode ter sido gravada pela última vez por outro encadeamento se ambos os encadeamentos não usarem sincronização; o código com corridas de dados não tem semântica definida útil no modelo de memória Java. Nem todas as condições de corrida são corridas de dados, e nem todas as corridas de dados são condições de corrida, mas ambas podem fazer com que programas simultâneos falhem de maneiras imprevisíveis.

Retirado do excelente livro - Java Concurrency in Practice, de Joshua Bloch & Co.

Shirgill Farhan
fonte
Observe que a pergunta tem uma tag independente de idioma.
martinkunev
1

TL; DR: A distinção entre corrida de dados e condição de corrida depende da natureza da formulação do problema e de onde traçar a fronteira entre comportamento indefinido e comportamento bem definido, mas indeterminado. A distinção atual é convencional e reflete melhor a interface entre o arquiteto do processador e a linguagem de programação.

1. Semântica

A corrida de dados refere-se especificamente aos "acessos à memória" conflitantes não sincronizados (ou ações ou operações) para o mesmo local de memória. Se não houver conflito nos acessos à memória, enquanto ainda houver comportamento indeterminado causado pela ordem das operações, trata-se de uma condição de corrida.

Observe que "acessos à memória" aqui têm um significado específico. Eles se referem ao carregamento de memória "puro" ou ações de armazenamento, sem qualquer semântica adicional aplicada. Por exemplo, um armazenamento de memória de um encadeamento não sabe (necessariamente) quanto tempo leva para os dados serem gravados na memória e, finalmente, se propagam para outro encadeamento. Para outro exemplo, um armazenamento de memória em um local antes de outro armazenamento em outro local pelo mesmo thread não garante (necessariamente) que os primeiros dados gravados na memória estejam à frente do segundo. Como resultado, a ordem desses acessos puros à memória não é (necessariamente) capaz de ser "fundamentada" e qualquer coisa pode acontecer, a menos que seja bem definido de outra forma.

Quando os "acessos à memória" são bem definidos em termos de ordenação por meio de sincronização, semânticas adicionais podem garantir que, mesmo que o tempo dos acessos à memória seja indeterminado, sua ordem possa ser "raciocinada" por meio das sincronizações. Observe que, embora a ordem entre os acessos à memória possa ser racional, eles não são necessariamente determinados, daí a condição de corrida.

2. Por que a diferença?

Mas se a ordem ainda é indeterminada na condição de corrida, por que se preocupar em distingui-la da corrida de dados? A razão é mais prática do que teórica. É porque a distinção existe na interface entre a linguagem de programação e a arquitetura do processador.

Uma instrução de carga / armazenamento de memória na arquitetura moderna é geralmente implementada como acesso à memória "puro", devido à natureza do pipeline fora de ordem, especulação, multi-nível de cache, interconexão cpu-ram, especialmente multi-core, etc. Existem muitos fatores que levam a um tempo e ordem indeterminados. Aplicar a ordem de cada instrução de memória incorre em uma grande penalidade, especialmente em um design de processador que oferece suporte a vários núcleos. Portanto, a semântica de ordenação é fornecida com instruções adicionais, como várias barreiras (ou cercas).

A corrida de dados é a situação de execução de instruções do processador sem barreiras adicionais para ajudar a raciocinar a ordenação de acessos à memória conflitantes. O resultado não é apenas indeterminado, mas também possivelmente muito estranho, por exemplo, duas gravações no mesmo local de palavra por threads diferentes podem resultar com cada metade da palavra escrita, ou podem operar apenas em seus valores armazenados em cache localmente. - São comportamentos indefinidos, do ponto de vista do programador. Mas eles são (geralmente) bem definidos do ponto de vista do arquiteto do processador.

Os programadores precisam encontrar uma maneira de raciocinar a execução do código. A disputa de dados é algo que eles não fazem sentido, portanto, sempre devem evitar (normalmente). É por isso que as especificações de linguagem de baixo nível geralmente definem a corrida de dados como comportamento indefinido, diferente do comportamento de memória bem definido da condição de corrida.

3. Modelos de memória de linguagem

Processadores diferentes podem ter comportamentos diferentes de acesso à memória, ou seja, modelo de memória do processador. É estranho para os programadores estudar o modelo de memória de cada processador moderno e, então, desenvolver programas que possam se beneficiar deles. É desejável que a linguagem possa definir um modelo de memória de forma que os programas dessa linguagem sempre se comportem conforme o esperado conforme o modelo de memória define. É por isso que Java e C ++ têm seus modelos de memória definidos. É responsabilidade dos desenvolvedores do compilador / tempo de execução garantir que os modelos de memória da linguagem sejam aplicados em diferentes arquiteturas de processador.

Dito isso, se uma linguagem não deseja expor o comportamento de baixo nível do processador (e está disposta a sacrificar certos benefícios de desempenho das arquiteturas modernas), ela pode escolher definir um modelo de memória que esconda completamente os detalhes de "puro" acessa a memória, mas aplica semântica de ordenação para todas as suas operações de memória. Em seguida, os desenvolvedores do compilador / tempo de execução podem escolher tratar cada variável de memória como volátil em todas as arquiteturas de processador. Para essas linguagens (que suportam memória compartilhada entre threads), não há corridas de dados, mas ainda podem haver condições de corrida, mesmo com uma linguagem de consistência sequencial completa.

Por outro lado, o modelo de memória do processador pode ser mais rígido (ou menos relaxado, ou em um nível mais alto), por exemplo, implementando consistência sequencial como o primeiro processador fazia. Em seguida, todas as operações de memória são ordenadas e nenhuma disputa de dados existe para qualquer idioma em execução no processador.

4. Conclusão

Voltando à pergunta original, IMHO, não há problema em definir a corrida de dados como um caso especial de condição de corrida, e a condição de corrida em um nível pode se tornar corrida de dados em um nível superior. Depende da natureza da formulação do problema e de onde traçar a fronteira entre o comportamento indefinido e o comportamento bem definido, mas indeterminado. Apenas a convenção atual define o limite na interface do processador de linguagem, não significa necessariamente que é sempre e deve ser o caso; mas a convenção atual provavelmente reflete melhor a interface (e sabedoria) de última geração entre o arquiteto do processador e a linguagem de programação.

Xiao-Feng Li
fonte