Barrières

Programmation concurrentielle avec barrières

Ce site ne sera plus alimenté de contenu après août 2014. Tous les nouveaux articles seront redigés pour www.waitingforcode.com

Un concept intéressant dans le développement multi threading sont des barrières. Grâce à elles on peut synchroniser encore mieux l'exécution du code par plusieurs tâches.

Le package java.util.concurrent contient une classe CyclicBarrier. Son but est de permettre une synchronisation séquentielle et groupée des Threads. Dans le code on détermine une ou plusieurs barrières qu'ils doivent atteindre chronologiquement pour avancer. Il est donc impossible qu'un Thread passe à la deuxième barrière sans qu'un autre Thread ne l'a pas atteinte. Regardons cela sur un exemple.

Imaginons qu'on veut illustrer trois groupes de touristes qui voyagent en bus depuis la Pologne vers l'Espagne. Ils quittent leur pays, traversent l'Allemagne et la France avant d'arriver en Espagne. On suppose que les frontières ne sont pas ouvertes et chaque pays dispose d'un checkpoint où les douaniers vérifient les passeports. Voici le code :

import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.BrokenBarrierException;
import java.util.Iterator;
import java.util.ArrayList;

public class Barrier {
  public static void main(String[] args) {
    ArrayList<Country> barriers = new ArrayList<Country>();
    barriers.add(new Country(3, "Poland - Germany"));
    barriers.add(new Country(3, "Germany - France"));
    barriers.add(new Country(3, "France - Spain"));
    Thread t1 = new Thread(new Tourist("#1", barriers));
    Thread t2 = new Thread(new Tourist("#2", barriers));
    Thread t3 = new Thread(new Tourist("#3", barriers));
    
    t1.start();
    t2.start();
    t3.start();
  } 
}

class Tourist implements Runnable {
  private String name;
  private ArrayList<Country> barriers = new ArrayList<Country>();

  public Tourist(String n, ArrayList b) {
    name = n;
    barriers = b;
  }

  public void run() {
    try {
      for (Iterator listIter = barriers.iterator(); listIter.hasNext();) {
        Country barrier = (Country) listIter.next();
        System.out.println("The group " + name + " pass the frontier " + barrier.getName());
        barrier.await();
      }
      System.out.println("The group " + name + " is in Spain");
    } catch (InterruptedException e) {
      System.out.println("An InterruptedException occured " + e.getMessage());
    } catch (BrokenBarrierException e) {
      System.out.println("A BrokenBarrierException occured " + e.getMessage());
    }
  }
}

class Country extends CyclicBarrier {
  private String name;    

  public Country(int capacity, String n) {
    super(capacity);
    name = n;
  }
  public String getName() {
    return name;
  }
}

Sur l'écran on voit le résultat :

The group #1 pass the frontier Poland - Germany
The group #2 pass the frontier Poland - Germany
The group #3 pass the frontier Poland - Germany
The group #1 pass the frontier Germany - France
The group #2 pass the frontier Germany - France
The group #3 pass the frontier Germany - France
The group #3 pass the frontier France - Spain
The group #1 pass the frontier France - Spain
The group #2 pass the frontier France - Spain
The group #1 is in Spain
The group #3 is in Spain
The group #2 is in Spain

Dans le code on voit bien la présence d'une classe qui hérite de CyclicBarrier, la classe Country. Elle possède une méthode await() qui permet de patienter que tous les Threads arrivent à ce point. On pourrait également gérer le timeout pour cette fonction ainsi que la gestion des Threads cassés. Les deux pourraient nous aider à éviter des problèmes liés au deadlock, décrits dans l'article concernant la synchronisation des Threads en Java.

Qu'est-ce que c'est CyclicBarrier en Java

CyclicBarrier est une classe qui permet de mieux gérer la synchronisation entre de différents threads. Grâce à elle, les threads peuvent s'attendre les uns les autres afin d'attendre ensemble un point précis. Ce point est nommé barrier point (barrière). Les threads participants dans le procesuss s'appelent parties.

Chaque nouveau processus CyclicBarrier est invoquée avec un nombre de parties qui doivent être exécutés. Les threads attendent les uns les autres à côté de la barrière grâce à l'appel de la méthode CyclicBarrier.await(). Quant tous les threads ont appelés cette fonction, on considère que la barrière a été atteinte par toutes les parties. Dans ce cas, grâce au constructeur CyclicBarrier(int nb, Runnable action), on peut déclencher une action comprise dans l'implémentation du paramètre action.

Exemple CyclicBarrier

Pour transférer CyclicBarrier dans le monde réel, on peut imaginer la situation où l'on gère l'assemblage d'une voiture. Dans notre exemple simplfié, une voitre sera composée d'une vitre, des portes et d'une carrosserie. On ne se préoccupera pas d'ordre de montage dans cet exemple.

Au tout début de notre code on initialisera CyclicBarrier en lui passant en paramètre le nombre des threads qui doivent appeler la méthode await() afin qu'une barrière soit considérée comme atteinte. A chaque fois où toutes les parties l'atteignent, la méthode run() de l'instance de la classe spécifiée dans le deuxième paramètre est appelée.

import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierSample {
    private static final String[] components = new String[] {"window", "door", "body"};
    public static int producedCars = 0;

    public static void main(String[] args)  {
        int carsToAssembly = 5;

        CyclicBarrier cyclicBarrier = new CyclicBarrier(components.length, new Garage());
        System.out.println("Assembly started for " + carsToAssembly + " cars");

        for (int i = 0; i < carsToAssembly; i++) {
            for (String component : components) {
                new Assembly(i, cyclicBarrier, component, 3000).start();
            }
        }
    }

    public synchronized static void incrementProducedChars() {
        producedCars++;
    }
}

class Assembly extends Thread {
    private CyclicBarrier cyclicBarrier;
    private String component;
    private int id;
    private int waitingTime = 1000; // 1 sec for waiting time

    public Assembly(int id, CyclicBarrier cyclicBarrier, String component, int waitingTime) {
        this.id = id;
        this.cyclicBarrier = cyclicBarrier;
        this.component = component;
        this.waitingTime = waitingTime;
    }

    public void run() {
        try {
            System.out.println("Assemblying " + component + " for car " + id);
            Thread.sleep(waitingTime);
            cyclicBarrier.await();
        } catch (Exception e) {
            System.out.println("An exception occured on assemblying " + component + " : " + e.getMessage());
        }
    }
}

class Garage implements Runnable {

    public void run() {
        CyclicBarrierSample.incrementProducedChars();
        System.out.println("New car was produced. We have already produced " + CyclicBarrierSample.producedCars + " cars");
    }
}

Voici ce que cela donne sur l'écran :

Assembly started for 5 cars
Assemblying winow for car 0
Assemblying door for car 0
Assemblying body for car 0
Assemblying winow for car 1
Assemblying door for car 1
Assemblying body for car 1
Assemblying winow for car 2
Assemblying door for car 2
Assemblying winow for car 3
Assemblying door for car 3
Assemblying body for car 2
Assemblying body for car 3
Assemblying door for car 4
Assemblying winow for car 4
Assemblying body for car 4
New car was produced. We have already produced 1 cars
New car was produced. We have already produced 2 cars
New car was produced. We have already produced 3 cars
New car was produced. We have already produced 4 cars
New car was produced. We have already produced 5 cars

Regardons maintenant ce que se passe quand on ne veut pas assembler les portes pour la première voiture. Pour faire cela, on modifie la méthode run() de la classe Assembly :

if (id != 0 || (id == 0 && !component.equals("door"))) cyclicBarrier.await();

On verra alors sur l'écran :

Assembly started for 5 cars
Assemblying window for car 0
Assemblying window for car 1
Assemblying door for car 1
Assemblying door for car 2
Assemblying body for car 2
Assemblying body for car 3
Assemblying body for car 0
Assemblying body for car 1
Assemblying window for car 2
Assemblying window for car 3
Assemblying door for car 3
Assemblying door for car 0
Assemblying window for car 4
Assemblying door for car 4
Assemblying body for car 4
New car was produced. We have already produced 1 cars
New car was produced. We have already produced 2 cars
New car was produced. We have already produced 3 cars
New car was produced. We have already produced 4 cars

Le code ne va jamais s'exécuter complètement car les threads de la première voiture vont attendre l'installation des portes sans savoir qu'elle n'aura jamais lieu. Pour éviter ce genre de situations où les threads attendent les uns les autres pendant un temps indéfini, on peut préciser le timeout dans la méthode await. Grâce à lui, les threads vont patienter juste un certain nombre de temps et ne bloqueront pas l'exécution du programme. Regardons comment on a modifié la même méthode qu'avant et qu'est-ce que cela a donné sur l'écran après l'exécution :

if (id != 0 || (id == 0 && !component.equals("door"))) cyclicBarrier.await(10l, TimeUnit.SECONDS);

Sur l'écran on verra alors, au bout de 13 secondes :

Assembly started for 5 cars
Assemblying window for car 0
Assemblying door for car 0
Assemblying body for car 0
Assemblying window for car 1
Assemblying door for car 1
Assemblying body for car 1
Assemblying window for car 2
Assemblying door for car 2
Assemblying body for car 2
Assemblying window for car 3
Assemblying door for car 3
Assemblying body for car 3
Assemblying door for car 4
Assemblying window for car 4
Assemblying body for car 4
New car was produced. We have already produced 1 cars
New car was produced. We have already produced 2 cars
New car was produced. We have already produced 3 cars
New car was produced. We have already produced 4 cars
An exception occured on assemblying door : null
An exception occured on assemblying window : null
Bartosz KONIECZNY Concurrence

Une question ? Une remarque ?

*

*

Moi

Développeur d'applications Internet et journaliste passionné par l'adjectif français. Un aigle polonais orienté vers la progression, volant très haut et écoutant du zouk après les matches du foot français.

Vous appréciez mon travail ?

Pour contribuer au développement de ce site, ou pour remercier pour des articles rédigés, vous pouvez faire un don.

Un conseil PHP

Comment créer un signature key pour OAuth

Un signature key est l'un des deux composants qui constituent la signature de la requête OAuth. Il est composé de : - la clé secrète que le fournisseur (Google, Yahoo...) attribue au client, elle doit être toujours présente dans le signature key - le ticket secret qui est renvoyé à de différentes étapes de l'échange avec le web service du fournisseur; s'il est absent (comme lors de la récupération du ticket de la requête), il faut alors passer une chaîne de caractères vide Les deux paramètres doivent être encodés. Une méthode pour générer le signature key peut se présenter ainsi :

function setSignatureKey($tokenSecret)
{
  return str_replace('%7E', '~', rawurlencode($this->userData['secret'])).'&'.str_replace('%7E', '~', rawurlencode($tokenSecret));
}