web-dev-qa-db-fra.com

bloc synchronisé - verrouiller plus d'un objet

Je suis en train de modéliser un jeu où plusieurs joueurs (discussions) se déplacent en même temps ... Les informations sur l'emplacement d'un joueur sont stockées deux fois: le joueur a une variable "hostField" qui fait référence à un champ situé sur Le tableau et chaque terrain ont une liste de tableaux qui contient les joueurs qui se trouvent actuellement sur ce terrain.

Je ne suis pas très heureux du fait que je dispose d'informations redondantes, mais je n'ai trouvé aucun moyen de l'éviter sans parcourir en boucle un grand ensemble de données.

Cependant, lorsqu'un joueur passe d'un terrain à un autre, j'aimerais m'assurer que (1) les informations redondantes restent liées (2) personne d'autre ne manipule le terrain pour le moment.

Par conséquent, je dois faire quelque chose comme

synchronized(player, field) {
    // code
}

Ce qui n'est pas possible, non?

Que devrais-je faire? :)

37
speendo

En fait, la synchronisation concerne le code, pas les objets ou les données. La référence d'objet utilisée comme paramètre dans un bloc synchronisé représente le verrou.

Donc si vous avez un code comme:

class Player {

  // Same instance shared for all players... Don't show how we get it now.
  // Use one dimensional board to simplify, doesn't matter here.
  private List<Player>[] fields = Board.getBoard(); 

  // Current position
  private int x; 

  public synchronized int getX() {
    return x;
  }

  public void setX(int x) {
    synchronized(this) { // Same as synchronized method
      fields[x].remove(this);
      this.x = x;
      field[y].add(this);
    }
  }
}

Alors, bien qu’il soit dans le bloc synchronisé, le champ d’accès à n’est pas protégé car le verrou n’est pas identique (il se trouve sur différentes instances). Ainsi, votre liste de joueurs pour votre plateau peut devenir incohérente et provoquer des exceptions d'exécution.

Au lieu de cela, si vous écrivez le code suivant, cela fonctionnera car nous n’avons qu’un verrou partagé pour tous les lecteurs:

class Player {

  // Same instance shared for all players... Don't show how we get it now.
  // Use one dimensional board to simplify, doesn't matter here.
  private List<Player>[] fields; 

  // Current position
  private int x;

  private static Object sharedLock = new Object(); // Any object's instance can be used as a lock.

  public int getX() {
    synchronized(sharedLock) {
      return x;
    }
  }

  public void setX(int x) {
    synchronized(sharedLock) {
      // Because of using a single shared lock,
      // several players can't access fields at the same time
      // and so can't create inconsistencies on fields.
      fields[x].remove(this); 
      this.x = x;
      field[y].add(this);
    }
  }
}

Assurez-vous d'utiliser un seul verrou pour accéder à tous les joueurs, sinon l'état de votre plateau sera incohérent.

21
Nicolas Bousquet

Une solution triviale serait

synchronized(player) {
    synchronized(field) {
        // code
    }
}

Cependant, assurez-vous de toujours verrouiller les ressources dans le même ordre pour éviter les blocages.

Notez qu'en pratique, le goulot d'étranglement est le champ, de sorte qu'un verrou unique sur le champ (ou sur un objet de verrouillage commun dédié, comme l'a bien souligné @ ripper234) peut suffire (à moins que vous ne manipuliez simultanément des joueurs de manière conflictuelle) .

53
Péter Török

Lors de l'utilisation de la simultanéité, il est toujours difficile de donner de bonnes réponses. Cela dépend fortement de ce que vous faites réellement et de ce qui compte vraiment.

De ma compréhension, un mouvement de joueur implique:

1 Mise à jour de la position du joueur.

2 Retirer le lecteur du champ précédent.

3 Ajouter un joueur à un nouveau champ.

Imaginez que vous utilisiez plusieurs serrures à la fois mais que vous n'en acquériez qu'une à la fois: - Un autre joueur peut parfaitement regarder le mauvais moment, essentiellement entre 1 et 2 ou 2 et 3. Certains joueurs peuvent sembler avoir disparu du tableau par exemple.

Imaginez votre verrouillage imbriqué comme ceci:

synchronized(player) {
  synchronized(previousField) {
    synchronized(nextField) {
      ...
    }
  }
}

Le problème est ... Cela ne fonctionne pas, voir cet ordre d'exécution pour 2 threads:

Thread1 :
Lock player1
Lock previousField
Thread2 :
Lock nextField and see that player1 is not in nextField.
Try to lock previousField and so way for Thread1 to release it.
Thread1 :
Lock nextField
Remove player1 from previous field and add it to next field.
Release all locks
Thread 2 : 
Aquire Lock on previous field and read it : 

Thread 2 pense que ce joueur1 a disparu de l'ensemble du conseil. Si cela pose un problème pour votre application, vous ne pouvez pas utiliser cette solution.

Problème supplémentaire pour le verrouillage imbriqué: les threads peuvent rester bloqués . Imagine 2 joueurs: ils échangent leur position exactement au même moment:

player1 aquire it's own position at the same time
player2 aquire it's own position at the same time
player1 try to acquire player2 position : wait for lock on player2 position.
player2 try to acquire player1 position : wait for lock on player1 position.

=> Les deux joueurs sont bloqués.

La meilleure solution à mon avis est de n'utiliser qu'un seul verrou, pour tout l'état du jeu.

Lorsqu'un joueur veut lire l'état, il verrouille tout l'état du jeu (joueurs et plateau) et en fait une copie pour son propre usage. Il peut alors traiter sans aucun verrou.

Lorsqu'un joueur veut écrire l'état, il verrouille tout l'état du jeu, écrit le nouvel état puis relâche le verrou.

=> Le verrouillage est limité aux opérations de lecture/écriture de l’état du jeu. Le joueur peut effectuer un "long" examen de l'état du tableau sur sa propre copie.

Cela empêche tout état incohérent, comme un joueur dans plusieurs domaines ou aucun, mais n'empêche pas ce joueur peut utiliser un "ancien" état.

Cela peut paraître bizarre, mais c'est le cas typique d'un jeu d'échecs. Lorsque vous attendez que l'autre joueur bouge, vous voyez le tableau comme avant le coup. Vous ne savez pas quel coup fera l'autre joueur et jusqu'à ce qu'il soit enfin déplacé, vous travaillez dans un "ancien" état.

7
Nicolas Bousquet

Vous ne devriez pas vous sentir mal à propos de votre modélisation - il ne s'agit que d'une association navigable dans les deux sens. 

Si vous prenez soin (comme dans les autres réponses données) de manipuler atomique, par exemple dans les méthodes de terrain, c'est bien.


public class Field {

  private Object lock = new Object();

  public removePlayer(Player p) {
    synchronized ( lock) {
      players.remove(p);
      p.setField(null);
    }
  }

  public addPlayer(Player p) {
    synchronized ( lock) {
      players.add(p);
      p.setField(this);
    }
  }
}

Ce serait bien si "Player.setField" était protégé.

Si vous avez besoin de plus d'atomicité pour la sémantique de "déplacement", montez d'un niveau pour le tableau.

1
mtraut

En lisant toutes vos réponses, j'ai essayé d'appliquer le design suivant:

  1. Verrouiller uniquement les joueurs, pas les champs
  2. Effectuer des opérations sur le terrain uniquement dans des méthodes/blocs synchronisés
  3. dans une méthode/un bloc synchronisé, vérifiez toujours en premier lieu si les conditions préalables à l'origine de l'appel de la méthode/du bloc synchronisé sont toujours valables

Je pense que 1. évite les blocages et 3. est important car les choses pourraient changer pendant l'attente d'un joueur.

De plus, je peux aller sans verrouiller les champs car dans mon jeu, plus d'un joueur peut rester sur un terrain, mais pour certains threads, une interaction doit être faite. Cette interaction peut être réalisée en synchronisant les lecteurs - pas besoin de synchroniser les champs ...

Qu'est-ce que tu penses?

0
speendo