11
votes

Est-ce que ce fil de mise en oeuvre de la file d'attente (sans verrouillage)?

J'essaie de créer une mise en œuvre de la file d'attente sans verrouillage en Java, principalement pour l'apprentissage personnel. La file d'attente doit être générale, autorisant simultanément n'importe quel nombre de lecteurs et / ou d'écrivains.

Voulez-vous l'examiner et suggérer des améliorations / problèmes que vous trouvez? P>

Merci. P> Merci. P> Merci. P >

import java.util.concurrent.atomic.AtomicReference;

public class LockFreeQueue<T> {
    private static class Node<E> {
        E value;
        volatile Node<E> next;

        Node(E value) {
            this.value = value;
        }
    }

    private AtomicReference<Node<T>> head, tail;

    public LockFreeQueue() {
        // have both head and tail point to a dummy node
        Node<T> dummyNode = new Node<T>(null);
        head = new AtomicReference<Node<T>>(dummyNode);
        tail = new AtomicReference<Node<T>>(dummyNode);
    }

    /**
     * Puts an object at the end of the queue.
     */
    public void putObject(T value) {
        Node<T> newNode = new Node<T>(value);
        Node<T> prevTailNode = tail.getAndSet(newNode);
        prevTailNode.next = newNode;
    }

    /**
     * Gets an object from the beginning of the queue. The object is removed
     * from the queue. If there are no objects in the queue, returns null.
     */
    public T getObject() {
        Node<T> headNode, valueNode;

        // move head node to the next node using atomic semantics
        // as long as next node is not null
        do {
            headNode = head.get();
            valueNode = headNode.next;
            // try until the whole loop executes pseudo-atomically
            // (i.e. unaffected by modifications done by other threads)
        } while (valueNode != null && !head.compareAndSet(headNode, valueNode));

        T value = (valueNode != null ? valueNode.value : null);

        // release the value pointed to by head, keeping the head node dummy
        if (valueNode != null)
            valueNode.value = null;

        return value;
}


1 commentaires

4 Réponses :


4
votes

Le code n'est pas du thread-coffre-fort. Considérons putobject (...) : xxx

L'instruction 2ND ajoute le nouveau nœud avant que le pointeur suivant de la noeud précédent a été défini. Cela n'arrive que dans la 3ème déclaration. Ainsi, il y a une fenêtre dans laquelle le suivant est null ; C'est-à-dire une condition de course.

même si vous avez réparé cela, il y a un problème plus insidieux. Un fil de lecture Le champ suivant pour un objet de nœud ne sera pas nécessairement voir la valeur qu'un deuxième thread vient d'écrire. C'est une conséquence du modèle de mémoire Java. Dans ce cas, la voie à s'assurer que la lecture suivante voit toujours la valeur écrite précédente est de:

  • déclarer suivant pour être volatile ou
  • Faites à la fois la lecture et l'écriture dans un mutex primitif sur le même objet.

    Edit: En lisant le code de getObject () et putobject () plus en détail, je peux voir que rien ne force la valeur non nulle de Suivant pour être rincé à la mémoire dans putobject , et rien ne force getObject Pour lire Suivant de la mémoire principale. Donc, le code getObject peut voir la valeur erronée de suivant , ce qui lui permet de retourner null quand il y a vraiment un élément de la file d'attente.


7 commentaires

Désolé, mais vous vous trompez avec la prochaine version nulle. En fait, Tail.Next est toujours nul, et cela ne fait aucun problème. Si la prochaine est NULL dans GetObject, la boucle se termine et NULL est renvoyée (la file d'attente est vide). En outre, la boucle de getObject ne peut tourner que si un autre thread a écrit la tête avec succès - par exemple. L'autre thread a réussi, ce que nous voulons d'algorithmes sans verrouillage.


Désolé, mais il est toujours incorrect, dans de nombreux domaines (l'ensemble Edit2, par exemple, indique que vous n'avez pas remarqué que la file d'attente contient toujours au moins un nœud factice).


Merci de votre aide. J'ai mis à jour mon code pour créer suivant volatiles et renommé NextNode à Valuenode dans getObject () pour faire son but plus clair. Mais je suppose que la lecture d'une valeur fade dans le fil de lecteur ne fait pas le code "pas thread-coffre-fort"; C'est juste une question de performance, non?


@Hosam - Non, je pense que getObject () peut retourner null quand il y a vraiment un élément de la file d'attente. En fait, cela peut le faire à plusieurs reprises.


@Stephen C: Oui, cela peut, mais cela ne fait que retarder le traitement de cet élément. Je suppose que cela dépend de la manière dont nous le considérons, si la disponibilité d'un élément de la file d'attente est requise immédiatement ou peut être reçue ultérieurement. Je l'ai réparé quand même (je pense), alors merci de le pointer.


@Hosam - 1) Il est théoriquement possible qu'un thread particulier ait toujours vu une valeur obsolète de la prochaine lorsqu'il appelle getObject () . 2) Je dirais que la méthode getObject () qui renvoyée occasionnellement null pour une file d'attente non vide enfreint le principe de la moins surprise.


@Stephen: Tu as raison. C'est certainement vrai. Avez-vous d'autres suggestions ou notes sur le code?



1
votes

Je ne vois que deux problèmes avec votre code:

  • On est le problème avec les opérations de mémoire Commander Stephen C mentionné (peut être résolu en déclarant Suivant et de valeur volatile ) ( Nœud: la valeur a le même problème)

  • second est un élément plus subtil et non associé à la concurrence: après avoir renvoyé un objet dans getObject , vous conservez toujours sa référence de la tête. Cela pourrait conduire à une fuite de mémoire.

    Sinon, l'algorithme est correct. Une démonstration vague (supposer que ce qui précède est fixe):

    l1: queue ne peut jamais être supprimé de la file d'attente. Cela tient parce que quelque chose est stocké dans queue , il a suivant == null . En outre, lorsque vous attribuez quelque chose à xxx.next (uniquement dans putobject ), il ne peut pas être queue , en raison de l'atomicité du getAndset et de la commande entre l'écriture volatile et la lecture suivante - supposons que vous lisez un non NULL , cette valeur doit avoir été écrite par putobject et donc arrive-après la dernière ligne de celui-ci. Cela signifie qu'il arrive-après la ligne précédente, ce qui signifie que la valeur que nous lisons n'est pas de queue . .

    En conséquence, c'est que chaque objet dans putobject sera éventuellement accessible de tête . C'est parce que nous nous connectons après queue , et ce nœud ne peut être supprimé qu'après que nous écrivions la référence du nouveau nœud à son suivant , ce qui signifie que le nouveau nœud est accessible depuis tête .

    Les objets ajoutés sont commandés de manière triviale par le getAndset dans putobject .

    Les objets de déséquilibre sont commandés conformément au succès comparaisant des opérations dans getObject .

    Le getObject / putobject est commandé en fonction de l'écriture / lecture au champ volatile suivant . .


3 commentaires

Comment voulez-vous dire "tu conserve toujours sa référence de la tête"? Head est défini sur NEXTNODE dans cette méthode.


Je veux dire, la valeur dans newnode est renvoyée et retenue via tête . Si aucun autre élément n'est pris à partir de la file d'attente (comme si la file d'attente est vide par la suite), la référence à la valeur renvoyée n'est jamais libérée.


Merci! J'ai corrigé la question de la fuite de mémoire et j'ai effectué suivant volatile. Mais je ne comprends pas encore pourquoi la valeur doit être volatile. Pourriez-vous s'il vous plaît expliquer plus loin?



1
votes

Je pense que vous obtiendrez une NPE lorsque vous essayez de "libérer la valeur ..." si vous appelez xxx

puisque vous n'effectuez aucune vérification de nullité sur valuenode là, malgré la protection contre elle ci-dessus.


1 commentaires

Tu as raison. J'avais manqué ça totalement! Je l'ai réparé maintenant. Merci!



1
votes

Vous devriez jeter un oeil à la mise en œuvre de Java.Util.ConCurrent.ConCurrentLinkedQueue http: //java.sun. COM / J2SE / 1.5.0 / DOCS / API / JAVA / UTIL / Concurrent / ConcurrentLinkedQueue.html Il fait à peu près ce que vous essayez d'atteindre


2 commentaires

Merci. Je vais vérifier pour plus d'idées. Cependant, le ConcurrentLinkedqueue est trop complexe, car il prend en charge de nombreuses méthodes, tandis que ma file d'attente est beaucoup plus simple, ce qui me permet de faire plus d'hypothèses et d'essayer de plus d'optimisations.


Il y a un argument selon lequel le code sans verrouillage est complexe par nature. ;)