# Primitives de synchronisation

Code source : Lib/asyncio/locks.py

Les primitives de synchronisation asyncio sont conçues pour être similaires à celles du module `threading` avec deux mises en garde importantes :

- les primitives asyncio ne sont pas thread-safe, elles ne doivent donc pas être utilisées pour la synchronisation des fils d'exécution du système d'exploitation (utilisez `threading` pour cela) ;

- les méthodes de ces primitives de synchronisation n'acceptent pas l'argument timeout ; utilisez la fonction `asyncio.wait_for()` pour effectuer des opérations avec des délais d'attente.

asyncio possède les primitives de synchronisation de base suivantes :

- `Lock`

- `Event`

- `Condition`

- `Semaphore`

- `BoundedSemaphore`

- `Barrier`

## Verrou (lock)

class asyncio.Lock

  Implémente un verrou exclusif (mutex) pour les tâches asynchrones. Ce n'est pas compatible avec les programmes à fils d'exécution multiples.

  Un verrou asyncio peut être utilisé pour garantir un accès exclusif à une ressource partagée.

  La meilleure façon d'utiliser un verrou est une instruction `async with`

      lock = asyncio.Lock()

      # ... later
      async with lock:
          # access shared state

  ce qui équivaut à :

      lock = asyncio.Lock()

      # ... later
      await lock.acquire()
      try:
          # access shared state
      finally:
          lock.release()

  Modifié dans la version 3.10: suppression du paramètre loop.

  coroutine acquire()

    Verrouille (ou acquiert) le verrou.

    Cette méthode attend que le verrou soit déverrouillé (unlocked), le verrouille (positionné sur locked) et renvoie `True`.

    Lorsque plus d'une coroutine est bloquée dans `acquire()` en attendant que le verrou soit déverrouillé, seule une coroutine continue finalement.

    L'acquisition d'un verrou est équitable : la coroutine qui acquiert le verrou est celle qui était la première à attendre le verrou.

  release()

    Libère le verrou.

    Lorsque le verrou est verrouillé, le déverrouille et termine.

    Si le verrou est déjà déverrouillé, une `RuntimeError` est levée.

  locked()

    Renvoie `True` si le verrou est verrouillé.

## Événement (Event)

class asyncio.Event

  Objet événement. Non compatible avec les programmes à plusieurs fils d'exécution.

  Un événement asynchrone peut être utilisé pour notifier plusieurs tâches asynchrones qu'un événement s'est produit.

  Un objet Event gère un drapeau interne qui peut être activé (ou mis à vrai) avec la méthode `set()` et désactivé (ou mis à faux) avec la méthode `clear()`. La méthode `wait()` se bloque jusqu'à ce que l'indicateur soit activé. L'indicateur est initialement désactivé.

  Modifié dans la version 3.10: suppression du paramètre loop.

  Exemple :

      async def waiter(event):
          print('waiting for it ...')
          await event.wait()
          print('... got it!')

      async def main():
          # Create an Event object.
          event = asyncio.Event()

          # Spawn a Task to wait until 'event' is set.
          waiter_task = asyncio.create_task(waiter(event))

          # Sleep for 1 second and set the event.
          await asyncio.sleep(1)
          event.set()

          # Wait until the waiter task is finished.
          await waiter_task

      asyncio.run(main())

  coroutine wait()

    Attend que l'évènement soit activé.

    Si l'événement est activé (vrai), renvoie `True` immédiatement. Sinon bloque jusqu'à ce qu'une autre tâche appelle `set()`.

  set()

    Active l'événement.

    Toutes les tâches en attente de l'événement sont immédiatement réveillées.

  clear()

    Efface (désactive) l'événement.

    Les tâches en attente sur `wait()` seront désormais bloquées jusqu'à ce que la méthode `set()` soit à nouveau appelée.

  is_set()

    Renvoie `True` si l'évènement est actif.

## Condition

class asyncio.Condition(lock=None)

  Objet Condition. Non compatible avec les programmes à plusieurs fils d'exécution.

  Une primitive de condition asynchrone peut être utilisée par une tâche pour attendre qu'un événement se produise, puis obtenir un accès exclusif à une ressource partagée.

  Essentiellement, un objet Condition combine les fonctionnalités d'un `Event` et d'un `Lock`. Il est possible que plusieurs objets Condition partagent un seul verrou, ce qui permet de coordonner l'accès exclusif à une ressource partagée entre différentes tâches intéressées par des états particuliers de cette ressource partagée.

  L'argument optionnel lock doit être un objet `Lock` ou `None`. Dans ce dernier cas, un nouvel objet Lock est créé automatiquement.

  Modifié dans la version 3.10: suppression du paramètre loop.

  La meilleure façon d'utiliser une Condition est une instruction `async with`

      cond = asyncio.Condition()

      # ... later
      async with cond:
          await cond.wait()

  ce qui équivaut à :

      cond = asyncio.Condition()

      # ... later
      await cond.acquire()
      try:
          await cond.wait()
      finally:
          cond.release()

  coroutine acquire()

    Verrouille le verrou sous-jacent.

    Cette méthode attend que le verrou sous-jacent soit déverrouillé, le verrouille et renvoie `True`.

  notify(n=1)

    Réveille au plus n tâches (1 par défaut) en attente de cette condition. La méthode ne fait rien si aucune tâche n'est en attente.

    Le verrou doit être verrouillé avant que cette méthode ne soit appelée et libéré peu de temps après. S'il est appelé avec un verrou déverrouillé, une erreur `RuntimeError` est levée.

  locked()

    Renvoie `True` si le verrou sous-jacent est verrouillé.

  notify_all()

    Réveille toutes les tâches en attente sur cette condition.

    Cette méthode agit comme `notify()`, mais réveille toutes les tâches en attente.

    Le verrou doit être verrouillé avant que cette méthode ne soit appelée et libéré peu de temps après. S'il est appelé avec un verrou déverrouillé, une erreur `RuntimeError` est levée.

  release()

    Libère le verrou sous-jacent.

    Lorsqu'elle est invoquée sur un verrou déverrouillé, une `RuntimeError` est levée.

  coroutine wait()

    Attend d'être notifié.

    Si la tâche appelante n'a pas verrouillé le verrou lorsque cette méthode est appelée, une `RuntimeError` est levée.

    Cette méthode libère le verrou sous-jacent, puis se bloque jusqu'à ce qu'elle soit réveillée par un appel `notify()` ou `notify_all()`. Une fois réveillée, la Condition verrouille à nouveau son verrou et cette méthode renvoie `True`.

  coroutine wait_for(predicate)

    Attend jusqu'à ce qu'un prédicat devienne vrai.

    Le prédicat doit être un appelable dont le résultat est interprété comme une valeur booléenne. La valeur finale est la valeur de retour.

## Sémaphore

class asyncio.Semaphore(value=1)

  Objet Sémaphore. Non compatible avec les programmes à plusieurs fils d'exécution.

  Un sémaphore gère un compteur interne qui est décrémenté à chaque appel `acquire()` et incrémenté à chaque appel `release()`. Le compteur ne peut jamais descendre en dessous de zéro ; quand `acquire()` trouve qu'il est égal à zéro, il se bloque, en attendant qu'une tâche appelle `release()`.

  L'argument optionnel value donne la valeur initiale du compteur interne (`1` par défaut). Si la valeur donnée est inférieure à `0` une `ValueError` est levée.

  Modifié dans la version 3.10: suppression du paramètre loop.

  La meilleure façon d'utiliser un sémaphore est une instruction `async with`

      sem = asyncio.Semaphore(10)

      # ... later
      async with sem:
          # work with shared resource

  ce qui équivaut à :

      sem = asyncio.Semaphore(10)

      # ... later
      await sem.acquire()
      try:
          # work with shared resource
      finally:
          sem.release()

  coroutine acquire()

    Acquiert un sémaphore.

    Si le compteur interne est supérieur à zéro, le décrémente d'une unité et renvoie `True` immédiatement. Si c'est zéro, attend que `release()` soit appelée et renvoie `True`.

  locked()

    Renvoie `True` si le sémaphore ne peut pas être acquis immédiatement.

  release()

    Relâche un sémaphore, incrémentant le compteur interne d'une unité. Peut réveiller une tâche en attente d'acquisition du sémaphore.

    Contrairement à `BoundedSemaphore`, `Semaphore` permet de faire plus d'appels `release()` que d'appels `acquire()`.

## Sémaphore capé (BoundedSemaphore)

class asyncio.BoundedSemaphore(value=1)

  Objet sémaphore capé. Non compatible avec les programmes à plusieurs fils d'exécution.

  Bounded Semaphore est une version de `Semaphore` qui lève une `ValueError` dans `release()` s'il augmente le compteur interne au-dessus de la value initiale.

  Modifié dans la version 3.10: suppression du paramètre loop.

## Barrière (Barrier)

class asyncio.Barrier(parties)

  Objet barrière. Non compatible avec les programmes à plusieurs fils d'exécution.

  Une barrière est une simple primitive de synchronisation qui permet de bloquer jusqu'à ce que parties tâches l'attendent. Les tâches attendent sur la méthode `wait()` et sont bloquées jusqu'à ce que le nombre spécifié de tâches attendent sur `wait()`. À ce stade, toutes les tâches en attente se débloquent simultanément.

  `async with` peut être utilisé comme alternative à l'attente sur `wait()`.

  La barrière peut être réutilisée un nombre illimité de fois.

  Exemple :

      async def example_barrier():
         # barrier with 3 parties
         b = asyncio.Barrier(3)

         # create 2 new waiting tasks
         asyncio.create_task(b.wait())
         asyncio.create_task(b.wait())

         await asyncio.sleep(0)
         print(b)

         # The third .wait() call passes the barrier
         await b.wait()
         print(b)
         print("barrier passed")

         await asyncio.sleep(0)
         print(b)

      asyncio.run(example_barrier())

  Le résultat de cet exemple est :

      <asyncio.locks.Barrier object at 0x... [filling, waiters:2/3]>
      <asyncio.locks.Barrier object at 0x... [draining, waiters:0/3]>
      barrier passed
      <asyncio.locks.Barrier object at 0x... [filling, waiters:0/3]>

  Nouveau dans la version 3.11.

  coroutine wait()

    Passe la barrière. Lorsque toutes les tâches bloquées à la barrière ont appelé cette fonction, elles sont toutes débloquées simultanément.

    Lorsqu'une tâche en attente ou bloquée à la barrière est annulée, cette tâche sort de la barrière qui reste dans le même état. Si la barrière est en cours de « remplissage », le nombre de tâche en attente diminue de 1.

    La valeur de retour est un entier compris entre 0 et `parties-1`, différent pour chaque tâche. Cela peut être utilisé pour sélectionner une tâche qui fera du ménage, par exemple :

        ...
        async with barrier as position:
           if position == 0:
              # Only one task prints this
              print('End of *draining phase*')

    Cette méthode peut lever une exception `BrokenBarrierError` si la barrière est brisée ou réinitialisée alors qu'une tâche est en attente. Cela peut lever une `CancelledError` si une tâche est annulée.

  coroutine reset()

    Ramène la barrière à l'état vide par défaut. Toutes les tâches en attente reçoivent l'exception `BrokenBarrierError`.

    Si une barrière est brisée, il peut être préférable de la quitter et d'en créer une nouvelle.

  coroutine abort()

    Met la barrière dans un état cassé. Cela provoque l'échec de tout appel actif ou futur à `wait()` avec une `BrokenBarrierError`. Utilisez ceci par exemple si l'une des tâches doit être abandonnée, pour éviter des tâches en attente infinie.

  parties

    Le nombre de tâches nécessaires pour franchir la barrière.

  n_waiting

    Le nombre de tâches actuellement en attente à la barrière pendant le remplissage.

  broken

    Booléen qui vaut `True` si la barrière est rompue.

exception asyncio.BrokenBarrierError

  Cette exception, une sous-classe de `RuntimeError`, est déclenchée lorsque l'objet `Barrier` est réinitialisé ou cassé.

Modifié dans la version 3.9: l'acquisition d'un verrou en utilisant `wait lock` ou `yield from lock` ou `with` (`with await lock`, `with (yield from lock)`) a été supprimée. Utilisez `async with lock` à la place.