Skip to content

Instantly share code, notes, and snippets.

@PiDroid-B
Created December 10, 2019 10:02
Show Gist options
  • Save PiDroid-B/325f59b5cf5698eea8d84818b071f950 to your computer and use it in GitHub Desktop.
Save PiDroid-B/325f59b5cf5698eea8d84818b071f950 to your computer and use it in GitHub Desktop.
FastFood

Fast food :

Issu de : ZestDeSavoir.com

On ne va pas attendre que la première commande soit servie pour prendre la deuxième

Pour chaque commande d'un menu :

burger :

Il n'y a que 3 cuisiniers.
On ne peut pas faire plus de 3 burgers en même temps

Le async with BURGER_SEM veut dire que lorsqu'une commande est passée en cuisine :

  • soit il y a un cuisinier libre, et celui-ci commence immédiatement à préparer le hamburger,
  • soit tous les cuisiniers sont occupés, auquel cas on attend qu'il y en ait un qui se libère pour s'occuper de notre hamburger.
BURGER_SEM = asyncio.Semaphore(3)

async def get_burger(client):
    print("    > Commande du burger en cuisine pour {}".format(client))
    async with BURGER_SEM:
        await asyncio.sleep(3)
        print("    < Le burger de {} est prêt".format(client))

soda :

Il n'y a qu'une machine à soda.
On ne peut faire qu'un soda à la fois

Le async with SODA_LOCK signifie que lorsque le serveur arrive à la machine à soda pour y déposer un gobelet :

  • soit la machine est libre (déverrouillée), auquel cas il peut la verrouiller pour l'utiliser immédiatement,
  • soit celle-ci est déjà en train de fonctionner, auquel cas il attend (de façon asynchrone, donc en rendant la main) que le soda en cours de préparation soit prêt avant de verrouiller la machine à son tour.
SODA_LOCK = asyncio.Lock()

async def get_soda(client):
    # Acquisition du verrou
    # la syntaxe 'async with FOO' peut être lue comme 'with (yield from FOO)'
    async with SODA_LOCK:
        # Une seule tâche à la fois peut exécuter ce bloc
        print("    > Remplissage du soda pour {}".format(client))
        await asyncio.sleep(1)
        print("    < Le soda de {} est prêt".format(client))

frites :

Le bac à frites permet de cuire 5 portions d'un coup. Il est nécessaire d'attendre pour chaque cuisson tous les 5 portions

Passons enfin au bac à frites. Cette fois, asyncio ne nous fournira pas d'objet magique, donc il va nous falloir réfléchir un peu plus. Il faut que l'on puisse l'utiliser une fois pour faire les frites des 5 prochaines commandes. Dans ce cas, un compteur semble une bonne idée :

  • Chaque fois que l'on prend une portion de frites, on décrémente le compteur ;
  • S'il n'y a plus de frites dans le bac, il faut en refaire.
FRIES_COUNTER = 0
FRIES_LOCK = asyncio.Lock()

async def get_fries(client):
    global FRIES_COUNTER
    async with FRIES_LOCK:
        print("    > Récupération des frites pour {}".format(client))
        if FRIES_COUNTER == 0:
            print("   ** Démarrage de la cuisson des frites")
            await asyncio.sleep(4)
            FRIES_COUNTER = 5
            print("   ** Les frites sont cuites")
        FRIES_COUNTER -= 1
        print("    < Les frites de {} sont prêtes".format(client))

exemple de sortie :

>>> loop.run_until_complete(asyncio.wait([serve('A'), serve('B')]))
=> Commande passée par B
=> Commande passée par A
    > Remplissage du soda pour B
    > Récupération des frites pour B
   ** Démarrage de la cuisson des frites
    > Commande du burger en cuisine pour B
    > Commande du burger en cuisine pour A
    < Le soda de B est prêt
    > Remplissage du soda pour A
    < Le soda de A est prêt
    < Le burger de B est prêt
    < Le burger de A est prêt
   ** Les frites sont cuites
    < Les frites de B sont prêtes
    > Récupération des frites pour A
    < Les frites de A sont prêtes
<= B servi en 0:00:04.003111
<= A servi en 0:00:04.003093

Version refaite :

exemple de sortie :



délai d'arriver des clients en seconde : 
		 [0.7, 2.8, 5.0]

              (B)urger  | (F)rites | (S)oda
              | 1 | 2 | 3 |
              |BFS|BFS|BFS|
              =============
  0s + 000ms  |   |   |   |
  0s + 702ms => Commande passée par 0
  0s + 702ms  |  +|   |   |
  0s + 702ms  |+ o|   |   |** Préparation du soda du client 0
  0s + 702ms  |o+o|   |   |** Préparation du burger du client 0 - 2 serveur(s) disponible(s)
  1s + 703ms  |oo-|   |   |** Démarrage de la cuisson des frites
  2s + 802ms => Commande passée par 1
  2s + 802ms  |oo |+  |   |
  2s + 802ms  |oo |o+ |   |** Préparation du burger du client 1 - 1 serveur(s) disponible(s)
  2s + 802ms  |oo |oo+|   |
  3s + 703ms  |-o |ooo|   |** Préparation du soda du client 1
  3s + 803ms  | o |oo-|   |
  4s + 704ms  | - |oo |   |** les frites du client 0 - 4 portions restantes
  4s + 704ms  |   |o- |   |** les frites du client 1 - 3 portions restantes
  4s + 704ms <= 0 servi en 0:00:04.002815
  5s + 001ms => Commande passée par 2
  5s + 001ms  |   |o  |+  |
  5s + 001ms  |   |o  |o +|** Préparation du burger du client 2 - 1 serveur(s) disponible(s)
  5s + 001ms  |   |o  |o+o|** Préparation du soda du client 2
  5s + 001ms  |   |o  |o-o|** les frites du client 2 - 2 portions restantes
  5s + 804ms  |   |-  |o o|
  5s + 804ms <= 1 servi en 0:00:03.001828
  6s + 002ms  |   |   |o -|
  8s + 002ms  |   |   |-  |
  8s + 002ms <= 2 servi en 0:00:03.000853
Plus personne ? on ferme !

Process finished with exit code 0
import asyncio
import random
from datetime import datetime
from enum import Enum
from dataclasses import dataclass
class CMD_STATE(Enum):
Undefined = 0
Ask = 1
In_Progress = 2
Get = 3
@dataclass
class CommandClient:
""" Une commande pour un client contient :
- un burger
- des frites
- un soda
"""
burger: CMD_STATE
frites: CMD_STATE
soda: CMD_STATE
@staticmethod
def _format(value):
"""Renvoi une représentation textuel de l'état d'une sous-commande (burger, frites ou soda)"""
if value == CMD_STATE.Ask:
return "+"
elif value == CMD_STATE.In_Progress:
return "o"
elif value == CMD_STATE.Get:
return "-"
else:
return " "
def __init__(self, burger=CMD_STATE.Undefined, frites=CMD_STATE.Undefined, soda=CMD_STATE.Undefined):
"""Constructeur"""
super().__init__()
self.burger = burger
self.frites = frites
self.soda = soda
def __repr__(self):
"""Renvoi une représentation textuel de l'état d'une commande au format 'BFS' (burger, frites, soda)"""
return CommandClient._format(self.burger) + \
CommandClient._format(self.frites) + \
CommandClient._format(self.soda)
def do_release_finished(self):
"""Clôture les sous-commandes terminée"""
def do_update(param):
if param == CMD_STATE.Get:
return CMD_STATE.Undefined
return param
self.burger = do_update(self.burger)
self.frites = do_update(self.frites)
self.soda = do_update(self.soda)
class FastFood:
"""Classe gérant la préparation de commande du fastfood
Attributes:
_soda_lock : mutex, only one soda machine
_burger_semaphore : semaphore = 3, only 3 servers
_fries_counter : remaining portions
_fries_lock : mutex, only one deep fryer
_last_comment : last message
"""
_soda_lock = asyncio.Lock()
_burger_semaphore = asyncio.Semaphore(3)
_fries_counter = 0
_fries_lock = asyncio.Lock()
_last_comment = ""
def __init__(self, name, nb_commande):
"""Constructeur
- name : nom du fastfood
- nb_commande : nombre de commande total
"""
# Nom du fastfood
self.name = name
# lancement de la boucle par défaut
self.loop = asyncio.new_event_loop()
# initialisation du tableau des commandes "vides"
self.cmd_clients = [CommandClient()] * nb_commande
# lancement du timer
self.start_timer = datetime.now()
# affichage de l'entete de sortie
self._print_line(True)
def _print_line(self, is_header=False):
"""
Affiche le tableau de résultat ligne par ligne
Si is_header alors affiche la légende et l'entête
"""
# affichage de l'entete de sortie
if is_header:
# nombre de client
nb_clients = len(self.cmd_clients)
# taille d'une ligne (hors préfixe d'horodatage)
raw_len = nb_clients * 4 + 1
# espace qui sera utilisé pour afficher les temps d'exécution
prefix_time = "\n" + " " * 14
# legende
legende = "(B)urger | (F)rites | (S)oda"
# header 1 : colonnes de 3 chr : id par client
hd1 = "|" + "|".join(map(str, [f"{x:^3}" for x in range(1, nb_clients + 1)])) + "|"
# header 2 : sous-colonnes Burger/Frites/Soda par client
hd2 = "|BFS" * nb_clients + "|"
# header 3 : ligne vide pour timer
hd3 = " |" + "|".join(map(str, [f" "] * nb_clients)) + "|"
# concat header
raw = prefix_time + f"{legende:^{raw_len}}" + \
prefix_time + hd1 + \
prefix_time + hd2 + \
prefix_time + "=" * raw_len + \
"\n" + self._get_tick() + hd3
else:
# Etat de la commande pour chaque client
# en colonnes : (B)urger | (F)rites | (S)oda
try :
raw = self._get_tick() + " |" + "|".join(map(str, self.cmd_clients)) + '|' + self._last_comment
self._last_comment = ""
except Exception as e:
print(type(e), e)
exit(1)
print(raw)
def _get_tick(self):
"""
Renvoi la période écoulé depuis l'ouverture (string formaté)
"""
duration = datetime.now() - self.start_timer
return f"{duration.seconds:>3}s + {duration.microseconds // 1000:03d}ms"
async def _client_change_state(self, client, burger=CMD_STATE.Undefined, frites=CMD_STATE.Undefined,
soda=CMD_STATE.Undefined, notify=True):
"""
Change l'état de la commande d'un client dans le tableau cmd_clients
Pour chaque paramètre (burger, frites, soda) de chaque client
Voir CMD_STATE
Undefined = 0 : pas de commande en cours
Ask = 1 : commande initiée
In_Progress = 2 : commande en cours de préparation
Get = 3 : commande terminée
notify
On affiche le changement si True
ALGO :
1 - Pour chaque changement d'état et pour l'ensemble des clients :
les valeurs Get(3) passent à Undefined(0)
2 - Pour client en param, pour chaque argument (burger, frites, soda) :
si argument != Undefined(0)
alors affectation de l'argument
3 - on affiche la ligne complète
"""
# 1 on remet les états à jour
for clt in self.cmd_clients:
clt.do_release_finished()
# 2 on modifie l'état de la commande du client passé en paramètre
burger = burger if burger != CMD_STATE.Undefined else self.cmd_clients[client].burger
frites = frites if frites != CMD_STATE.Undefined else self.cmd_clients[client].frites
soda = soda if soda != CMD_STATE.Undefined else self.cmd_clients[client].soda
self.cmd_clients[client] = CommandClient(burger, frites, soda)
# on affiche la ligne
if notify :
self._print_line()
async def _get_soda(self, client):
"""
Tâche _get_soda : préparation d'un soda pour un client
Contexte : Une seule machine à soda, on ne peut en faire qu'un à la fois
"""
# Acquisition du verrou
# la syntaxe 'async with FOO' peut être lue comme 'with (yield from FOO)'
# print(" > Commande du soda pour {}".format(client))
await self._client_change_state(client, soda=CMD_STATE.Ask)
async with self._soda_lock:
# Une seule tâche à la fois peut exécuter ce bloc
self._last_comment = f"** Préparation du soda du client {client}"
await self._client_change_state(client, soda=CMD_STATE.In_Progress, notify=False)
# print(" X Remplissage du soda pour {}".format(client))
await asyncio.sleep(1)
await self._client_change_state(client, soda=CMD_STATE.Get)
# print(" < Le soda de {} est prêt".format(client))
async def _get_burger(self, client):
"""
Tâche _get_burger : préparation d'un burger
Contexte : 3 serveurs, on ne peut faire qu'un burger par serveur
"""
await self._client_change_state(client, burger=CMD_STATE.Ask)
# print(" > Commande du burger en cuisine pour {}".format(client))
async with self._burger_semaphore:
# accès à _burger_semaphore._value comme un malpropre (valeur non accessible autrement)
self._last_comment = f"** Préparation du burger du client {client} - " \
f"{self._burger_semaphore._value} serveur(s) disponible(s)"
await self._client_change_state(client, burger=CMD_STATE.In_Progress, notify=False)
# print(" X Préparation du burger pour {}".format(client))
await asyncio.sleep(3)
await self._client_change_state(client, burger=CMD_STATE.Get)
# print(" < Le burger de {} est prêt".format(client))
async def _get_fries(self, client):
"""
Tâche _get_fries : préparation des frites
Contexte :
- le bac à frite contient 5 portions
- une fois vide, la cuisson d'un nouveau bac prend 4 secondes
"""
await self._client_change_state(client, frites=CMD_STATE.Ask)
# print(" > Commande des frites pour {}".format(client))
await self._client_change_state(client, frites=CMD_STATE.In_Progress, notify=False)
async with self._fries_lock:
if self._fries_counter == 0:
self._last_comment = "** Démarrage de la cuisson des frites"
# print(f"{self._get_tick()} ** Démarrage de la cuisson des frites")
await asyncio.sleep(4)
self._fries_counter = 5
# print(f"{self._get_tick()} ** Les frites sont cuites")
self._last_comment = "** Les frites sont cuites"
self._fries_counter -= 1
self._last_comment = f"** les frites du client {client} - " \
f"{self._fries_counter} portions restantes"
await self._client_change_state(client, frites=CMD_STATE.Get)
# print(" < Les frites de {} sont prêtes".format(client))
async def nv_commande(self, client, delay):
"""
Tâche nv_commande : nouvelle commande passée
Elle va initié les sous-commandes qui en découlent (burger, frites, soda)
"""
try :
if not isinstance(client,int) :
raise TypeError("Client doit être de type 'int'")
if client > len(self.cmd_clients)-1 :
raise Exception("Client doit correspondre à l'ordre d'arrivée (index dans la file)")
if not isinstance(delay,(int,float)) :
raise TypeError("Delay represente le délai entre l'ouverture et l'arrivée du client, "
"doit être de type 'int' ou 'float'")
except Exception as e:
print("\n", type(e))
print("\n", e)
print("\n\nSortie en erreur")
exit(1)
await asyncio.sleep(delay)
start_time = datetime.now()
print( self._get_tick(), "=> Commande passée par {}".format(client))
await asyncio.wait(
[
self._get_soda(client),
self._get_fries(client),
self._get_burger(client)
]
)
total = datetime.now() - start_time
print(self._get_tick(), "<= {} servi en {}".format(client, datetime.now() - start_time))
return total
# ### ancienne méthode ###
# async def on_ferme(self):
# """Tâche on_ferme : plus de commande, on ferme le fastfood"""
# while True:
# await asyncio.sleep(1)
# if len(asyncio.Task.all_tasks()) == 1:
# print("Plus personne, on ferme !")
# asyncio.get_event_loop().stop()
def main():
random.seed()
# nombre de commande à réaliser
nb_cmd = 3
# simulation des arrivées clients (délai aléatoire pour chacun d'entre eux)
delay = [random.randint(1, 50) / 10 for x in range(nb_cmd)]
delay.sort()
print("\n\ndélai d'arriver des clients en seconde : \n\t\t", delay)
# initialisation de notre objet coroutine
ff = FastFood("BestOf", nb_cmd)
# on passe les commandes (création des tâches dans la boucle d'événement)
tasks = [ff.nv_commande(num_commande, delay[num_commande]) for num_commande in range(nb_cmd)]
# ### ancienne méthode ###
# for num_commande in range(nb_cmd):
# asyncio.ensure_future(ff.nv_commande(num_commande, delay[num_commande]))
# on passe la tâche en charge de la fermeture (si pluys de commande)
# asyncio.ensure_future(ff.on_ferme())
try:
# on lance la boucle
# ### ancienne méthode ###
# asyncio.get_event_loop().run_forever()
asyncio.get_event_loop().run_until_complete(asyncio.gather(*tasks))
print("Plus personne ? on ferme !")
except KeyboardInterrupt as e:
print("Barrez-vous ! c'est fermé et tant pis pour vos comandes !")
except Exception as e:
print("OUPS", type(e), e)
if __name__ == "__main__":
main()
@JulienPalard
Copy link

def _format(value):: Je pense que tu peux carrément te séparer de cette fonction si tu mets les caractères (o, +, ...) dans l'enum directement : https://docs.python.org/3/library/enum.html#using-a-descriptive-string

@JulienPalard
Copy link

self.loop = asyncio.new_event_loop() : Ne crée pas ta propre boucle asyncio car une seule peut tourner en même temps, si tu crée la tienne, et que tu veux faire tourner ta lib dans un programme qui a deja sa boucle, tu ne sera pas compatible. Prend juste la boucle actuelle.

@JulienPalard
Copy link

hd1 = "|" + "|".join(map(str, [f"{x:^3}" for x in range(1, nb_clients + 1)])) + "|" : Utilise plutot la lib tabulate.

@JulienPalard
Copy link

JulienPalard commented Dec 10, 2019

Pour stocker le verrou de la fritteuse, sa capacité, et le nombre de portions qui sont dedans, tu devrais faire une class Fryer.

pouvoir faire :

fryer = Fryer(capacity=5, cooking_time=4)
portion = await fryer.get_portion()

@JulienPalard
Copy link

exit(1) la builtin exit c'est surtout pour le repr, préfère sys.exit(1) dans une lib.

@JulienPalard
Copy link

self.cmd_clients = [CommandClient()] * nb_commande : Ici je pense que tu crée nb_commande fois le même client, ce qui me paraît être un bug, j'imagine que tu veux nb_commande clients différents. fais un print(self.cmd_clients) juste après tu verras qu'ils ont tous la même adresse mémoire, ils partagent donc leur attributs d'instance (il n'y a qu'une instance).

@PiDroid-B
Copy link
Author

J'ai migré l'ensemble vers un repository (plus facile de gérer sous forme d'issues)
https://github.com/PiDroid-B/example-python-fastfood

J'essaie de répondre à l'ensemble en fin de journée

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment