Costumo encontrar esses termos sendo usados no contexto da programação simultânea. Eles são a mesma coisa ou diferentes?
Costumo encontrar esses termos sendo usados no contexto da programação simultânea. Eles são a mesma coisa ou diferentes?
Respostas:
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.
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
.
data race
realmente significa em JLS?
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.
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.