Question

Lors de l'écriture d'applications multithread, l'un des problèmes les plus courants rencontrés est celui des conditions de concurrence.

Mes questions à la communauté sont les suivantes :

Qu'est-ce qu'une condition de concurrence ?Comment les détecter ?Comment les gérez-vous ?Enfin, comment éviter qu’ils se produisent ?

Était-ce utile?

La solution

Une condition de concurrence critique se produit lorsque deux threads ou plus peuvent accéder aux données partagées et tentent de les modifier en même temps.Étant donné que l'algorithme de planification des threads peut basculer entre les threads à tout moment, vous ne connaissez pas l'ordre dans lequel les threads tenteront d'accéder aux données partagées.Par conséquent, le résultat de la modification des données dépend de l'algorithme de planification des threads, c'est-à-direles deux threads « courent » pour accéder/modifier les données.

Des problèmes surviennent souvent lorsqu'un thread effectue une opération "vérifier puis agir" (par ex."vérifier" si la valeur est X, puis "agir" pour faire quelque chose qui dépend de la valeur étant X) et un autre thread fait quelque chose à la valeur entre le "vérifier" et "l'acte".Par exemple:

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.
}

Le fait est que y pourrait être 10, ou cela pourrait être n'importe quoi, selon qu'un autre thread a modifié x entre la vérification et l'action.Vous n'avez aucun moyen réel de le savoir.

Afin d'éviter que des conditions de concurrence critique ne se produisent, vous devez généralement verrouiller les données partagées pour garantir qu'un seul thread peut accéder aux données à la fois.Cela signifierait quelque chose comme ceci :

// 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

Autres conseils

Une « condition de concurrence critique » existe lorsqu'un code multithread (ou autrement parallèle) qui accéderait à une ressource partagée pourrait le faire de manière à provoquer des résultats inattendus.

Prenons cet exemple :

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

Si 5 threads exécutaient ce code à la fois, la valeur de x NE SERAIT PAS 50 000 000.Cela varierait en fait à chaque course.

En effet, pour que chaque thread incrémente la valeur de x, il doit procéder comme suit :(simplifié, évidemment)

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

N'importe quel thread peut se trouver à n'importe quelle étape de ce processus, à tout moment, et ils peuvent se chevaucher lorsqu'une ressource partagée est impliquée.L'état de x peut être modifié par un autre thread entre la lecture de x et sa réécriture.

Disons qu'un thread récupère la valeur de x, mais ne l'a pas encore stocké.Un autre thread peut également récupérer le même valeur de x (car aucun thread ne l'a encore modifié) et ils stockeraient alors tous les deux le même valeur (x+1) de retour dans x !

Exemple:

Thread 1: reads x, value is 7
Thread 1: add 1 to x, value is now 8
Thread 2: reads x, la valeur est 7
Thread 1: stores 8 in x
Thread 2: adds 1 to x, value is now 8
Thread 2: stocke 8 po x

Les conditions de concurrence peuvent être évitées en employant une sorte de verrouillage mécanisme avant le code qui accède à la ressource partagée :

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

Ici, la réponse est 50 000 000 à chaque fois.

Pour en savoir plus sur le verrouillage, recherchez :mutex, sémaphore, section critique, ressource partagée.

Qu'est-ce qu'une condition de concurrence ?

Vous prévoyez d'aller au cinéma à 17 heures.Vous vous renseignez sur la disponibilité des billets à 16h.Le représentant dit qu'ils sont disponibles.Vous vous détendez et rejoignez la billetterie 5 minutes avant le spectacle.Je suis sûr que vous pouvez deviner ce qui se passe :c'est une salle comble.Le problème ici résidait dans le délai entre le contrôle et l’action.Vous avez demandé à 4 heures et avez agi à 5 heures.Entre-temps, quelqu'un d'autre s'est emparé des billets.Il s'agit d'une condition de concurrence - en particulier d'un scénario de conditions de concurrence « vérifier puis agir ».

Comment les détecter ?

Révision du code religieux, tests unitaires multi-thread.Il n'y a pas de raccourci.Il y a peu de plugins Eclipse qui émergent à ce sujet, mais rien de stable pour l'instant.

Comment les gérer et les prévenir ?

La meilleure chose serait de créer des fonctions sans effets secondaires et sans état, d'utiliser autant que possible des immuables.Mais ce n'est pas toujours possible.Ainsi, l'utilisation de java.util.concurrent.atomic, de structures de données concurrentes, d'une synchronisation appropriée et d'une concurrence basée sur les acteurs sera utile.

La meilleure ressource pour la concurrence est JCIP.Vous pouvez également en obtenir d'autres détails sur l'explication ci-dessus ici.

Il existe une différence technique importante entre les conditions de course et les courses aux données.La plupart des réponses semblent supposer que ces termes sont équivalents, mais ce n’est pas le cas.

Une course aux données se produit lorsque 2 instructions accèdent au même emplacement mémoire, au moins un de ces accès est une écriture et il n'y a pas de ça se passe avant de commander parmi ces accès.Maintenant, ce qui constitue un ordre arrive avant est sujet à de nombreux débats, mais en général, les paires ulock-lock sur la même variable de verrouillage et les paires de signaux d'attente sur la même variable de condition induisent un ordre arrive avant.

Une condition de concurrence critique est une erreur sémantique.C'est un défaut qui se produit dans le timing ou l'ordre des événements et qui conduit à un programme erroné. comportement.

De nombreuses conditions de concurrence peuvent être (et sont en fait) provoquées par des courses aux données, mais cela n’est pas nécessaire.En fait, la course aux données et les conditions de course ne sont ni des conditions nécessaires ni suffisantes l’une pour l’autre. Ce L'article de blog explique également très bien la différence, avec un exemple simple de transaction bancaire.Voici un autre simple exemple cela explique la différence.

Maintenant que nous avons défini la terminologie, essayons de répondre à la question initiale.

Étant donné que les conditions de concurrence sont des bugs sémantiques, il n’existe aucun moyen général de les détecter.En effet, il n’existe aucun moyen d’avoir un oracle automatisé capable de distinguer le correct du correct.comportement incorrect du programme dans le cas général.La détection des races est un problème indécidable.

En revanche, les courses aux données ont une définition précise qui ne concerne pas nécessairement l'exactitude, et on peut donc les détecter.Il existe de nombreux types de détecteurs de course aux données (détection de course aux données statique/dynamique, détection de course aux données basée sur un verrou, détection de course aux données basée sur les événements antérieurs, détection de course aux données hybride).Un détecteur dynamique de course aux données de pointe est DiscussionSanitizer ce qui fonctionne très bien en pratique.

La gestion des courses aux données en général nécessite une certaine discipline de programmation pour induire des fronts avant entre les accès aux données partagées (soit pendant le développement, soit une fois qu'ils sont détectés à l'aide des outils mentionnés ci-dessus).cela peut être fait via des verrous, des variables de condition, des sémaphores, etc.Cependant, on peut également utiliser différents paradigmes de programmation comme la transmission de messages (au lieu de la mémoire partagée) qui évitent les courses aux données par construction.

Une définition en quelque sorte canonique est "lorsque deux threads accèdent au même emplacement en mémoire en même temps et qu'au moins un des accès est un accès en écriture. " Dans cette situation, le thread "lecteur" peut obtenir l'ancienne valeur ou la nouvelle valeur, selon le thread "gagne la course". Ce n'est pas toujours un bug - en fait, certains algorithmes de bas niveau très compliqués le font sur but, mais cela devrait généralement être évité.@Steve Gury donne un bon exemple de cas où cela pourrait poser un problème.

Une condition de concurrence critique est une sorte de bug, qui ne se produit qu'avec certaines conditions temporelles.

Exemple:Imaginez que vous ayez deux threads, A et B.

Dans le fil A :

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

Dans le fil B :

object.a = 0

Si le thread A est préempté juste après avoir vérifié que object.a n'est pas nul, B le fera a = 0, et lorsque le thread A gagnera le processeur, il effectuera une "division par zéro".

Ce bug ne se produit que lorsque le thread A est préempté juste après l'instruction if, c'est très rare, mais cela peut arriver.

Les conditions de concurrence se produisent dans les applications multithread ou les systèmes multi-processus.Une condition de concurrence critique, dans sa forme la plus élémentaire, est tout ce qui suppose que deux choses qui ne se trouvent pas dans le même thread ou processus se produiront dans un ordre particulier, sans prendre de mesures pour garantir qu'elles se produiront.Cela se produit généralement lorsque deux threads transmettent des messages en définissant et en vérifiant les variables membres d'une classe auxquelles ils peuvent accéder.Il y a presque toujours une condition de concurrence critique lorsqu'un thread appelle sleep pour donner à un autre thread le temps de terminer une tâche (à moins que sleep ne soit dans une boucle, avec un mécanisme de vérification).

Les outils permettant de prévenir les conditions de concurrence dépendent de la langue et du système d'exploitation, mais les outils les plus courants sont les mutex, les sections critiques et les signaux.Les mutex sont utiles lorsque vous voulez vous assurer que vous êtes le seul à faire quelque chose.Les signaux sont bons lorsque vous voulez vous assurer que quelqu'un d'autre a fini de faire quelque chose.La réduction des ressources partagées peut également aider à prévenir les comportements inattendus

Détecter les conditions de course peut être difficile, mais il y a quelques signes.Le code qui repose fortement sur la mise en veille est sujet aux conditions de concurrence, alors vérifiez d'abord les appels à la mise en veille dans le code concerné.L'ajout de mises en veille particulièrement longues peut également être utilisé pour le débogage afin d'essayer de forcer un ordre particulier d'événements.Cela peut être utile pour reproduire le comportement, voir si on peut le faire disparaître en modifiant le timing des choses, et pour tester les solutions mises en place.Les mises en veille doivent être supprimées après le débogage.

Cependant, le signe distinctif indiquant qu'il y a une condition de concurrence critique est s'il y a un problème qui ne se produit que par intermittence sur certaines machines.Les bugs courants seraient des plantages et des blocages.Avec la journalisation, vous devriez pouvoir trouver la zone affectée et travailler à partir de là.

La condition de concurrence n'est pas seulement liée au logiciel, mais également au matériel.En fait, le terme a été inventé à l’origine par l’industrie du matériel informatique.

Selon Wikipédia:

Le terme vient de l'idée de deux signaux s'affrontent à influencer le résultat en premier.

Condition de concurrence dans un circuit logique :

enter image description here

L'industrie du logiciel a pris ce terme sans modification, ce qui le rend un peu difficile à comprendre.

Vous devez effectuer un remplacement pour le mapper au monde logiciel :

  • "deux signaux" => "deux threads"/"deux processus"
  • "influencer la sortie" => "influencer un état partagé"

Ainsi, dans l'industrie du logiciel, la condition de concurrence signifie que "deux threads"/"deux processus" s'affrontent pour "influencer un état partagé", et le résultat final de l'état partagé dépendra d'une différence de temps subtile, qui pourrait être causée par un état spécifique. ordre de lancement des threads/processus, planification des threads/processus, etc.

Une condition de concurrence critique est une situation de programmation concurrente dans laquelle deux threads ou processus concurrents se disputent une ressource et l'état final qui en résulte dépend de celui qui obtient la ressource en premier.

Microsoft a en fait publié un rapport très détaillé article sur cette question des conditions de concurrence et des impasses.Le résumé le plus résumé serait le paragraphe de titre :

Une condition de course se produit lorsque deux threads accèdent à une variable partagée en même temps.Le premier thread lit la variable et le deuxième thread lit la même valeur à partir de la variable.Ensuite, le premier thread et le deuxième thread effectuent leurs opérations sur la valeur, et ils courent pour voir quel thread peut écrire la valeur en dernier à la variable partagée.La valeur du thread qui écrit sa valeur en dernier est préservée, car le thread écrit sur la valeur que le thread précédent a écrit.

Qu'est-ce qu'une condition de concurrence ?

Situation dans laquelle le processus dépend de manière critique de la séquence ou du calendrier d'autres événements.

Par exemple, le processeur A et le processeur B les deux besoins ressource identique pour leur exécution.

Comment les détecter ?

Il existe des outils pour détecter automatiquement les conditions de concurrence :

Comment les gérez-vous ?

La condition de concurrence peut être gérée par Mutex ou Sémaphores.Ils agissent comme un verrou permettant à un processus d'acquérir une ressource en fonction de certaines exigences afin d'éviter les situations de concurrence critique.

Comment éviter qu’ils se produisent ?

Il existe différentes manières de prévenir les conditions de concurrence critique, telles que Évitement des sections critiques.

  1. Il n’y a pas deux processus simultanés dans leurs régions critiques.(Exclusion mutuelle)
  2. Aucune hypothèse n'est faite sur les vitesses ou le nombre de processeurs.
  3. Aucun processus s'exécutant en dehors de sa région critique qui bloque d'autres processus.
  4. Aucun processus ne doit attendre indéfiniment pour entrer dans sa région critique.(A attend les ressources B, B attend les ressources C, C attend les ressources A)

Une condition de concurrence critique est une situation indésirable qui se produit lorsqu'un périphérique ou un système tente d'effectuer deux ou plusieurs opérations en même temps, mais en raison de la nature du périphérique ou du système, les opérations doivent être effectuées dans l'ordre approprié afin d'être exécutées. fait correctement.

Dans la mémoire ou le stockage de l'ordinateur, une situation de concurrence critique peut se produire si des commandes de lecture et d'écriture d'une grande quantité de données sont reçues presque au même instant et que la machine tente d'écraser tout ou partie des anciennes données alors que ces anciennes données sont toujours en cours d'exécution. lire.Le résultat peut être un ou plusieurs des éléments suivants :un crash informatique, une « opération illégale », une notification et un arrêt du programme, des erreurs de lecture des anciennes données ou des erreurs d'écriture des nouvelles données.

Voici l'exemple classique de solde de compte bancaire qui aidera les débutants à comprendre facilement les threads en Java par rapport à Java.conditions de course:

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));

}

Essayez cet exemple de base pour une meilleure compréhension de la condition de concurrence :

    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;
        }
    }
}

Vous ne voulez pas toujours ignorer une condition de concurrence critique.Si vous avez un indicateur qui peut être lu et écrit par plusieurs threads, et que cet indicateur est défini sur « terminé » par un thread afin que les autres threads arrêtent le traitement lorsque l'indicateur est défini sur « terminé », vous ne voulez pas que « course condition" à éliminer.En fait, celle-ci peut être qualifiée de condition de concurrence bénigne.

Cependant, en utilisant un outil de détection des conditions de concurrence, celle-ci sera détectée comme une condition de concurrence nuisible.

Plus de détails sur les conditions de course ici, http://msdn.microsoft.com/en-us/magazine/cc546569.aspx.

Considérons une opération qui doit afficher le décompte dès que le décompte est incrémenté.c'est-à-dire, dès que Contre-fil incrémente la valeur Fil d'affichage doit afficher la valeur récemment mise à jour.

int i = 0;

Sortir

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

Ici Contre-fil obtient le verrou fréquemment et met à jour la valeur avant Fil d'affichage l'affiche.Il existe ici une condition de course.La condition de concurrence peut être résolue en utilisant la synchronisation

Tu peux prévenir les conditions de concurrence, si vous utilisez des classes "Atomic".La raison est simplement que le thread ne sépare pas les opérations get et set, l'exemple est ci-dessous :

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

Du coup, vous en aurez 7 en lien "ai".Bien que vous ayez effectué deux actions, les deux opérations confirment le même thread et aucun autre thread n'interférera avec cela, cela signifie qu'il n'y a pas de conditions de concurrence !

Une condition de concurrence critique est une situation indésirable qui se produit lorsque deux processus ou plus peuvent accéder et modifier les données partagées en même temps. Cela s'est produit en raison d'un conflit d'accès à une ressource.Un problème de section critique peut provoquer une condition de concurrence critique.Pour résoudre une condition critique parmi le processus, nous n'avons supprimé qu'un seul processus à la fois qui exécute la section critique.

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();
        }
    }
}
}
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top