{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Ayudantía 4: Threading 🧵\n",
    "\n",
    "## Ayudantes  👾\n",
    "- [Clemente Campos](https://github.com/mskdancers)\n",
    "- [Patricio Hinostroza](https://github.com/Dvckhv)\n",
    "- [Julio Huerta](https://github.com/julius)\n",
    "- [Carlos Olguin](https://github.com/CarlangaUC)\n",
    "- [Catalina Miranda](https://github.com/catalinamirandah)\n",
    "- [Felipe Vidal](https://github.com/fvidalf)\n",
    "\n",
    "## 📖 Contenidos 📖\n",
    "\n",
    "En esta ayudantía usaremos:\n",
    "\n",
    "- Uso de _Threads_ para manejar concurrencia\n",
    "- Manejo de _Locks_ en zonas críticas \n",
    "- Manejo de _Events_"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ¿Para qué nos sirven las _Threads_?\n",
    "Las _Threads_ nos sirven para ejecutar código concurrentemente. Si tuviéramos por ejemplo, dos trabajadores que están recolectando materiales a la vez, podríamos modelar la recolección de cada uno con un _Thread_, y así uno no tendrá que esperar al otro para terminar."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Juanito empezando a añadir oro...\n",
      "\n",
      "Natalia empezando a añadir oro...\n",
      "\n",
      "Juanito lleva 0 de oro!\n",
      "\n",
      "Natalia lleva 0 de oro!\n",
      "\n",
      "Juanito lleva 10 de oro!\n",
      "\n",
      "Natalia lleva 10 de oro!\n",
      "\n",
      "Juanito lleva 20 de oro!\n",
      "\n",
      "Natalia lleva 20 de oro!\n",
      "\n",
      "Juanito lleva 30 de oro!\n",
      "\n",
      "Natalia lleva 30 de oro!\n",
      "\n",
      "Juanito lleva 40 de oro!\n",
      "\n",
      "Natalia lleva 40 de oro!\n",
      "\n",
      "Juanito lleva 50 de oro!\n",
      "\n",
      "Natalia lleva 50 de oro!\n",
      "\n",
      "Juanito lleva 60 de oro!\n",
      "\n",
      "Natalia lleva 60 de oro!\n",
      "\n",
      "Juanito lleva 70 de oro!\n",
      "\n",
      "Natalia lleva 70 de oro!\n",
      "\n",
      "Juanito lleva 80 de oro!\n",
      "\n",
      "Natalia lleva 80 de oro!\n",
      "\n",
      "Juanito lleva 90 de oro!\n",
      "\n",
      "Natalia lleva 90 de oro!\n",
      "\n",
      "se añadió 200 al deposito\n"
     ]
    }
   ],
   "source": [
    "import threading\n",
    "import time\n",
    "\n",
    "\n",
    "class Deposito:\n",
    "    def __init__(self):\n",
    "        self.oro = 0\n",
    "\n",
    "\n",
    "def añadir_oro(Deposito, cantidad, nombre):\n",
    "    print(f\"{nombre} empezando a añadir oro...\\n\")\n",
    "    divisor = cantidad//10\n",
    "    for i in range(cantidad):\n",
    "        if not i % divisor:\n",
    "            time.sleep(1)\n",
    "            print(f\"{nombre} lleva {i} de oro!\\n\")\n",
    "        Deposito.oro += int(1)  # Este int() redundante es solo para que el ejemplo funcione\n",
    "\n",
    "class Trabajador(threading.Thread):\n",
    "    def __init__(self, deposito, cantidad, nombre):\n",
    "        super().__init__()\n",
    "        self.deposito = deposito\n",
    "        self.cantidad = cantidad\n",
    "        self.nombre = nombre\n",
    "\n",
    "    def run(self):\n",
    "        añadir_oro(self.deposito, self.cantidad, self.nombre)\n",
    "\n",
    "\n",
    "deposito = Deposito()\n",
    "t1 = Trabajador(deposito, 10**2, \"Juanito\")\n",
    "t2 = Trabajador(deposito, 10**2, \"Natalia\")\n",
    "\n",
    "t1.start()\n",
    "t2.start()\n",
    "t1.join()\n",
    "t2.join()\n",
    "\n",
    "print(\"se añadió\", deposito.oro, \"al deposito\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahora, veamos qué pasa si aumentamos la cantidad de oro de 10^2 a 10^7 🤔"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Juanito empezando a añadir oro...\n",
      "\n",
      "Natalia empezando a añadir oro...\n",
      "\n",
      "Juanito lleva 0 de oro!\n",
      "\n",
      "Natalia lleva 0 de oro!\n",
      "\n",
      "Juanito lleva 1000000 de oro!\n",
      "\n",
      "Natalia lleva 1000000 de oro!\n",
      "\n",
      "Juanito lleva 2000000 de oro!\n",
      "\n",
      "Natalia lleva 2000000 de oro!\n",
      "\n",
      "Juanito lleva 3000000 de oro!\n",
      "\n",
      "Natalia lleva 3000000 de oro!\n",
      "\n",
      "Juanito lleva 4000000 de oro!\n",
      "\n",
      "Natalia lleva 4000000 de oro!\n",
      "\n",
      "Juanito lleva 5000000 de oro!\n",
      "\n",
      "Natalia lleva 5000000 de oro!\n",
      "\n",
      "Juanito lleva 6000000 de oro!\n",
      "\n",
      "Natalia lleva 6000000 de oro!\n",
      "\n",
      "Juanito lleva 7000000 de oro!\n",
      "\n",
      "Natalia lleva 7000000 de oro!\n",
      "\n",
      "Juanito lleva 8000000 de oro!\n",
      "\n",
      "Natalia lleva 8000000 de oro!\n",
      "\n",
      "Juanito lleva 9000000 de oro!\n",
      "\n",
      "Natalia lleva 9000000 de oro!\n",
      "\n",
      "se añadió 15460814 al deposito\n"
     ]
    }
   ],
   "source": [
    "import threading\n",
    "import time\n",
    "\n",
    "\n",
    "class Deposito:\n",
    "    def __init__(self):\n",
    "        self.oro = 0\n",
    "\n",
    "\n",
    "def añadir_oro(Deposito, cantidad, nombre):\n",
    "    print(f\"{nombre} empezando a añadir oro...\\n\")\n",
    "    divisor = cantidad//10\n",
    "    for i in range(cantidad):\n",
    "        if not i % divisor:\n",
    "            time.sleep(1)\n",
    "            print(f\"{nombre} lleva {i} de oro!\\n\")\n",
    "        Deposito.oro += int(1)  # Este int() redundante es solo para que el ejemplo funcione\n",
    "\n",
    "class Trabajador(threading.Thread):\n",
    "    def __init__(self, deposito, cantidad, nombre):\n",
    "        super().__init__()\n",
    "        self.deposito = deposito\n",
    "        self.cantidad = cantidad\n",
    "        self.nombre = nombre\n",
    "\n",
    "    def run(self):\n",
    "        añadir_oro(self.deposito, self.cantidad, self.nombre)\n",
    "\n",
    "\n",
    "deposito = Deposito()\n",
    "t1 = Trabajador(deposito, 10**7, \"Juanito\")\n",
    "t2 = Trabajador(deposito, 10**7, \"Natalia\")\n",
    "\n",
    "t1.start()\n",
    "t2.start()\n",
    "t1.join()\n",
    "t2.join()\n",
    "\n",
    "print(\"se añadió\", deposito.oro, \"al deposito\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esto sucede ya que las threads acceden al valor al mismo tiempo, y para resolverlo, debemos recordar el uso de _Locks_ en zonas críticas:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Juanito empezando a añadir oro...\n",
      "\n",
      "Natalia empezando a añadir oro...\n",
      "\n",
      "Juanito lleva 0 de oro!\n",
      "\n",
      "Natalia lleva 0 de oro!\n",
      "\n",
      "Juanito lleva 1000000 de oro!\n",
      "\n",
      "Natalia lleva 1000000 de oro!\n",
      "\n",
      "Juanito lleva 2000000 de oro!\n",
      "\n",
      "Natalia lleva 2000000 de oro!\n",
      "\n",
      "Juanito lleva 3000000 de oro!\n",
      "\n",
      "Natalia lleva 3000000 de oro!\n",
      "\n",
      "Juanito lleva 4000000 de oro!\n",
      "\n",
      "Natalia lleva 4000000 de oro!\n",
      "\n",
      "Juanito lleva 5000000 de oro!\n",
      "\n",
      "Natalia lleva 5000000 de oro!\n",
      "\n",
      "Juanito lleva 6000000 de oro!\n",
      "\n",
      "Natalia lleva 6000000 de oro!\n",
      "\n",
      "Natalia lleva 7000000 de oro!\n",
      "\n",
      "Juanito lleva 7000000 de oro!\n",
      "\n",
      "Natalia lleva 8000000 de oro!\n",
      "\n",
      "Juanito lleva 8000000 de oro!\n",
      "\n",
      "Natalia lleva 9000000 de oro!\n",
      "\n",
      "Juanito lleva 9000000 de oro!\n",
      "\n",
      "se añadió 20000000 al deposito\n"
     ]
    }
   ],
   "source": [
    "import threading\n",
    "import time\n",
    "\n",
    "\n",
    "class Deposito:\n",
    "    def __init__(self):\n",
    "        self.oro = 0\n",
    "\n",
    "\n",
    "def añadir_oro(Deposito, cantidad, nombre, lock_oro):\n",
    "    print(f\"{nombre} empezando a añadir oro...\\n\")\n",
    "    divisor = cantidad//10\n",
    "    for i in range(cantidad):\n",
    "        if not i % divisor:\n",
    "            time.sleep(1)\n",
    "            print(f\"{nombre} lleva {i} de oro!\\n\")\n",
    "        with lock_oro:\n",
    "            Deposito.oro += int(1)  # Este int() redundante es solo para que el ejemplo funcione\n",
    "\n",
    "class Trabajador(threading.Thread):\n",
    "    lock_oro = threading.Lock()\n",
    "\n",
    "    def __init__(self, deposito, cantidad, nombre):\n",
    "        super().__init__()\n",
    "        self.deposito = deposito\n",
    "        self.cantidad = cantidad\n",
    "        self.nombre = nombre\n",
    "\n",
    "    def run(self):\n",
    "        añadir_oro(self.deposito, self.cantidad, self.nombre, self.lock_oro)\n",
    "\n",
    "\n",
    "deposito = Deposito()\n",
    "t1 = Trabajador(deposito, 10**7, \"Juanito\")\n",
    "t2 = Trabajador(deposito, 10**7, \"Natalia\")\n",
    "\n",
    "t1.start()\n",
    "t2.start()\n",
    "t1.join()\n",
    "t2.join()\n",
    "\n",
    "print(\"se añadió\", deposito.oro, \"al deposito\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## DCChef!\n",
    "Cansados de las filas para usar el microondas y del precio de la comida dentro de la universidad, el DCC decidió crear su propio restaurante, y te han designado a ti como el gerente! Decides modelar el funcionamiento de tu programa con lo que has aprendido en el curso de Programación Avanzada. Todo parece ir bien hasta que te das cuenta que se ha formado una colosal fila para entrar, ya que tu programa no permite que los meseros tomen los pedidos mientras los cocineros están cocinando! ¿Cómo podríamos hacer que estas acciones se ejecuten a la vez? ¿Es esto el fin para el DCChef? \n",
    "\n",
    "### Threads!\n",
    "Para lograr esto, en el archivo --main.py-- se van a cargar los archivos de datos y se van a instanciar las clases ```Cocina```, ```Mesero``` y ```Cocinero```. Luego se va a iniciar el thread inicial de la clase ```Cocina```, que a su vez inicia los threads de múltiples instancias de ```Mesero``` y ```Cocinero```. Así, los meseros van a ir tomando pedidos y agregándolos a la cola de pedidos, para que un cocinero los cocine. Cuando el plato esté listo, un mesero lo llevará a la mesa que corresponde y lo agregará a la cola de pedidos listos. El código base ya está implementado, pero tendrás que completar los siguientes archivos:\n",
    "\n",
    "### entidades.py\n",
    "\n",
    "- ```Persona```: Clase abstracta que representa a las personas que trabajan en el restaurante. Debe tener los siguientes Locks como atributos de clase: ```lock_bodega```, ```lock_cola_pedidos```, ```lock_cola_pedidos_listos```.\n",
    "\n",
    "- ```Cocinero.__init__```: Deberás definir el atributo ```evento_plato_asignado``` como un ```Event```.\n",
    "\n",
    "- ```Cocinero.run```: En este método debes implementar que mientras el cocinero esté trabajando, espere a que se le asigne un plato (a través del evento definido anteriormente). Cuando se le sea asignado el plato, hará los preparativos para empezar a cocinar, que debe ser simulado esperando un tiempo aleatorio entre 1 y 3 segundos*. Finalmente, se debe llamar al método ```cocinar```.\n",
    "\n",
    "\\* Para esto se puede utilizar el método ```sleep``` de la librería ```time```.\n",
    "\n",
    "- ```Cocinero.cocinar```: Primero, el valor del atributo ```disponible``` del cocinero debe cambiar a ```False```, y luego deberá sacar un plato de la cola de pedidos con el método ```sacar_plato```. Después de eso deberás imprimir un mensaje de la forma ```'Cocinero {nombre_cocinero} cocinando {nombre_plato}'```, buscar los ingredientes para el plato con el método ```buscar_ingredientes``` y simular el tiempo de cocina de la misma manera que en el método anterior. Cuando el plato esté listo, se debe agregar a la cola de peedidos listos con el método ```agregar_plato```, para finalmente resetear el evento ```evento_plato_asignado``` y cambiar ```disponible``` a ```True```.\n",
    "\n",
    "- ```Cocinero.sacar_plato```: Debes sacar el primer plato de la cola de pedidos de la cocina y luego retornarlo. Cada plato es una tupla donde el primer elemento es la mesa que pidió el plato y el segundo es el nombre del plato. **Debes asegurarte que sólo un cocinero a la vez pueda sacar un plato de la cola de pedidos**.\n",
    "\n",
    "- ```Cocinero.buscar_ingredientes```: Recibe un plato, y dos diccionarios, uno de recetas y otro representando la bodega. En el diccionario de recetas, la llave es el nombre de un plato y el valor es una lista de tuplas donde cada una contiene el nombre del ingrediente y la cantidad necesaria para el plato. El diccionario de bodega lleva como llaves los nombres de los ingredientes y como valor asociado la cantidad de ese ingrediente que se encuentra en la bodega. Se deberá imprimir un mensaje con el formato ```'Cocinero {nombre} buscando los ingredientes en la bodega...'```, para luego disminuir la cantidad de ingredientes en la bodega según lo que se sacó (puedes asumir que siempre habrán suficientes). **Tienes que asegurarte que sólo un cocinero pueda acceder a la bodega al mismo tiempo**.\n",
    "\n",
    "- ```Cocinero.agregar_plato```: Recibe como argumento un plato y lo agrega al final de la cola de pedidos listos. **Debes asegurarte que sólo un cocinero a la vez pueda agregar un plato a la cola de pedidos listos**.\n",
    "\n",
    "- ```Mesero.__init__```: Deberás definir el atributo ```evento_manejar_pedido``` como un ```Event```.\n",
    "\n",
    "- ```Mesero.run```: En este método debes implementar que mientras el mesero esté trabajando, si es que está disponible, deberás activar el evento ```evento_manejar_pedido```.\n",
    "\n",
    "- ```Mesero.agregar_pedido```: Este método se debe encargar de agregar un pedido a la cola de pedidos. Primero, deberás resetear el evento ```evento_manejar_pedido```, luego esperar un tiempo aleatorio entre 1 y 2 segundos, para después agregar el pedido a la cola y volver a activar el evento ```evento_manejar_pedido```. **Debes asegurarte que sólo un mesero a la vez pueda acceder a la cola de pedidos**.\n",
    "\n",
    "- ```Mesero.entregar_pedido```: Este método debe simular la entrega de los pedidos a las mesas. Primero, deberás resetear el evento ```evento_manejar_pedido```, simular la entrega del plato esperando un tiempo aleatorio entre 1 y 3 segundos, para luego ejecutar el método ```pedido_entregado```, donde el pedido a entregar es el primer pedido de la cola de pedidos listos de la cocina. Finalmente deberás imprimir un mensaje con el formato ```'Mesero {nombre} entregando pedido a la mesa {numero_mesa}...'```.\n",
    "\n",
    "- ```Mesero.pedido_entregado```: Debes imprimir un mensaje del formato ```'El plato {nombre_plato} de la mesa {numero_mesa} fue entregado'```, y activar el evento ```evento_manejar_pedido```. \n",
    "\n",
    "### cocina.py\n",
    "\n",
    "- ```Cocina.iniciar_threads```: Debes encargarte de iniciar todos los threads de los meseros y los cocineros.\n",
    "\n",
    "- ```Cocina.asignar_cocinero```: Este método asignará los platos a los cocineros. Mientras la cocina esté abierta, deberás esperar 1 segundo y verificar si hay pedidos en la cola de pedidos, y en caso de que hayan, buscar un cocinero disponible y activar su evento ```evento_plato_asignado```.\n",
    "\n",
    "- ```Cocina.asignar_mesero```: Este método asigna los pedidos a los meseros. Mientras la cocina esté abierta, deberás esperar 1 segundo y verificar si hay pedidos en la cola de pedidos listos, y en caso de que hayan, buscar un mesero disponible y activar su evento ```evento_manejar_pedido```. Luego debes llamar al método ```entregar_pedido``` del mesero con los argumentos respectivos. Finalmente, cuando la cocina cierre debes llamar al método ```finalizar_jornada_laboral```.\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.12"
  },
  "orig_nbformat": 4
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
