Pergunta

Ao escrever aplicativos multithread, um dos problemas mais comuns enfrentados são as condições de corrida.

Minhas perguntas para a comunidade são:

O que é uma condição de corrida?Como você os detecta?Como você lida com eles?Finalmente, como você evita que eles ocorram?

Foi útil?

Solução

Uma condição de corrida ocorre quando dois ou mais threads podem acessar dados compartilhados e tentam alterá-los ao mesmo tempo.Como o algoritmo de agendamento de threads pode alternar entre threads a qualquer momento, você não sabe a ordem em que os threads tentarão acessar os dados compartilhados.Portanto, o resultado da alteração nos dados depende do algoritmo de escalonamento de threads, ou seja,ambos os threads estão "correndo" para acessar/alterar os dados.

Os problemas geralmente ocorrem quando um thread faz uma "verificação e ação" (por exemplo,"verificar" se o valor for X, então "agir" para fazer algo que depende do valor ser X) e outro thread faz algo com o valor entre "verificar" e "agir".Por exemplo:

if (x == 5) // The "Check"
{
   y = x * 2; // The "Act"

   // If another thread changed x in between "if (x == 5)" and "y = x * 2" above,
   // y will not be equal to 10.
}

A questão é que y pode ser 10 ou qualquer coisa, dependendo se outro thread alterou x entre a verificação e a ação.Você não tem nenhuma maneira real de saber.

Para evitar a ocorrência de condições de corrida, normalmente você colocaria um bloqueio nos dados compartilhados para garantir que apenas um thread possa acessar os dados por vez.Isso significaria algo assim:

// Obtain lock for x
if (x == 5)
{
   y = x * 2; // Now, nothing can change x until the lock is released. 
              // Therefore y = 10
}
// release lock for x

Outras dicas

Existe uma "condição de corrida" quando o código multithread (ou paralelo) que acessaria um recurso compartilhado poderia fazê-lo de maneira a causar resultados inesperados.

Veja este exemplo:

for ( int i = 0; i < 10000000; i++ )
{
   x = x + 1; 
}

Se você tivesse 5 threads executando esse código ao mesmo tempo, o valor de x NÃO seria 50.000.000.Na verdade, variaria com cada execução.

Isso ocorre porque, para que cada thread incremente o valor de x, eles precisam fazer o seguinte:(simplificado, obviamente)

Retrieve the value of x
Add 1 to this value
Store this value to x

Qualquer thread pode estar em qualquer etapa deste processo a qualquer momento, e eles podem interferir um no outro quando um recurso compartilhado está envolvido.O estado de x pode ser alterado por outro thread durante o tempo entre x ser lido e quando ele for gravado.

Digamos que um thread recupere o valor de x, mas ainda não o armazenou.Outro thread também pode recuperar o mesmo valor de x (porque nenhum thread o alterou ainda) e então ambos estariam armazenando o mesmo valor (x+1) de volta em x!

Exemplo:

Thread 1: reads x, value is 7
Thread 1: add 1 to x, value is now 8
Thread 2: reads x, o valor é 7
Thread 1: stores 8 in x
Thread 2: adds 1 to x, value is now 8
Thread 2: armazena 8 em x

As condições de corrida podem ser evitadas empregando algum tipo de bloqueio mecanismo antes do código que acessa o recurso compartilhado:

for ( int i = 0; i < 10000000; i++ )
{
   //lock x
   x = x + 1; 
   //unlock x
}

Aqui, a resposta sempre é 50.000.000.

Para obter mais informações sobre bloqueio, pesquise:mutex, semáforo, seção crítica, recurso compartilhado.

O que é uma condição de corrida?

Você está planejando ir ao cinema às 17h.Você pergunta sobre a disponibilidade dos ingressos às 16h.O representante diz que eles estão disponíveis.Você relaxa e chega à bilheteria 5 minutos antes do show.Tenho certeza que você pode adivinhar o que acontece:é casa cheia.O problema aqui estava no tempo entre a verificação e a ação.Você perguntou às 4 e agiu às 5.Enquanto isso, outra pessoa pegou os ingressos.Essa é uma condição de corrida - especificamente um cenário de condições de corrida do tipo "verificar e agir".

Como você os detecta?

Revisão de código religioso, testes de unidade multithread.Não há atalho.Existem poucos plugins Eclipse surgindo sobre isso, mas nada estável ainda.

Como você lida com eles e os previne?

A melhor coisa seria criar funções livres de efeitos colaterais e sem estado, usar imutáveis ​​​​tanto quanto possível.Mas isso nem sempre é possível.Portanto, usar java.util.concurrent.atomic, estruturas de dados simultâneas, sincronização adequada e simultaneidade baseada em ator ajudará.

O melhor recurso para simultaneidade é o JCIP.Você também pode obter mais alguns detalhes sobre a explicação acima aqui.

Há uma diferença técnica importante entre condições de corrida e corridas de dados.A maioria das respostas parece pressupor que esses termos são equivalentes, mas não são.

Uma corrida de dados ocorre quando 2 instruções acessam o mesmo local de memória, pelo menos um desses acessos é de escrita e não há acontece antes do pedido entre esses acessos.Agora, o que constitui uma ordem acontece antes está sujeito a muito debate, mas em geral os pares ulock-lock na mesma variável de bloqueio e os pares de sinal de espera na mesma variável de condição induzem uma ordem acontece antes.

Uma condição de corrida é um erro semântico.É uma falha que ocorre no tempo ou na ordem dos eventos que leva a erros de programação. comportamento.

Muitas condições de corrida podem ser (e de fato são) causadas por corridas de dados, mas isso não é necessário.Na verdade, as corridas de dados e as condições de corrida não são condições necessárias nem suficientes uma para a outra. Esse a postagem do blog também explica muito bem a diferença, com um exemplo simples de transação bancária.Aqui está outro simples exemplo isso explica a diferença.

Agora que definimos a terminologia, vamos tentar responder à pergunta original.

Dado que as condições de corrida são erros semânticos, não existe uma maneira geral de detectá-los.Isso ocorre porque não há como ter um oráculo automatizado que possa distinguir o correto do correto.comportamento incorreto do programa no caso geral.A detecção de corrida é um problema indecidível.

Por outro lado, as corridas de dados têm uma definição precisa que não está necessariamente relacionada com a correção e, portanto, é possível detectá-las.Existem muitos tipos de detectores de corrida de dados (detecção de corrida de dados estática/dinâmica, detecção de corrida de dados baseada em lockset, detecção de corrida de dados baseada em acontecimentos anteriores, detecção de corrida de dados híbrida).Um detector de corrida de dados dinâmico de última geração é ThreadSanitizer o que funciona muito bem na prática.

Lidar com corridas de dados em geral requer alguma disciplina de programação para induzir limites antes do que acontece entre os acessos aos dados compartilhados (seja durante o desenvolvimento, ou uma vez detectados usando as ferramentas mencionadas acima).isso pode ser feito através de bloqueios, variáveis ​​de condição, semáforos, etc.No entanto, também é possível empregar diferentes paradigmas de programação, como passagem de mensagens (em vez de memória compartilhada), que evitam corridas de dados por construção.

Uma definição meio canônica é "quando duas threads acessam o mesmo local na memória ao mesmo tempo, e pelo menos um dos acessos é de gravação." Nessa situação, o thread "leitor" pode obter o valor antigo ou o novo valor, dependendo de qual thread "ganha a corrida". Isso nem sempre é um bug - na verdade, alguns algoritmos de baixo nível realmente complicados fazem isso em propósito - mas geralmente deve ser evitado.@Steve Gury dá um bom exemplo de quando isso pode ser um problema.

Uma condição de corrida é um tipo de bug que ocorre apenas com determinadas condições temporais.

Exemplo:Imagine que você tem dois threads, A e B.

No tópico A:

if( object.a != 0 )
    object.avg = total / object.a

No tópico B:

object.a = 0

Se o thread A for interrompido logo após verificar se object.a não é nulo, B fará a = 0, e quando o thread A ganhar o processador, ele fará uma "divisão por zero".

Esse bug só acontece quando o thread A é interrompido logo após a instrução if, é muito raro, mas pode acontecer.

As condições de corrida ocorrem em aplicativos multithread ou sistemas multiprocessos.Uma condição de corrida, em sua forma mais básica, é qualquer coisa que pressupõe que duas coisas que não estão no mesmo thread ou processo acontecerão em uma ordem específica, sem tomar medidas para garantir que isso aconteça.Isso acontece comumente quando dois threads estão passando mensagens definindo e verificando variáveis ​​de membro de uma classe que ambos podem acessar.Quase sempre há uma condição de corrida quando um thread chama sleep para dar tempo a outro thread para concluir uma tarefa (a menos que o sleep esteja em um loop, com algum mecanismo de verificação).

As ferramentas para prevenir condições de corrida dependem do idioma e do sistema operacional, mas algumas das mais comuns são mutexes, seções críticas e sinais.Mutexes são bons quando você quer ter certeza de que é o único fazendo alguma coisa.Os sinais são bons quando você quer ter certeza de que outra pessoa terminou de fazer alguma coisa.Minimizar os recursos compartilhados também pode ajudar a prevenir comportamentos inesperados

Detectar condições de corrida pode ser difícil, mas existem alguns sinais.O código que depende muito de sleeps está sujeito a condições de corrida, portanto, primeiro verifique se há chamadas para sleep no código afetado.Adicionar suspensões particularmente longas também pode ser usado para depuração para tentar forçar uma ordem específica de eventos.Isso pode ser útil para reproduzir o comportamento, ver se é possível fazê-lo desaparecer alterando o tempo das coisas e para testar soluções implementadas.As suspensões devem ser removidas após a depuração.

O sinal de assinatura de que alguém tem uma condição de corrida é se houver um problema que ocorre apenas de forma intermitente em algumas máquinas.Bugs comuns seriam travamentos e impasses.Com o registro, você poderá encontrar a área afetada e trabalhar a partir daí.

A condição de corrida não está apenas relacionada ao software, mas também ao hardware.Na verdade, o termo foi inicialmente cunhado pela indústria de hardware.

De acordo com Wikipédia:

O termo tem origem na ideia de dois sinais competindo entre si para influenciar a saída primeiro.

Condição de corrida em um circuito lógico:

enter image description here

A indústria de software adotou esse termo sem modificações, o que o torna um pouco difícil de entender.

Você precisa fazer alguma substituição para mapeá-lo para o mundo do software:

  • "dois sinais" => "dois threads"/"dois processos"
  • "influenciar a saída" => "influenciar algum estado compartilhado"

Portanto, a condição de corrida na indústria de software significa "dois threads"/"dois processos" competindo entre si para "influenciar algum estado compartilhado", e o resultado final do estado compartilhado dependerá de alguma diferença sutil de tempo, que pode ser causada por algum específico ordem de lançamento de thread/processo, agendamento de thread/processo, etc.

Uma condição de corrida é uma situação de programação simultânea em que dois threads ou processos simultâneos competem por um recurso e o estado final resultante depende de quem obtém o recurso primeiro.

Na verdade, a Microsoft publicou um relatório muito detalhado artigo sobre esta questão de condições de corrida e impasses.O resumo mais resumido seria o parágrafo do título:

Uma condição de corrida ocorre quando dois threads acessam uma variável compartilhada ao mesmo tempo.O primeiro thread lê a variável e o segundo thread lê o mesmo valor da variável.Em seguida, o primeiro thread e o segundo thread executam suas operações no valor e correm para ver qual thread pode escrever o valor por último na variável compartilhada.O valor do encadeamento que escreve seu valor por último é preservado, porque o thread está escrevendo sobre o valor que o thread anterior escreveu.

O que é uma condição de corrida?

A situação em que o processo depende criticamente da sequência ou do tempo de outros eventos.

Por exemplo Processador A e processador B ambas as necessidades recurso idêntico para sua execução.

Como você os detecta?

Existem ferramentas para detectar condições de corrida automaticamente:

Como você lida com eles?

A condição de corrida pode ser tratada por Mutex ou Semáforos.Eles atuam como um bloqueio que permite que um processo adquira um recurso com base em determinados requisitos para evitar condições de corrida.

Como você evita que eles ocorram?

Existem várias maneiras de prevenir a condição de corrida, como Evitar Seção Crítica.

  1. Não existem dois processos simultaneamente dentro de suas regiões críticas.(Exclusão mútua)
  2. Nenhuma suposição é feita sobre velocidades ou número de CPUs.
  3. Nenhum processo em execução fora de sua região crítica que bloqueie outros processos.
  4. Nenhum processo precisa esperar uma eternidade para entrar na sua região crítica.(A espera por recursos B, B espera por recursos C, C espera por recursos A)

Uma condição de corrida é uma situação indesejável que ocorre quando um dispositivo ou sistema tenta realizar duas ou mais operações ao mesmo tempo, mas devido à natureza do dispositivo ou sistema, as operações devem ser realizadas na sequência adequada para serem executadas. feito corretamente.

Na memória ou armazenamento do computador, uma condição de corrida pode ocorrer se comandos para ler e escrever uma grande quantidade de dados forem recebidos quase no mesmo instante, e a máquina tentar substituir alguns ou todos os dados antigos enquanto esses dados antigos ainda estão sendo armazenados. ler.O resultado pode ser um ou mais dos seguintes:uma falha no computador, uma "operação ilegal", notificação e desligamento do programa, erros na leitura de dados antigos ou erros na gravação de novos dados.

Aqui está o exemplo clássico de Saldo de Conta Bancária que ajudará os novatos a entender Threads em Java facilmente.condições da corrida:

public class BankAccount {

/**
 * @param args
 */
int accountNumber;
double accountBalance;

public synchronized boolean Deposit(double amount){
    double newAccountBalance=0;
    if(amount<=0){
        return false;
    }
    else {
        newAccountBalance = accountBalance+amount;
        accountBalance=newAccountBalance;
        return true;
    }

}
public synchronized boolean Withdraw(double amount){
    double newAccountBalance=0;
    if(amount>accountBalance){
        return false;
    }
    else{
        newAccountBalance = accountBalance-amount;
        accountBalance=newAccountBalance;
        return true;
    }
}

public static void main(String[] args) {
    // TODO Auto-generated method stub
    BankAccount b = new BankAccount();
    b.accountBalance=2000;
    System.out.println(b.Withdraw(3000));

}

Experimente este exemplo básico para melhor compreensão da condição de corrida:

    public class ThreadRaceCondition {

    /**
     * @param args
     * @throws InterruptedException
     */
    public static void main(String[] args) throws InterruptedException {
        Account myAccount = new Account(22222222);

        // Expected deposit: 250
        for (int i = 0; i < 50; i++) {
            Transaction t = new Transaction(myAccount,
                    Transaction.TransactionType.DEPOSIT, 5.00);
            t.start();
        }

        // Expected withdrawal: 50
        for (int i = 0; i < 50; i++) {
            Transaction t = new Transaction(myAccount,
                    Transaction.TransactionType.WITHDRAW, 1.00);
            t.start();

        }

        // Temporary sleep to ensure all threads are completed. Don't use in
        // realworld :-)
        Thread.sleep(1000);
        // Expected account balance is 200
        System.out.println("Final Account Balance: "
                + myAccount.getAccountBalance());

    }

}

class Transaction extends Thread {

    public static enum TransactionType {
        DEPOSIT(1), WITHDRAW(2);

        private int value;

        private TransactionType(int value) {
            this.value = value;
        }

        public int getValue() {
            return value;
        }
    };

    private TransactionType transactionType;
    private Account account;
    private double amount;

    /*
     * If transactionType == 1, deposit else if transactionType == 2 withdraw
     */
    public Transaction(Account account, TransactionType transactionType,
            double amount) {
        this.transactionType = transactionType;
        this.account = account;
        this.amount = amount;
    }

    public void run() {
        switch (this.transactionType) {
        case DEPOSIT:
            deposit();
            printBalance();
            break;
        case WITHDRAW:
            withdraw();
            printBalance();
            break;
        default:
            System.out.println("NOT A VALID TRANSACTION");
        }
        ;
    }

    public void deposit() {
        this.account.deposit(this.amount);
    }

    public void withdraw() {
        this.account.withdraw(amount);
    }

    public void printBalance() {
        System.out.println(Thread.currentThread().getName()
                + " : TransactionType: " + this.transactionType + ", Amount: "
                + this.amount);
        System.out.println("Account Balance: "
                + this.account.getAccountBalance());
    }
}

class Account {
    private int accountNumber;
    private double accountBalance;

    public int getAccountNumber() {
        return accountNumber;
    }

    public double getAccountBalance() {
        return accountBalance;
    }

    public Account(int accountNumber) {
        this.accountNumber = accountNumber;
    }

    // If this method is not synchronized, you will see race condition on
    // Remove syncronized keyword to see race condition
    public synchronized boolean deposit(double amount) {
        if (amount < 0) {
            return false;
        } else {
            accountBalance = accountBalance + amount;
            return true;
        }
    }

    // If this method is not synchronized, you will see race condition on
    // Remove syncronized keyword to see race condition
    public synchronized boolean withdraw(double amount) {
        if (amount > accountBalance) {
            return false;
        } else {
            accountBalance = accountBalance - amount;
            return true;
        }
    }
}

Você nem sempre deseja descartar uma condição de corrida.Se você tem um sinalizador que pode ser lido e escrito por vários threads, e esse sinalizador é definido como 'feito' por um thread para que outro thread pare de processar quando o sinalizador estiver definido como 'feito', você não quer que "race condição" a ser eliminada.Na verdade, esta pode ser chamada de condição racial benigna.

No entanto, usando uma ferramenta para detecção de condição de corrida, ela será identificada como uma condição de corrida prejudicial.

Mais detalhes sobre as condições da corrida aqui, http://msdn.microsoft.com/en-us/magazine/cc546569.aspx.

Considere uma operação que deve exibir a contagem assim que a contagem for incrementada.ou seja, assim que ContadorThread aumenta o valor DisplayThread precisa exibir o valor atualizado recentemente.

int i = 0;

Saída

CounterThread -> i = 1  
DisplayThread -> i = 1  
CounterThread -> i = 2  
CounterThread -> i = 3  
CounterThread -> i = 4  
DisplayThread -> i = 4

Aqui ContadorThread obtém o bloqueio com frequência e atualiza o valor antes DisplayThread exibe.Aqui existe uma condição Race.A condição de corrida pode ser resolvida usando sincronização

Você pode evitar condição de corrida, se você usar classes "Atomic".O motivo é apenas o thread não separar a operação get e set, o exemplo está abaixo:

AtomicInteger ai = new AtomicInteger(2);
ai.getAndAdd(5);

Como resultado, você terá 7 no link “ai”.Embora você tenha feito duas ações, mas ambas as operações confirmam o mesmo thread e nenhum outro thread irá interferir nisso, isso significa que não há condições de corrida!

Uma condição de corrida é uma situação indesejável que ocorre quando dois ou mais processos podem acessar e alterar os dados compartilhados ao mesmo tempo. Ocorreu porque houve acessos conflitantes a um recurso.Problema na seção crítica pode causar condição de corrida.Para resolver a condição crítica entre os processos, retiramos apenas um processo por vez que executa a seção crítica.

public class Synchronized_RACECONDITION {
    private static final int NUM_INCREMENTS = 10000;

    private static int count = 0;

    public static void main(String[] args) {
        testSyncIncrement();
        testNonSyncIncrement();
    }

    private static void testSyncIncrement() {
        count = 0;

        ExecutorService executor = Executors.newFixedThreadPool(2);

        IntStream.range(0, NUM_INCREMENTS)
                .forEach(i -> executor.submit(Synchronized_RACECONDITION::incrementSync));

        ConcurrentUtils.stop(executor);

        System.out.println("   Sync: " + count);
    }

    private static void testNonSyncIncrement() {
        count = 0;

        ExecutorService executor = Executors.newFixedThreadPool(2);

        IntStream.range(0, NUM_INCREMENTS)
                .forEach(i -> executor.submit(Synchronized_RACECONDITION::increment));

        ConcurrentUtils.stop(executor);

        System.out.println("NonSync: " + count);
    }

    private static synchronized void incrementSync() {
        count = count + 1;
    }

    private static void increment() {
        count = count + 1;
    }
static  class ConcurrentUtils {

    public static void stop(ExecutorService executor) {
        try {
            executor.shutdown();
            executor.awaitTermination(60, TimeUnit.SECONDS);
        }
        catch (InterruptedException e) {
            System.err.println("termination interrupted");
        }
        finally {
            if (!executor.isTerminated()) {
                System.err.println("killing non-finished tasks");
            }
            executor.shutdownNow();
        }
    }
}
}
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top