{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<p>\n",
    "<font size='5' face='Georgia, Arial'>IIC2233 Apunte Programación Avanzada</font><br>\n",
    "<font size='1'>&copy; 2015 Karim Pichara - Christian Pieringer. Todos los derechos reservados.</font>\n",
    "<br>\n",
    "<font size='1'> Modificado desde 2017-2 al 2026-1 por Equipo Docente IIC2233</font>\n",
    "</p>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Tabla de contenidos\n",
    "\n",
    "1. [*Lock* como atributo de una subclase de `Thread`](#Lock-como-atributo-de-una-subclase-de-Thread)\n",
    "2. [Patrón productor-consumidor](#Patrón-productor-consumidor)\n",
    "    1. [`Queue`](#Queue)\n",
    "3. [Simulaciones](#Simulaciones)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## *Lock* como atributo de una subclase de `Thread`\n",
    "\n",
    "Como recordarás, una manera de crear nuestros *threads* consiste en crear una clase que herede de `Thread` y sobreescribir el método `run` e `__init__`. Podemos aprovechar de colocar los *locks* que necesitemos **como atributo de clase**. De esta manera, tendremos acceso a un mismo *lock* para todos los *threads* de nuestra clase y organizaremos mejor nuestro código.\n",
    "\n",
    "#### Paréntesis: atributos de clase y atributos de instancia\n",
    "\n",
    "Al usar clases, normalmente definimos atributos para las instancias mediante el uso de `self.atributo = valor`. Esto genera la creación de un **atributo de instancia**, cuyo valor es accesible desde cualquier punto de la instancia mediante `self`. Si definimos un atributo al nivel de los métodos, se le conoce como **atributo de clase** y es accesible por todas las instancias mediante el nombre de la clase: `Clase.atributo`. También, es posible acceder mediante `self` en una instancia, siempre y cuando no tenga un atributo de instancia del mismo nombre definido. Cuando se busca un valor de atributo mediante `self.atributo`, el orden de búsqueda es:\n",
    "1. Buscar en la instancia. (atributo de instancia)\n",
    "2. Si no existe, buscar en la clase. (atributo de clase)\n",
    "3. Si no existe, error (`AttributeError`).\n",
    "\n",
    "### Ejemplo\n",
    "\n",
    "En el siguiente ejemplo, escribiremos en un mismo archivo `txt` desde varios *threads*."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[EscritorArchivo número 1] ¡Comenzó a trabajar![EscritorArchivo número 2] ¡Comenzó a trabajar!\n",
      "[EscritorArchivo número 2] ¡escribió una línea!\n",
      "\n",
      "[EscritorArchivo número 1] ¡escribió una línea!\n",
      "[EscritorArchivo número 3] ¡Comenzó a trabajar!\n",
      "[EscritorArchivo número 3] ¡escribió una línea!\n",
      "[EscritorArchivo número 4] ¡Comenzó a trabajar!\n",
      "[EscritorArchivo número 4] ¡escribió una línea!\n",
      "[EscritorArchivo número 5] ¡Comenzó a trabajar!\n",
      "[EscritorArchivo número 5] ¡escribió una línea!\n",
      "[EscritorArchivo número 6] ¡Comenzó a trabajar!\n",
      "[EscritorArchivo número 6] ¡escribió una línea!\n",
      "[EscritorArchivo número 7] ¡Comenzó a trabajar!\n",
      "[EscritorArchivo número 7] ¡escribió una línea!\n",
      "[EscritorArchivo número 6] ¡escribió una línea!\n",
      "[EscritorArchivo número 7] ¡escribió una línea!\n",
      "[EscritorArchivo número 3] ¡escribió una línea!\n",
      "[EscritorArchivo número 2] ¡escribió una línea!\n",
      "[EscritorArchivo número 7] ¡escribió una línea!\n",
      "[EscritorArchivo número 4] ¡escribió una línea!\n",
      "[EscritorArchivo número 5] ¡escribió una línea!\n",
      "[EscritorArchivo número 6] ¡escribió una línea!\n",
      "[EscritorArchivo número 3] ¡escribió una línea!\n",
      "[EscritorArchivo número 4] ¡escribió una línea!\n",
      "[EscritorArchivo número 4] ¡escribió una línea!\n",
      "[EscritorArchivo número 7] ¡escribió una línea!\n",
      "[EscritorArchivo número 5] ¡escribió una línea!\n",
      "[EscritorArchivo número 7] ¡escribió una línea!\n",
      "[EscritorArchivo número 5] ¡escribió una línea!\n",
      "[EscritorArchivo número 6] ¡escribió una línea!\n",
      "[EscritorArchivo número 7] ¡escribió una línea!\n",
      "[EscritorArchivo número 6] ¡escribió una línea!\n",
      "[EscritorArchivo número 5] ¡escribió una línea!\n",
      "[EscritorArchivo número 6] ¡escribió una línea!\n",
      "[EscritorArchivo número 7] ¡escribió una línea!\n",
      "Todos los threads terminaron. Revise el resultado en files/salida.txt\n"
     ]
    }
   ],
   "source": [
    "import threading\n",
    "import time\n",
    "import os\n",
    "from random import random\n",
    "import typing\n",
    "\n",
    "\n",
    "class EscritorArchivo(threading.Thread):\n",
    "    \"\"\"\n",
    "    Esta clase modela un thread. Dentro creamos un objeto para bloqueo dentro de la clase. \n",
    "    El Lock es una variable independiente de cada thread y es común para todas las instancias.\n",
    "    \"\"\"\n",
    "\n",
    "    # Atributo de clase\n",
    "    # Accesible mediante Clase.lock o self.lock desde una instancia\n",
    "    lock = threading.Lock()\n",
    "\n",
    "    def __init__(self, numero: int, archivo: typing.TextIO) -> None:\n",
    "        # Mandamos un nombre a la clase padre con el atributo name\n",
    "        super().__init__(name=f\"EscritorArchivo número {numero}\")\n",
    "        self.numero = numero\n",
    "        self.archivo = archivo\n",
    "\n",
    "    def run(self) -> None:\n",
    "        print(f\"[{self.name}] ¡Comenzó a trabajar!\")\n",
    "        for _ in range(self.numero):\n",
    "            with self.lock:  # Acceso al lock\n",
    "                self.archivo.write(f\"Línea escrita por # {self.name}\\n\")\n",
    "                print(f\"[{self.name}] ¡escribió una línea!\")\n",
    "            # Hacemos que se demore una cantidad random uniforme [0, 1)\n",
    "            time.sleep(random())\n",
    "\n",
    "\n",
    "# Creamos un archivo para escribir una salida\n",
    "# Luego creamos los threads que escribirán dentro del archivo\n",
    "with open(os.path.join(\"files\", \"salida.txt\"), \"w\", encoding=\"utf-8\") as archivo:\n",
    "    # Creamos los threads\n",
    "    cantidad_threads = 7\n",
    "\n",
    "    threads = []\n",
    "    for i in range(1, cantidad_threads + 1):\n",
    "        threads.append(EscritorArchivo(i, archivo))\n",
    "\n",
    "    # Hacemos partir los threads\n",
    "    for thread in threads:\n",
    "        thread.start()\n",
    "\n",
    "    # Esperamos a todos los threads antes de cerrar el archivo\n",
    "    for thread in threads:\n",
    "        thread.join()\n",
    "\n",
    "    print(\"Todos los threads terminaron. Revise el resultado en files/salida.txt\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Patrón productor-consumidor\n",
    "\n",
    "Un problema común en programación concurrente es el patrón **productor-consumidor**. Este se origina cuando dos o más *threads*, conocidos como **productores** y **consumidores**, acceden a un mismo espacio de almacenamiento o ***buffer***.\n",
    "\n",
    "Bajo este esquema, los productores ponen ítems en el *buffer* y los consumidores sacan elementos del *buffer*. Este modelo permite la comunicación entre distintos *threads*. Por lo general el *buffer* compartido en este modelo se implementa mediante una **cola sincronizada** o una **cola _thread-safe_**, que funciona correctamente al ser usada por múltiples *threads*.\n",
    "\n",
    "Si bien los `deque` permiten agregar y sacar elementos desde ambos extremos en forma segura con *threads*, **nada nos asegura** que si vimos que había un objeto para sacar, ese objeto todavía esté cuando queramos sacarlo. Por lo tanto, tenemos que asegurarnos nosotros mismos – vía *locks* – de que revisar si había algo y sacarlo sea una operación atómica.\n",
    "\n",
    "Por ejemplo, supongamos que el productor es un panadero, y los consumidores son clientes de un supermercado. El panadero colocará piezas de pan cuando las tenga listas, y los consumidores sacarán estas piezas cuando estén disponibles. Implementemos esto:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[Cliente 0] ¡Quiero pan!\n",
      "[Cliente 1] ¡Quiero pan!\n",
      "[Cliente 2] ¡Quiero pan!\n",
      "[Cliente 3] ¡Quiero pan!\n",
      "[Cliente 4] ¡Quiero pan!\n",
      "[Cliente 5] ¡Quiero pan!\n",
      "[Cliente 6] ¡Quiero pan!\n",
      "[Cliente 7] ¡Quiero pan!\n",
      "[Cliente 8] ¡Quiero pan!\n",
      "[Cliente 9] ¡Quiero pan!\n",
      "[Cliente 10] ¡Quiero pan!\n",
      "[Cliente 11] ¡Quiero pan!\n",
      "[Cliente 12] ¡Quiero pan!\n",
      "[Cliente 13] ¡Quiero pan!\n",
      "[Cliente 14] ¡Quiero pan!\n",
      "[Panadero] Produje 5 piezas de pan en la partida 0\n",
      "[Cliente 1] ¡Saqué mi Hallulla!\n",
      "[Cliente 12] ¡Saqué mi Marraqueta!\n",
      "[Cliente 14] ¡Saqué mi Hallulla!\n",
      "[Cliente 3] ¡Saqué mi Marraqueta!\n",
      "[Cliente 5] ¡Saqué mi Hallulla!\n",
      "[Panadero] Produje 5 piezas de pan en la partida 1\n",
      "[Cliente 2] ¡Saqué mi Baguette!\n",
      "[Cliente 9] ¡Saqué mi Baguette!\n",
      "[Cliente 7] ¡Saqué mi Marraqueta!\n",
      "[Cliente 11] ¡Saqué mi Baguette!\n",
      "[Cliente 13] ¡Saqué mi Hallulla!\n",
      "[Panadero] Produje 5 piezas de pan en la partida 2\n",
      "[Cliente 0] ¡Saqué mi Hallulla!\n",
      "[Cliente 6] ¡Saqué mi Marraqueta!\n",
      "[Cliente 8] ¡Saqué mi Marraqueta!\n",
      "[Cliente 10] ¡Saqué mi Hallulla!\n",
      "[Cliente 4] ¡Saqué mi Marraqueta!\n"
     ]
    }
   ],
   "source": [
    "from collections import deque\n",
    "from random import choices\n",
    "import threading\n",
    "import time\n",
    "\n",
    "\n",
    "piezas_de_pan = deque()\n",
    "\n",
    "\n",
    "def panadero() -> None:\n",
    "    # El panadero hará 3 veces pan\n",
    "    for partida in range(3):\n",
    "        # En cada vez, producirá 5 piezas de pan\n",
    "        # Se demorará 5 segundos por vez (que rápido :D)\n",
    "        time.sleep(5)\n",
    "        piezas = choices([\"Marraqueta\", \"Baguette\", \"Hallulla\"], k=5)\n",
    "        print(\"[Panadero] Produje 5 piezas de pan en la partida\", partida)\n",
    "        piezas_de_pan.extend(piezas)\n",
    "\n",
    "\n",
    "lock_sacar_pan = threading.Lock()\n",
    "\n",
    "\n",
    "def cliente(i: int) -> None:\n",
    "    print(f\"[Cliente {i}] ¡Quiero pan!\")\n",
    "    while True:\n",
    "        # El cliente verifica si hay pan antes de sacarlo\n",
    "        # Necesitamos asegurarnos que si vio que había pan, nadie se lo quite\n",
    "        # Para eso, ponemos un lock para que la operación\n",
    "        # de ver si había pan y luego sacarlo sea atómica\n",
    "        with lock_sacar_pan:\n",
    "            if piezas_de_pan:\n",
    "                mi_pan = piezas_de_pan.popleft()\n",
    "                print(f\"[Cliente {i}] ¡Saqué mi {mi_pan}!\")\n",
    "                # Cuando logre sacar su pan, deja de verificar y termina\n",
    "                break\n",
    "\n",
    "\n",
    "thread_panadero = threading.Thread(target=panadero)\n",
    "threads_clientes = []\n",
    "\n",
    "for i in range(15):\n",
    "    threads_clientes.append(threading.Thread(target=cliente, args=(i, )))\n",
    "\n",
    "thread_panadero.start()\n",
    "for thread_cliente in threads_clientes:\n",
    "    thread_cliente.start()\n",
    "\n",
    "# Esperamos a que todos los threads finalicen para que esta celda termine con su ejecución\n",
    "for thread_cliente in threads_clientes:\n",
    "    thread_cliente.join()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La implementación anterior es correcto, pero tiene un problema de eficiencia: mientras los clientes esperan, utilizan tiempo de CPU en forma innecesaria verificando si hay pan o no, ya que si no hay pan vuelven a revisar de inmediato. Podríamos evitar este uso innecesario de tiempo si el panadero enviara una señal a los clientes cuando él tenga pan listo. No obstante, los clientes tendrán que verificar de igual manera que todavía quede pan, porque podría haber más clientes que piezas de pan disponibles en ese momento.\n",
    "\n",
    "Afortunadamente, en Python existe una biblioteca optimizada para manejar este tipo de casos.\n",
    "\n",
    "### `Queue`\n",
    "\n",
    "El módulo `queue` tiene implementada una cola hecha para situaciones donde hay varios *threads*. Tiene métodos que la hacen un poco diferente a la implementada en `collections`:\n",
    "\n",
    "- `put()`: Agrega un ítem al final de la cola (*push*)\n",
    "- `get()`: Remueve y retorna un ítem de la cola (*pop*). Lo interesante es que este método **espera** hasta que exista algo para sacar de la cola.\n",
    "- `task_done()`: Requiere ser llamado cada vez que un ítem extraído de la cola ha sido procesado.\n",
    "- `join()`: El *thread* que llame a este método queda en pausa hasta que todos los ítems de la cola hayan sido procesados.\n",
    "\n",
    "Volvamos al ejemplo anterior, ahora usando `Queue`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[Cliente 0] ¡Quiero pan!\n",
      "[Cliente 1] ¡Quiero pan!\n",
      "[Cliente 2] ¡Quiero pan!\n",
      "[Cliente 3] ¡Quiero pan!\n",
      "[Cliente 4] ¡Quiero pan!\n",
      "[Cliente 5] ¡Quiero pan!\n",
      "[Cliente 6] ¡Quiero pan!\n",
      "[Cliente 7] ¡Quiero pan!\n",
      "[Cliente 8] ¡Quiero pan!\n",
      "[Cliente 9] ¡Quiero pan!\n",
      "[Cliente 10] ¡Quiero pan!\n",
      "[Cliente 11] ¡Quiero pan!\n",
      "[Cliente 12] ¡Quiero pan!\n",
      "[Cliente 13] ¡Quiero pan!\n",
      "[Cliente 14] ¡Quiero pan!\n",
      "[Panadero] Produje 5 piezas de pan en la partida 0\n",
      "[Cliente 4] ¡Saqué mi Baguette!\n",
      "[Cliente 1] ¡Saqué mi Marraqueta!\n",
      "[Cliente 2] ¡Saqué mi Marraqueta!\n",
      "[Cliente 3] ¡Saqué mi Hallulla!\n",
      "[Cliente 0] ¡Saqué mi Baguette!\n",
      "[Panadero] Produje 5 piezas de pan en la partida 1\n",
      "[Cliente 7] ¡Saqué mi Marraqueta!\n",
      "[Cliente 6] ¡Saqué mi Hallulla!\n",
      "[Cliente 8] ¡Saqué mi Hallulla!\n",
      "[Cliente 9] ¡Saqué mi Marraqueta!\n",
      "[Cliente 5] ¡Saqué mi Marraqueta!\n",
      "[Panadero] Produje 5 piezas de pan en la partida 2\n",
      "[Cliente 12] ¡Saqué mi Baguette!\n",
      "[Cliente 14] ¡Saqué mi Marraqueta!\n",
      "[Cliente 13] ¡Saqué mi Hallulla!\n",
      "[Cliente 11] ¡Saqué mi Marraqueta!\n",
      "[Cliente 10] ¡Saqué mi Marraqueta!\n"
     ]
    }
   ],
   "source": [
    "from queue import Queue\n",
    "from random import choices\n",
    "import threading\n",
    "import time\n",
    "\n",
    "\n",
    "piezas_de_pan = Queue()\n",
    "\n",
    "\n",
    "def panadero() -> None:\n",
    "    # El panadero hará 3 veces pan\n",
    "    for partida in range(3):\n",
    "        # En cada vez, producirá 5 piezas de pan.\n",
    "        # Se demorará 5 segundos por vez (que rápido :D)\n",
    "        time.sleep(5)\n",
    "        print(\"[Panadero] Produje 5 piezas de pan en la partida\", partida)\n",
    "        piezas = choices([\"Marraqueta\", \"Baguette\", \"Hallulla\"], k=5)\n",
    "        for pieza in piezas:\n",
    "            piezas_de_pan.put(pieza)\n",
    "\n",
    "\n",
    "def cliente(i: int) -> None:\n",
    "    print(f\"[Cliente {i}] ¡Quiero pan!\")\n",
    "    mi_pan = piezas_de_pan.get()\n",
    "    print(f\"[Cliente {i}] ¡Saqué mi {mi_pan}!\")\n",
    "    piezas_de_pan.task_done()\n",
    "\n",
    "\n",
    "thread_panadero = threading.Thread(target=panadero)\n",
    "threads_clientes = []\n",
    "\n",
    "for i in range(15):\n",
    "    threads_clientes.append(threading.Thread(target=cliente, args=(i, )))\n",
    "\n",
    "thread_panadero.start()\n",
    "for thread_cliente in threads_clientes:\n",
    "    thread_cliente.start()\n",
    "\n",
    "# Esperamos a que todos los threads finalicen para que esta celda termine con su ejecución\n",
    "for thread_cliente in threads_clientes:\n",
    "    thread_cliente.join()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En este ejemplo, solo 1 *thread* obtenía el recurso de la `Queue`, pero puede darse el caso que queremos que cada *thread* tenga su propio recurso. En particular, pensemos en un aviso del curso. En este caso se manda un mensaje, pero queremos que todos los demás *threads* sean capaces de recibir ese recurso. Por lo tanto, en vez de usar una `Queue` compartida, podríamos usar una `Queue` por usuario, y cada uno de estos está atento a si su propia `Queue` tiene algún dato por procesar."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[CANVAS] Enviando un aviso a los estudiantes\n",
      "[CANVAS] Enviando un aviso a los estudiantes\n",
      "[Komi] Ohhh tengo un correo[Luigi] Ohhh tengo un correo\n",
      "[Luigi] Su contenido es: Aviso Semanal 🦖\n",
      "[Luigi] Ohhh tengo un correo\n",
      "\n",
      "[Gogeta] Ohhh tengo un correo\n",
      "[Luffy] Ohhh tengo un correo\n",
      "[Naruto] Ohhh tengo un correo\n",
      "[Komi] Su contenido es: Aviso Semanal 🦖[Luigi] Su contenido es: Recuerden hacer la evaluación de Threading! 🏃‍♀️\n",
      "[Luffy] Su contenido es: Aviso Semanal 🦖\n",
      "[Luffy] Ohhh tengo un correo\n",
      "[Luffy] Su contenido es: Recuerden hacer la evaluación de Threading! 🏃‍♀️\n",
      "\n",
      "[Komi] Ohhh tengo un correo\n",
      "[Gogeta] Su contenido es: Aviso Semanal 🦖[Naruto] Su contenido es: Aviso Semanal 🦖\n",
      "[Naruto] Ohhh tengo un correo\n",
      "\n",
      "[Gogeta] Ohhh tengo un correo\n",
      "[Komi] Su contenido es: Recuerden hacer la evaluación de Threading! 🏃‍♀️\n",
      "[Naruto] Su contenido es: Recuerden hacer la evaluación de Threading! 🏃‍♀️\n",
      "[Gogeta] Su contenido es: Recuerden hacer la evaluación de Threading! 🏃‍♀️\n"
     ]
    }
   ],
   "source": [
    "import threading\n",
    "import queue\n",
    "import random\n",
    "import time\n",
    "\n",
    "\n",
    "class Estudiante(threading.Thread):\n",
    "    def __init__(self, nombre: str) -> None:\n",
    "        # Enviamos el nombre con super() a la clase padre\n",
    "        threading.Thread.__init__(self, name=nombre)\n",
    "        \n",
    "        # Creamos nuestra propia cola de correo\n",
    "        self.cola_correos = queue.Queue()\n",
    "\n",
    "    def notificar(self, aviso: str) -> None:\n",
    "        self.cola_correos.put(aviso)\n",
    "\n",
    "    def run(self) -> None:\n",
    "        while True:\n",
    "            aviso = self.cola_correos.get()\n",
    "            print(f\"[{self.name}] Ohhh tengo un correo\")\n",
    "            time.sleep(random.randint(0, 2))\n",
    "            print(f\"[{self.name}] Su contenido es: {aviso}\")\n",
    "            self.cola_correos.task_done()\n",
    "\n",
    "class Canvas:\n",
    "    def __init__(self) -> None:\n",
    "        self.estudiantes = []\n",
    "\n",
    "    def agregar_estudiante(self, estudiante: Estudiante) -> None:\n",
    "        self.estudiantes.append(estudiante)\n",
    "\n",
    "    def mandar_aviso(self, aviso: str) -> None:\n",
    "        print(\"[CANVAS] Enviando un aviso a los estudiantes\")\n",
    "        for estudiante in self.estudiantes:\n",
    "            estudiante.notificar(aviso)\n",
    "\n",
    "canvas = Canvas()\n",
    "\n",
    "nombres = [\"Komi\", \"Naruto\", \"Luigi\", \"Gogeta\", \"Luffy\"]\n",
    "for nombre in nombres:\n",
    "    estudiante = Estudiante(nombre)\n",
    "    canvas.agregar_estudiante(estudiante)\n",
    "    estudiante.start()\n",
    "\n",
    "# Simulación del hilo principal que envía notificaciones\n",
    "canvas.mandar_aviso(\"Aviso Semanal 🦖\")\n",
    "canvas.mandar_aviso(\"Recuerden hacer la evaluación de Threading! 🏃‍♀️\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con `Queue` también podríamos comunicar *threads* para que hagan ciertas tareas. En ese caso, en vez de pasar objetos cualquiera podríamos pasar mensajes con cierto formato que todos puedan entender."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Simulaciones\n",
    "\n",
    "Otra aplicación regular para el uso de *threads* es la simulación de entidades con comportamiento simultáneo. Esto, en conjunto a *locks* y señales, permite que se puedan modelar y simular situaciones únicas.\n",
    "\n",
    "Por ejemplo, en el siguiente ejemplo se simula a un estudiante que estudia muy encarecidamente y programa línea por línea. Al mismo tiempo, se modela a su profesor que espera a que el estudiante tenga alguna duda durante su estudio, y solo en caso de que tenga una duda, este le responde:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[Estudiante] ¡A programar!\n",
      "[Estudiante] --------- Una linea escrita\n",
      "\n",
      "[Estudiante] Tengo una duda profe :(\n",
      "[Profesor] Mira, lo que pasa es que...\n",
      "[Profesor]... 4 segundos después\n",
      "[Estudiante] Ahora entiendo profe, gracias :)\n",
      "\n",
      "[Estudiante] --------- Una linea escrita\n",
      "\n",
      "[Estudiante] Tengo una duda profe :(\n",
      "[Profesor] Mira, lo que pasa es que...\n",
      "[Profesor]... 4 segundos después\n",
      "[Estudiante] Ahora entiendo profe, gracias :)\n",
      "\n",
      "[Estudiante] --------- Una linea escrita\n",
      "\n",
      "[Estudiante] Tengo una duda profe :(\n",
      "[Profesor] Mira, lo que pasa es que...\n",
      "[Profesor]... 1 segundos después\n",
      "[Estudiante] Ahora entiendo profe, gracias :)\n",
      "\n",
      "[Estudiante] --------- Una linea escrita\n",
      "\n",
      "[Estudiante] Tengo una duda profe :(\n",
      "[Profesor] Mira, lo que pasa es que...\n",
      "[Profesor]... 3 segundos después\n",
      "[Estudiante] Ahora entiendo profe, gracias :)\n",
      "\n",
      "[Estudiante] --------- Una linea escrita\n",
      "\n",
      "[Estudiante] --------- Una linea escrita\n",
      "\n",
      "[Estudiante] --------- Una linea escrita\n",
      "\n",
      "[Estudiante] Tengo una duda profe :(\n",
      "[Profesor] Mira, lo que pasa es que...\n",
      "[Profesor]... 2 segundos después\n",
      "[Estudiante] Ahora entiendo profe, gracias :)\n",
      "\n",
      "[Estudiante] --------- Una linea escrita\n",
      "\n",
      "[Estudiante] --------- Una linea escrita\n",
      "\n",
      "[Estudiante] --------- Una linea escrita\n",
      "\n",
      "[Estudiante] ¡Terminé de programar!\n"
     ]
    }
   ],
   "source": [
    "from threading import Event, Thread\n",
    "from time import sleep\n",
    "from random import randint\n",
    "\n",
    "\n",
    "class Estudiante(Thread): # Estudiante es un Thread\n",
    "             \n",
    "    def __init__(self, lineas_estudio: int, senal_duda: Event, senal_resuelto: Event):\n",
    "        super().__init__()\n",
    "        self.max_lineas = lineas_estudio # Líneas de código que programará\n",
    "        self.senal_duda = senal_duda # Señal para notificar que estudiante tiene una duda\n",
    "        self.senal_resuelto = senal_resuelto # Señal para notificar que profesor resolvió duda\n",
    "    \n",
    "    \n",
    "    def run(self) -> None: # Simulación de comportamiento de Estudiante\n",
    "        \n",
    "        print(\"[Estudiante] ¡A programar!\")\n",
    "        lineas = 0 # Comienza con archivo vacio.\n",
    "        while lineas < self.max_lineas:\n",
    "            sleep(1) # Se demora dos segundos en escribir una línea\n",
    "            lineas += 1 \n",
    "            print(\"[Estudiante] --------- Una linea escrita\\n\")\n",
    "            # Existe una probabilidad luego de cada línea de que tenga una duda\n",
    "            # A medida que escribe líneas, es menos probable\n",
    "            if randint(1, self.max_lineas) >= lineas:\n",
    "                print(\"[Estudiante] Tengo una duda profe :(\")\n",
    "                self.senal_duda.set() # Notifica que tuvo una duda.\n",
    "                self.senal_resuelto.wait() # Espera a que duda se resuelva\n",
    "                self.senal_resuelto.clear() # Reinicia señal para volver a usarse\n",
    "                print(\"[Estudiante] Ahora entiendo profe, gracias :)\\n\")\n",
    "        print(\"[Estudiante] ¡Terminé de programar!\")\n",
    "\n",
    "\n",
    "\n",
    "class Profe(Thread): # Profe es un Thread\n",
    "    \n",
    "    def __init__(self, senal_duda: Event, senal_resuelto: Event) -> None:\n",
    "        super().__init__() \n",
    "        self.senal_duda = senal_duda # Señal para notificar que estudiante tiene una duda\n",
    "        self.senal_resuelto = senal_resuelto # Señal para notificar que profesor resolvió duda\n",
    "        self.daemon = True # El profe debe ser daemon\n",
    "    \n",
    "    def run(self) -> None: # Simulación de comportamiento de Profe\n",
    "        \n",
    "        while True: # Espera indefinidamente por siempre. Como es daemon, se detendrá con el resto del programa\n",
    "            \n",
    "            self.senal_duda.wait() # Espera a que alumne tenga una duda.\n",
    "            self.senal_duda.clear() # Reinicia señal para volver a usarse.\n",
    "            print(\"[Profesor] Mira, lo que pasa es que...\")\n",
    "            tiempo = randint(1, 4)\n",
    "            sleep(tiempo) # Explica y se demora un poco\n",
    "            print(f\"[Profesor]... {tiempo} segundos después\") \n",
    "            self.senal_resuelto.set() # Notifica que la duda fue resuelta.\n",
    "\n",
    "\n",
    "senal_tengo_una_duda = Event()\n",
    "senal_duda_resuelta = Event()\n",
    "\n",
    "estudiante = Estudiante(10, senal_tengo_una_duda, senal_duda_resuelta)\n",
    "profe = Profe(senal_tengo_una_duda, senal_duda_resuelta)\n",
    "\n",
    "profe.start()\n",
    "estudiante.start()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "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.12.10"
  },
  "varInspector": {
   "cols": {
    "lenName": 16,
    "lenType": 16,
    "lenVar": 40
   },
   "kernels_config": {
    "python": {
     "delete_cmd_postfix": "",
     "delete_cmd_prefix": "del ",
     "library": "var_list.py",
     "varRefreshCmd": "print(var_dic_list())"
    },
    "r": {
     "delete_cmd_postfix": ") ",
     "delete_cmd_prefix": "rm(",
     "library": "var_list.r",
     "varRefreshCmd": "cat(var_dic_list()) "
    }
   },
   "types_to_exclude": [
    "module",
    "function",
    "builtin_function_or_method",
    "instance",
    "_Feature"
   ],
   "window_display": false
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
