Cours et TD du 30/11/2023 et 07/12/2023
Je n’ai rien suivi désolé, le cours vient d’ailleurs

Comment programmer en utilisant des lock, des structures de données partagées ?

Ensemble d’entiers partagés

chainedlist

type Node {
  int key;
  Node next;
}

Coarse-gain locking (le “gain-grossier”)

On va écrire une première fonction ajouter, on lui donne un certain key, qui est un entier. Il y a toujours au moins 2 Node : début et fin. Chaque élément existe dans la liste au plus une fois (l’occurrence d’un élément est 0 ou 1).

Procédure add(key : integer) return boolean
begin
  Node pred # prédécesseur
  Node curr # séquenceur
  lock(l) # on prend le lock
  try
    pred := head
    curr := pred.next
    while (curr.next < key) # rappel : key = le paramètre
      pred := curr # le prédécesseur devient le courant
      curr := curr.next # le courant devient le sucesseur
    endwhile
    if (key = curr.key) then # l'élément est déjà dans l'ensemble
      return false # on veut pas avoir de duplications
    else
      Node node = Node(key) // Création d'un nouveau noeud
      node.next := curr
      pred.next := node
      return true # tout s'est bien passé
    endif
  finally
    unlock(l) # on lâche le lock
  endtry
end


Procédure remove(key : integer) return boolean
begin
  Node pred # prédécesseur
  Node curr # séquenceur
  lock(l) # on prend le lock
  try
    pred := head
    curr := pred.next
    while (curr.next < key) # rappel : key = le paramètre
      pred := curr # le prédécesseur devient le courant
      curr := curr.next # le courant devient le sucesseur
    endwhile
    if (key = curr.key) then # on a trouvé l'élément
      pred.next := curr.next
      return true
    else
      return false # l'élément n'existe pas
    endif
  finally
    unlock(l) # on lâche le lock
  endtry
end

Inconvénient

L’ajout d’un élément à un emplacement X ne doit pas empêcher l’ajout d’un élément dans une autre zone

Fin-gain locking (plus locale)

fingain

Procédure add(key : integer) return boolean
begin
  lock(head) # verrou sur la tête de liste
  Node pred = head
  try
    curr := pred.next
    lock(curr) # à ce stade on lock sur l'élément `head` et le suivant
    try
      while (curr.next < key) # on fait la recherche, pendant la recherche on va déplacer nos locks
        unlock(pred)
        pred := curr # le prédécesseur devient le courant
        curr := curr.next # le courant devient le sucesseur
        lock(curr)
      endwhile
      if (key = curr.key) then # l'élément est déjà dans l'ensemble
        return false # on veut pas avoir de duplications
      endif
      Node node = Node(key) // Création d'un nouveau noeud
      node.next := curr
      pred.next := node
      return true # tout s'est bien passé
    finally
      unlock(curr) # on lâche le premier lock
    endtry
  finally
    unlock(pred) # on lâche le second lock
  endtry
end


Procédure remove(key : integer) return boolean
begin
  lock(head) # verrou sur la tête de liste
  try
    Node pred = head
    curr := pred.next
    lock(curr) # à ce stade on lock sur l'élément `head` et le suivant
    try
      while (curr.next < key) # on fait la recherche, pendant la recherche on va déplacer nos locks
        unlock(pred)
        pred := curr # le prédécesseur devient le courant
        curr := curr.next # le courant devient le sucesseur
        lock(curr)
      endwhile
      if (key = curr.key) then # on a trouvé l'élément
        pred.next := curr.next
        return true # tout s'est bien passé
      else
        return false # l'élément n'existe pas
      endif
    finally
      unlock(curr) # on lâche le premier lock
    endtry
  finally
    unlock(pred) # on lâche le second lock
  endtry
end

Comment ça marche ?

fingain_explication

On est toujours obligé de prendre les lock dans un certain ordre, comment on fait ?

bourrin
De manière plus générale, pendant que je me trouve à l’emplacement i, il y a d’autres processus qui peuvent venir et supprimer tous cette partie de la liste : par exemple, avant même d’avoir pris le lock, il y a un processus qui change le panorama, donc lorsqu’on va prendre le lock, tout le monde a déjà changé, ainsi c’est possible que je travaille sur une liste qui n’existe même plus (éléments plus accessibles à partir du head).

Dans l’exemple suivant, on veut retirer l’élément curr, mais un autre processus peut venir et faire lock sur le prédécesseur de pred, mais alors, lorsque ce processus lâche les locks, j’arrive à faire lock, mais je ne suis
pas au courant que la liste à changer. Je retire curr, et ainsi quand je fais return true, la signification est que l’opération a réussi même si c’est faux.

bourrin2
Donc, l’idée de “foncer”, faire lock et effectuer l’opération, c’est une bonne idée. Mais cela ne suffit pas, car entre le moment de la recherche et le moment du lock, il se passe des choses qui peuvent modifier notre liste.

Alors qu’est-ce qu’il faut vérifier ?

Procédure add(key : integer) return boolean
begin
  while true
    pred := head
    curr := pred.next
    while (curr.next < key)
      pred := curr # le prédécesseur devient le courant
      curr := curr.next # le courant devient le successeur
    endwhile
    # C'est que lorsqu'on arrive qu'on prend les locks
    lock(pred)
    lock(curr)
    try
      if validate(pred, curr) # on regarde la situation
        if (key = curr.key) # si l'élément est déjà dans l'ensemble
          return false # on veut pas avoir de duplication
        else
          Node node := Node(key) # création d'un nouveau noeud
          node.next := curr
          pred.next := node
          return true # tout s'est bien passé
        endif
      endif
    finally
      # On lâche les locks
      unlock(pred)
      unlock(curr)
    endtry
  endwhile
end


# Regarde si la structure n'a pas changer
Procédure validate(pred : Node, curr : Node) return boolean
begin
  node := head
  while (node.key <= pred.key)
    if (node = pred)
      # Si je suis arriver a pred, je regarde que la situation n'a pas changé
      return (pred.next = curr)
    endif
    node := node.next
  endwhile
  # Si on a dépassé pred, pred n'est plus accessible
  return false
end


Procédure remove(key : integer) return boolean
begin
  while true
    pred := head
    curr := pred.next
    while (curr.next < key)
      pred := curr # le prédécesseur devient le courant
      curr := curr.next # le courant devient le sucesseur
    endwhile
    # C'est que lorsqu'on arrive qu'on prend les locks
    lock(pred)
    lock(curr)
    try
      if (validate(pred, curr)) # on regarde la situation
        if (key = curr.key) # on a trouvé l'élément
          pred.next := curr.next
          return true # tout s'est bien passé
        else
          return false # l'élément n'eexiste pas
        endif
      endif
    finally
      # On lâche les locks
      unlock(pred)
      unlock(curr)
    endtry
  endwhile
end


Procédure contains(key : integer) return boolean
begin
  while true
    pred := head
    curr := pred.next
    while curr.next < key
      pred := curr # le prédécesseur devient le courant
      curr := curr.next # le courant devient le successeur
    endwhile
    # C'est que lorsqu'on arrive qu'on prend les locks
    lock(pred)
    lock(curr)
    try
      if (validate(pred, curr)) # on regarde la situation
        return (key = curr.key)
      endif
    finally
      # On lâche les locks
      unlock(pred)
      unlock(curr)
    endtry
  endwhile
end

boolean validate(Node pred, Node curr) On vérifie quoi ? Problèmes possibles ?

Comme je fais le validate après avoir pris les lock, les choses ne risquent pas de changer. Si la validation ne marche pas (la fonction retourne false) : on lâche les lock et on revient à la ligne while true pour essayer à nouveau. Notre approche dans ce code est optimiste, donc on commence sans prendre les lock.