{
 "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. [Introducción](#Introducción)\n",
    "2. [Procesos y núcleos](#Procesos-y-núcleos)\n",
    "3. [Paralelismo VS Concurrencia](#Paralelismo-VS-Concurrencia)\n",
    "4. [*Threading*](#Threading)\n",
    "    1. [Usos de *threads*](#Usos-de-threads)\n",
    "    2. [Creación de *threads*](#Creación-de-threads)\n",
    "        1. [¿Qué pasa con los `print`?](#¿Qué-pasa-con-los-print?)\n",
    "    4. [Esperando otros *threads*: `join()`](#join())\n",
    "    5. [`is_alive()`](#is_alive())\n",
    "    6. [*Daemons*](#Daemons)\n",
    "5. [Timers](#Timers)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Introducción"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Hasta el momento, los programas que hemos escrito solo ejecutan **una secuencia de instrucciones** a la vez. Siguen un único flujo que comienza, ejecuta instrucciones, y en algún momento termina. \n",
    "\n",
    "Cada vez que queremos que el usuario ingrese un valor (por ejemplo, una opción en un menú), tenemos que ejecutar `input()` y esperar que el usuario responda. Mientras eso ocurre, nuestro programa debe pausarse mientras espera la respuesta. De la misma manera, si el usuario apreta una tecla y nuestro programa no está esperando que lo haga, no tiene cómo darse cuenta ni cómo recibir esa entrada. Esto es una consecuencia de que los programas que hemos escrito solo siguen un único flujo de instrucciones, y por lo tanto están atentos a sólo una cosa al mismo tiempo.\n",
    "\n",
    "Muchos programas o aplicaciones que utilizamos realizan múltiples acciones simultáneamente. Cuando escribimos en un procesador de texto, al mismo tiempo se está ejecutando un código que revisa la ortografía. Cuando escribimos código fuente en una IDE, al mismo tiempo también se está ejecutando un código que revisa la sintaxis, que cambia los colores del texto y a veces nos hace sugerencias para completar lo que escribimos. Cuando estamos dentro de un juego, estamos usando teclas para mover a un personaje, pero también están pasando otros eventos en el ambiente del juego al mismo tiempo que movemos el personaje. Si tuviéramos que programar este comportamiento con las herramientas que tenemos, tendríamos que tener un ciclo que, por cada `input()` del usuario, ejecute métodos que revisen ortografía, sintaxis, que hagan sugerencias, que simulen el ambiente del juego, etc. Este comportamiento sería complicado de programar dentro de un programa que pone atención solo a un flujo de instrucciones.\n",
    "\n",
    "La solución entonces, es que nuestro programa esté atento a múltiples instrucciones simultáneamente. Podemos conseguir esto teniendo \"subprogramas\": uno que ejecuta el método que revisa ortografía, uno que ejecuta el método que revisa sintaxis, uno que ejecuta el método que hace sugerencias, uno que simula el juego... pero todo dentro del mismo programa. ¿Cómo conseguir esto? El concepto de ***thread*** nos permite definir **múltiples flujos de instrucciones** dentro del mismo código en nuestro programa.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Procesos y núcleos\n",
    "\n",
    "En nuestros computadores, podemos ver que se ejecutan varios programas o aplicaciones simultáneamente. Esto lo consigue el computador (en verdad, el sistema operativo) a través de la abstracción de un **proceso**. Un proceso es un **programa en ejecución** que ocupa un espacio de la memoria (RAM) del computador, y ejecuta un flujo de instrucciones. Cuando lanzamos un reproductor de música, un navegador web, un editor de texto, nuestra IDE favorita, o nuestro programa de Python, el computador lo ejecuta mediante un **proceso** que sigue un flujo de instrucciones a partir de la primera línea de código. \n",
    "\n",
    "**¿Cómo se ejecutan las instrucciones?**, la unidad que ejecuta instrucciones en un computador es la CPU, la cual posee un núcleo (*core*) que lee una instrucción del programa, la ejecuta, y pasa a la siguiente, hasta que las instrucciones se acaban.\n",
    "\n",
    "Cuando nuestro programa Python está en ejecución (mediante un proceso), hay un núcleo de nuestro computador que está siguiendo ese flujo de instrucciones. Decimos que hay un proceso que ejecuta nuestro programa Python en la CPU.\n",
    "\n",
    "Los sistemas modernos poseen múltiples núcleos (*cores*). Es común que nuestros computadores, teléfonos celulares y hasta televisores tengan más de un núcleo, y sean ***dualcore***, ***quadcore***, ***octacore***. Eso significa que puede haber varios procesos, uno en cada núcleo y ejecutando de manera realmente simultánea, o paralela. Cada núcleo ejecuta el flujo de instrucciones de un proceso distinto. Cuando hay más procesos que núcleos para ejecutarlos, los procesos se ponen en una cola, esperando su turno para usar un núcleo. El sistema operativo del computador se encarga de que cada núcleo ejecute algunas instrucciones de un proceso, luego lo deje en pausa, y luego ejecute las instrucciones de otros procesos disponibles. Esto ocurre de manera tan rápida, que lo que observamos es que todos los procesos se ejecutan de manera simultánea dentro del mismo núcleo.\n",
    "\n",
    "Pero cuando tenemos múltiples flujos de instrucciones en un programa, podemos hacer que varios núcleos ejecuten los distintos flujos de instrucciones **de un mismo proceso** al mismo tiempo. Eso es parte del poder que nos entregan los ***threads***.\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Paralelismo VS Concurrencia  \n",
    "Antes de profundizar en el concepto de *threads*, es crucial entender la diferencia entre dos conceptos que suelen confundirse: **paralelismo** y **concurrencia**.  \n",
    "\n",
    "\n",
    "El **paralelismo** implica la ejecución simultánea de varias tareas en diferentes núcleos o procesadores. Es decir, operan al mismo tiempo en hardware separado. Es por esto que no es posible obtener un paralelismo real cuando solo se posee un núcleo.\n",
    "\n",
    "La **concurrencia** consiste en gestionar varias tareas de forma intercalada en un mismo núcleo o procesador. Sin embargo, aunque solo una tarea se ejecute en un instante dado, los cambios de contexto se realizan tan rápido que aparentan simultaneidad.\n",
    "\n",
    "En Python, la concurrencia se puede alcanzar con *threads* mediante el módulo `threading`. Ahora bien, Python posee un mecanismo llamado *Global Interpreter Lock* ([GIL](https://docs.python.org/3/glossary.html#term-global-interpreter-lock)) el cual, independiente de la cantidad de núcleos que tenga un computador, evita ejecutar *threads* en paralelo. Esto se debe a que un limita la ejecución simultánea de *threads*, ya que se encarga de que solamente un *thread* pueda ejecutar código Python en un instante dado. Para lograr **paralelismo** real en operaciones intensivas de CPU se pueden emplear otras librerías como `multiprocessing`, o, en versiones recientes de python, desactivar manualmente el `GIL`.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## *Threading*"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Un ***thread*** (o hilo) es una unidad de ejecución de código dentro de un proceso. Si un proceso es el programa cargado en la memoria del computador, con su código y variables, un ***thread*** lleva la cuenta de en qué parte del código estamos ejecutando. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "![thread](imgs/thread.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Todo proceso al crearse posee al menos un ***thread***. A este *thread* le llamamos el *thread principal* del programa. Pero podemos crear más *threads*, y de esa manera hacer que nuestro programa esté ejecutando varias partes del código simultáneamente. Cada ***thread*** lleva su propio registro de variables locales y en qué parte del código del programa se encuentra. Un *thread* también puede crear otros *threads*.\n",
    "\n",
    "Gracias al sistema operativo, cada *thread* tiene una porción de tiempo para ejecutarse en el núcleo del computador, y podemos ver que varios *threads* se ejecutan de manera simultánea, o *concurrente*. La decisión de qué *thread* se ejecuta a continuación y por cuánto tiempo, se conoce como *thread scheduling* o *time slicing*, que funciona –en general– turnando los *threads* de la siguiente manera:\n",
    "\n",
    "1. Se escoge un *thread* entre los que están disponible para ejecutar.\n",
    "2. Se ejecuta ese *thread* durante un cierto tiempo.\n",
    "3. Se deja el *thread* actual en espera.\n",
    "4. Se vuelve al paso 1.\n",
    "\n",
    "Cuando hay un sólo núcleo en el computador, no existe realmente un paralelismo. Sin embargo, los turnos se pasan de manera tan rápida entre los *threads*, que se comporta como si todos estuvieran avanzando en paralelo. Es por esto que dentro de un sólo núcleo, usar *threads* hace que nuestro programa se ejecute de manera más lenta, pues ahora hay que gastar algo de tiempo en controlar los turnos de los *threads*. Sin embargo, la programación es mucho más clara y sencilla cuando usamos *threads* que se hacen cargo de distintos aspectos de nuestro programa, debido a que no debemos programar de forma explícita el salto entre distintas funcionalidades del programa en un solo hilo de ejecución."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Usos de *threads*\n",
    "\n",
    "Algunos ejemplos donde resulta útil implementar *threads* son:\n",
    "\n",
    "- **Separar funciones independientes que trabajan sobre un mismo conjunto de datos pero pueden ejecutarse simultáneamente**. Por ejemplo, un *thread* que se encargue de recibir *input* de texto de un usuario y colocarlo en una cola de texto por procesar, y otro *thread* que procese línea a línea y los saque de la cola. Este tipo de patrón, donde existen procesos que se ejecutan secuencialmente con acceso al mismo conjunto de datos pero que son independientes entre ellos, se conoce como modelo **productor-consumidor** y se ejemplifica en uno de los siguientes *notebooks*.\n",
    "- **Interfaces gráficas**, donde se necesita interactuar con el usuario mientras se ejecuta algún *thread* de cómputo pesado. Por ejemplo, las interfaces gráficas en un computador: reciben *input* de usuario mediante entradas de texto y botones, mientras otros *threads* actualizan la parte visual simultáneamente.\n",
    "- **Aplicaciones multiusuario**, en donde cada *thread* se encargaría de las peticiones de cada usuario independientemente. Por ejemplo, un servidor de páginas web debe atender a varios clientes a la vez: cada vez que se conecta un usuario, se debe responder a este de forma correcta, y múltiples usuarios pueden pedir al mismo tiempo una conexión."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Creación de *threads*"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En Python 3, el manejo de *threads* se realiza mediante el módulo `threading`, en donde los *threads* son representados por objetos. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import threading"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La clase `Thread` representa un hilo (o *thread*). La intención es que cada hilo ejecute una secuencia de instrucciones específica de manera simultanea al resto de nuestro programa. Para especificar qué instrucciones ejecutará, se le entrega una función, la que se recibe en el parámetro `target` al crear el *thread*. Es decir, cuando el *thread* comience a funcionar, ejecutará la función entregada en este parámetro."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "def contar_diez_ovejas() -> None:\n",
    "    print(\"Tengo sueño...\")\n",
    "    for numero in range(1, 11):\n",
    "        print(f\"({numero} oveja{'s' if numero > 1 else ''})\")\n",
    "    print(\"A dormir...\")\n",
    "\n",
    "mi_hilo = threading.Thread(target=contar_diez_ovejas)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Aquí, se creó una instancia de `Thread`, y se almacenó en la variable `mi_hilo`. Recibió como argumento para ejecutar, la función `contar_diez_ovejas`. Notamos que al crear una instancia de `Thread`, este **NO** se ejecuta automáticamente. Es por eso que no vemos *output* generado, ya que no se están ejecutando las instrucciones de `contar_diez_ovejas`, solo definimos un hilo que puede hacerlo.\n",
    "\n",
    "Para efectivamente ejecutar el *thread*, se debe llamar al método `start()`. **Es altamente recomendable que éste y todos los ejemplos los revises ejecutando código desde una consola, en lugar de los Jupyter Noteboks, para ver como se comporta.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Tengo sueño...\n",
      "(1 oveja)\n",
      "(2 ovejas)\n",
      "(3 ovejas)\n",
      "(4 ovejas)\n",
      "(5 ovejas)\n",
      "(6 ovejas)\n",
      "(7 ovejas)\n",
      "(8 ovejas)\n",
      "(9 ovejas)\n",
      "(10 ovejas)\n",
      "A dormir...\n"
     ]
    }
   ],
   "source": [
    "mi_hilo.start()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Es importante notar, que los `Thread` definidos de esta forma son, por defecto, de un solo uso. Es decir, una vez ejecutados, no es posible volver a ejecutarlos. Para eso, es necesario volver a instanciar el *thread*. El siguiente código lanzará un error, ya que el *thread* que intentamos comenzar ya lo hizo antes:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "ename": "RuntimeError",
     "evalue": "threads can only be started once",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mRuntimeError\u001b[39m                              Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[4]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m \u001b[43mmi_hilo\u001b[49m\u001b[43m.\u001b[49m\u001b[43mstart\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n",
      "\u001b[36mFile \u001b[39m\u001b[32m/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/threading.py:989\u001b[39m, in \u001b[36mThread.start\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m    986\u001b[39m     \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mRuntimeError\u001b[39;00m(\u001b[33m\"\u001b[39m\u001b[33mthread.__init__() not called\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m    988\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m._started.is_set():\n\u001b[32m--> \u001b[39m\u001b[32m989\u001b[39m     \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mRuntimeError\u001b[39;00m(\u001b[33m\"\u001b[39m\u001b[33mthreads can only be started once\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m    991\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m _active_limbo_lock:\n\u001b[32m    992\u001b[39m     _limbo[\u001b[38;5;28mself\u001b[39m] = \u001b[38;5;28mself\u001b[39m\n",
      "\u001b[31mRuntimeError\u001b[39m: threads can only be started once"
     ]
    }
   ],
   "source": [
    "mi_hilo.start()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Como se mencionó inicialmente, todo programa en ejecución tiene al menos un *thread*: el *thread* principal, que corresponde al hilo que ejecuta el flujo principal de nuestro programa. La clase `Thread` admite el argumento `name`, que es un nombre que le podemos dar al *thread* para poder identificarlo. El nombre que se le asigna por defecto al *thread* principal es `\"MainThread\"`. En el siguiente código, la función `saludar` imprime el nombre del *thread* que lo ejecuta."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "def saludar() -> None:\n",
    "    thread_actual = threading.current_thread()\n",
    "    print(f\"Hola desde {thread_actual.name}\\n\")\n",
    "\n",
    "\n",
    "hilo_1 = threading.Thread(name=\"Mi thread 1\", target=saludar)\n",
    "hilo_2 = threading.Thread(name=\"Mi thread 2\", target=saludar)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`threading.current_thread()` es una función que retorna **una referencia de la instancia del *thread* que está ejecutando este código** (el *thread* que está en la CPU). Gracias a eso, se puede acceder a atributos de la instancia de `Thread`, como el nombre (`name`). A continuación, veremos que cada *thread* imprime su nombre correspondiente, incluso el *thread* principal:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Hola desde Mi thread 1\n",
      "\n",
      "Hola desde Mi thread 2\n",
      "\n",
      "Hola desde MainThread\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# Llamaremos a saludar() desde los threads nombrados\n",
    "hilo_1.start()\n",
    "hilo_2.start()\n",
    "# Ahora llamaremos a saludar() desde el thread principal\n",
    "saludar()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En el siguiente ejemplo, crearemos tres *threads*: `hilo_lento`, `hilo_rapido_1` e `hilo_rapido_2`, que ejecutarán distintas instancias de las funciones `trabajador_rapido` y `trabajador_lento`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Thread principal: Fueron creados 3 threads\n"
     ]
    }
   ],
   "source": [
    "import threading\n",
    "import time\n",
    "\n",
    "\n",
    "# Función rápida, que toma 2 segundos\n",
    "def trabajador_rapido() -> None:\n",
    "    thread_actual = threading.current_thread()\n",
    "    print(f\"{thread_actual.name} partiendo...\")\n",
    "    \n",
    "    # Pondremos a dormir el thread por 2 segundos simulando\n",
    "    # que ocurre algun proceso dentro de la función\n",
    "    time.sleep(2)\n",
    "    print(f\"{thread_actual.name} terminando...\")\n",
    "\n",
    "# Función lenta, que toma 6 segundos\n",
    "def trabajador_lento() -> None:\n",
    "    thread_actual = threading.current_thread()\n",
    "    print(f\"{thread_actual.name} partiendo...\")\n",
    "    \n",
    "    # Ponemos a dormir el thread por 6 segundos simulando\n",
    "    # un proceso más largo que el anterior dentro de la función\n",
    "    time.sleep(6)\n",
    "    print(f\"{thread_actual.name} terminando...\")\n",
    "\n",
    "\n",
    "# Creamos los threads usando la clase Thread\n",
    "hilo_lento = threading.Thread(name=\"Hilo lento (6s)\", target=trabajador_lento)\n",
    "hilo_rapido_1 = threading.Thread(name=\"Hilo rápido (2s)\", target=trabajador_rapido)\n",
    "# Usa el nombre asignado por defecto\n",
    "hilo_rapido_2 = threading.Thread(target=trabajador_rapido)\n",
    "print(\"Thread principal: Fueron creados 3 threads\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Thread principal: Empezaré a iniciar los 3 threads\n",
      "Hilo rápido (2s) partiendo...\n",
      "Thread-6 (trabajador_rapido) partiendo...\n",
      "Hilo lento (6s) partiendo...\n",
      "Thread principal: Fueron iniciados 3 threads\n",
      "\n",
      "Thread principal: Segundo actual: 0\n",
      "Thread principal: Segundo actual: 1\n",
      "Hilo rápido (2s) terminando...Thread-6 (trabajador_rapido) terminando...\n",
      "\n",
      "Thread principal: Segundo actual: 2\n",
      "Thread principal: Segundo actual: 3\n",
      "Thread principal: Segundo actual: 4\n",
      "Thread principal: Segundo actual: 5\n",
      "Hilo lento (6s) terminando...\n",
      "Thread principal: Segundo actual: 6\n",
      "Thread principal: Segundo actual: 7\n",
      "Thread principal: Segundo actual: 8\n",
      "Thread principal: Segundo actual: 9\n"
     ]
    }
   ],
   "source": [
    "# Se inicializan los threads creados\n",
    "print(\"Thread principal: Empezaré a iniciar los 3 threads\")\n",
    "hilo_rapido_1.start()  # Dormirá por 2 segundos\n",
    "hilo_rapido_2.start()  # Dormirá por 2 segundos\n",
    "hilo_lento.start()  # Dormirá por 6 segundos\n",
    "print(\"Thread principal: Fueron iniciados 3 threads\")\n",
    "# Todas estas líneas que siguen serán ejecutadas mientras los threads\n",
    "# se ejecutan independientemente del programa principal\n",
    "\n",
    "print()\n",
    "# El thread principal ejecutará lo que queda de código\n",
    "# mientras los otros 3 threads hacen lo suyo\n",
    "\n",
    "for i in range(10):\n",
    "    print(f\"Thread principal: Segundo actual: {i}\")\n",
    "    time.sleep(1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### ¿Qué pasa con los `print`?\n",
    "\n",
    "A partir de este ejemplo en adelante, es probable que al ejecutar `print` falten o sobran saltos de línea. La pregunta es, ¿tienen que ver los *threads* con este extraño comportamiento? La respuesta es sí.\n",
    "\n",
    "De acuerdo a [este post en StackOverflow](https://stackoverflow.com/questions/31142566/print-skipping-newline) esto se produce porque – a nivel de máquina – escribir el texto del `print` y escribir el salto de línea son **dos instrucciones distintas**. Por lo tanto, es posible que un *thread* imprima el texto, se pause ese *thread*, luego otro *thread* imprima su texto y su respectivo salto de línea, y luego al volver al primer *thread* este imprima el salto de línea que faltaba.\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En el ejemplo, vemos que una vez inicializados los *threads*, **el programa principal continúa con el resto de instrucciones mientras los *threads* se ejecutan**. Luego los tres *threads* terminan independientemente en distintos momentos. El programa principal esperará a que todos los *threads* hayan terminado correctamente.\n",
    "\n",
    "También es posible, mediante el atributo `args` o `kwargs`, pasar los argumentos a la función definida como `target` del *thread*."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Thread 1 tiene sueño...Thread 2 tiene sueño...\n",
      "\n",
      "(Thread 1: 1 oveja)\n",
      "(Thread 2: 1 oveja)\n",
      "(Thread 1: 2 ovejas)\n",
      "(Thread 2: 2 ovejas)\n",
      "(Thread 1: 3 ovejas)(Thread 2: 3 ovejas)\n",
      "\n",
      "(Thread 2: 4 ovejas)(Thread 1: 4 ovejas)\n",
      "\n",
      "(Thread 1: 5 ovejas)(Thread 2: 5 ovejas)\n",
      "\n",
      "(Thread 1: 6 ovejas)\n",
      "(Thread 2: 6 ovejas)\n",
      "(Thread 1: 7 ovejas)(Thread 2: 7 ovejas)\n",
      "\n",
      "(Thread 2: 8 ovejas)(Thread 1: 8 ovejas)\n",
      "\n",
      "(Thread 1: 9 ovejas)(Thread 2: 9 ovejas)\n",
      "\n",
      "(Thread 2: 10 ovejas)\n",
      "(Thread 1: 10 ovejas)\n",
      "Thread 1 a dormir...\n",
      "(Thread 2: 11 ovejas)\n",
      "(Thread 2: 12 ovejas)\n",
      "(Thread 2: 13 ovejas)\n",
      "(Thread 2: 14 ovejas)\n",
      "(Thread 2: 15 ovejas)\n",
      "Thread 2 a dormir...\n"
     ]
    }
   ],
   "source": [
    "import threading\n",
    "import time\n",
    "\n",
    "\n",
    "def contar_ovejas_hasta(max_ovejas: int) -> None:\n",
    "    thread_actual = threading.current_thread()\n",
    "    print(f\"{thread_actual.name} tiene sueño...\")\n",
    "    for numero in range(1, max_ovejas + 1):\n",
    "        time.sleep(1)\n",
    "        print(f\"({thread_actual.name}: {numero} oveja{'s' if numero > 1 else ''})\")\n",
    "    print(f\"{thread_actual.name} a dormir...\")\n",
    "\n",
    "\n",
    "# Se crean los threads usando la clase Thread, asociada a la función objetivo para \n",
    "# ser ejecutada por el thread, y los atributos de la función son ingresados en \n",
    "# args o kwargs\n",
    "\n",
    "t1 = threading.Thread(name=\"Thread 1\", target=contar_ovejas_hasta, args=(10,))\n",
    "t2 = threading.Thread(name=\"Thread 2\", target=contar_ovejas_hasta, kwargs={\"max_ovejas\": 15})\n",
    "t1.start()\n",
    "t2.start()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En concordancia con el modelamiento basado en OOP, podemos crear también *threads* como instancias de una clase que hereda desde la clase `Thread`. Es decir, podemos crear clases para *threads* con comportamiento común. Para definir las instrucciones que serán ejecutadas por la instancia de *thread*, se debe hacer *override* del método `run()` en la clase, el cual se ejecuta luego de llamar a `start`, al igual que antes. Con esto, ahora podremos personalizar este método para definir las instrucciones a ejecutar directamente para cada clase de *thread*."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Thread principal: Fueron creados 2 threads\n"
     ]
    }
   ],
   "source": [
    "import threading\n",
    "import time\n",
    "\n",
    "\n",
    "class CuentaOvejas(threading.Thread): # Hereda de Thread\n",
    "    \"\"\"Este será nuestro nuevo Cuenta Ovejas basado en Thread\"\"\"\n",
    "    \n",
    "    def __init__(self, nombre: str, max_ovejas: int) -> None:\n",
    "        # En el caso de los threads, lo primero es invocar al init original. SIEMPRE.\n",
    "        super().__init__(name=nombre)\n",
    "        self.max_ovejas = max_ovejas # Se agrega un atributo de instancia extra\n",
    "    \n",
    "    def run(self) -> None:\n",
    "        # Este metodo define las instrucciones a ejecutar de este thread\n",
    "        # cuando ejecutamos el metodo start()\n",
    "        print(f\"\\t{self.name} tiene sueño...\")\n",
    "        tiempo_partida = time.time()\n",
    "        for numero in range(1, self.max_ovejas + 1):\n",
    "            time.sleep(1)\n",
    "            print(f\"\\t({self.name}: {numero} oveja{'s' if numero > 1 else ''})\")\n",
    "        print(f\"\\t{self.name} a dormir...\")\n",
    "        print(f\"\\t{self.name} se durmió después de {time.time() - tiempo_partida} seg.\")\n",
    "\n",
    "        \n",
    "class CuentaLiebres(threading.Thread): # Hereda de Thread\n",
    "    \"\"\"\n",
    "    Este será un nuevo Cuenta Liebres basado en Thread\n",
    "    Las liebres son más rápidas, así que cuenta dos por segundo\n",
    "    \"\"\"\n",
    "    \n",
    "    def __init__(self, nombre: str, max_liebres: int) -> None:\n",
    "        super().__init__(name=nombre)\n",
    "        self.max_liebres = max_liebres\n",
    "    \n",
    "    def run(self) -> None:\n",
    "        print(f\"\\t{self.name} tiene sueño...\")\n",
    "        tiempo_partida = time.time()\n",
    "        for numero in range(1, self.max_liebres + 1):\n",
    "            if numero % 2 == 1:\n",
    "                time.sleep(1)\n",
    "            print(f\"\\t({self.name}: {numero} liebre{'s' if numero > 1 else ''})\")\n",
    "        print(f\"\\t{self.name} a dormir...\")\n",
    "        print(f\"\\t{self.name} se durmió después de {time.time() - tiempo_partida} seg.\")\n",
    "        \n",
    "\n",
    "# Se crean los threads\n",
    "cuenta_ovejas = CuentaOvejas(\"Gogeta\", 10)\n",
    "cuenta_liebres = CuentaLiebres(\"Anya\", 10)\n",
    "print(\"Thread principal: Fueron creados 2 threads\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\tGogeta tiene sueño...\n",
      "\tAnya tiene sueño...\n",
      "Thread principal: Fueron iniciados 2 threads\n",
      "\n",
      "Thread principal: Segundo actual: 0\n",
      "\t(Gogeta: 1 oveja)\t(Anya: 1 liebre)\n",
      "\t(Anya: 2 liebres)\n",
      "\n",
      "Thread principal: Segundo actual: 1\n",
      "\t(Anya: 3 liebres)Thread principal: Segundo actual: 2\n",
      "\n",
      "\t(Anya: 4 liebres)\n",
      "\t(Gogeta: 2 ovejas)\n",
      "Thread principal: Segundo actual: 3\t(Anya: 5 liebres)\n",
      "\t(Anya: 6 liebres)\n",
      "\t(Gogeta: 3 ovejas)\n",
      "\n",
      "\t(Anya: 7 liebres)\t(Gogeta: 4 ovejas)\n",
      "\n",
      "\t(Anya: 8 liebres)\n",
      "Thread principal: Segundo actual: 4\n",
      "\t(Gogeta: 5 ovejas)\t(Anya: 9 liebres)\n",
      "\t(Anya: 10 liebres)\n",
      "\tAnya a dormir...\n",
      "\tAnya se durmió después de 5.021545886993408 seg.\n",
      "Thread principal: Segundo actual: 5\n",
      "\n",
      "Thread principal: Segundo actual: 6\t(Gogeta: 6 ovejas)\n",
      "\n",
      "Thread principal: Segundo actual: 7\t(Gogeta: 7 ovejas)\n",
      "\n",
      "\t(Gogeta: 8 ovejas)Thread principal: Segundo actual: 8\n",
      "\n",
      "\t(Gogeta: 9 ovejas)\n",
      "Thread principal: Segundo actual: 9\n",
      "\t(Gogeta: 10 ovejas)\n",
      "\tGogeta a dormir...\n",
      "\tGogeta se durmió después de 10.042298793792725 seg.\n"
     ]
    }
   ],
   "source": [
    "# Se inicializan los threads creados\n",
    "cuenta_ovejas.start()\n",
    "cuenta_liebres.start()\n",
    "print(\"Thread principal: Fueron iniciados 2 threads\")\n",
    "# Todas estas líneas serán ejecutadas mientras los threads se ejecutan\n",
    "# independientemente del programa principal\n",
    "    \n",
    "print()\n",
    "# El thread principal ejecutará lo que queda código\n",
    "# mientras los otros 2 threads hacen lo suyo\n",
    "for i in range(10):\n",
    "    print(f\"Thread principal: Segundo actual: {i}\")\n",
    "    time.sleep(1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Esperando otros *threads*: `join()`"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Hasta ahora hemos visto comportamiento de *threads* que se ejecutan \"en paralelo\" y de forma independiente al flujo del programa principal. Pero hay ocasiones donde el programa principal puede necesitar que otros *threads* terminen antes de ejecutar ciertas instrucciones.\n",
    "\n",
    "Por ejemplo, un programa que debe obtener el total de compras de una tienda, donde el detalle de las compras se encuentran en cinco archivos en distintos formatos. El programa delega a cinco *threads* distintos el leer y sumar el total de compras de cada archivo, **espera** a que cada *thread* termine de hacer sus cálculos y luego obtiene los cinco resultados y los suma.\n",
    "\n",
    "Si necesitamos que el programa principal espere a que termine la ejecución de algún *thread* (o grupo de *threads*), debemos usar el método `join(timeout=None)` después de que el *thread* ha sido iniciado mediante `start()`.\n",
    "\n",
    "Al usar `join()`, **el *thread* que llama al método** queda bloqueado hasta que los *threads* referenciados terminen correctamente. No obstante, si se especifica un `timeout` (en segundos) el programa esperará al *thread* referenciado solo por ese tiempo y luego continuará.\n",
    "\n",
    "La figura siguiente explica gráficamente la ejecución de un programa usando `join()`.\n",
    "\n",
    "![join](imgs/join.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En el código esto se refleja como que *Main Thread* incluye las llamadas `T1.join()` y `T2.join()`.  Como es *Main Thread* quien llama a `join`, es ese el *thread* que se bloquea esperando a otro; primero espera a `T1` y luego a `T2`. Por otro lado, no sólo el *Main Thread* puede esperar: **cualquier *thread* puede esperar a cualquier otro** usando `join()`.\n",
    "\n",
    "Veamos ahora una extensión del ejemplo anterior pero con la incorporación de `join()`. Aquí, los ayudantes del curso esperan a que los profesores (o casi todos) se duerman para empezar a festejar. Notar que el comportamiento de los ayudantes está en el programa principal, y el festejo no comienza hasta después de que la mayoría de los *threads* ejecutados terminan."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\tNaruto tiene sueño...The history saving thread hit an unexpected error (OperationalError('attempt to write a readonly database')).History will not be written to the database.\n",
      "\tGoku tiene sueño...\n",
      "\n",
      "\tKomi tiene sueño...\n",
      "\tAnya tiene sueño...\n",
      "\tChisato tiene sueño...\n",
      "\n",
      "Minions: Los protagonistas se fueron a dormir...\n",
      "\t(Goku: 1 oveja)\t(Naruto: 1 oveja)\n",
      "\t(Komi: 1 liebre)\n",
      "\t(Komi: 2 liebres)\n",
      "\t(Anya: 1 liebre)\n",
      "\t(Anya: 2 liebres)\n",
      "\t(Chisato: 1 liebre)\n",
      "\t(Chisato: 2 liebres)\n",
      "\n",
      "\t(Naruto: 2 ovejas)\t(Anya: 3 liebres)\n",
      "\t(Anya: 4 liebres)\n",
      "\n",
      "\t(Komi: 3 liebres)\n",
      "\t(Komi: 4 liebres)\n",
      "\t(Chisato: 3 liebres)\n",
      "\t(Chisato: 4 liebres)\n",
      "\t(Goku: 2 ovejas)\n",
      "\tGoku a dormir...\n",
      "\tGoku se durmió después de 2.014195203781128 seg.\n",
      "Minions: ¡GOKU SE DURMIÓ!\n",
      "\t(Naruto: 3 ovejas)\t(Komi: 5 liebres)\n",
      "\tKomi a dormir...\n",
      "\tKomi se durmió después de 3.010962963104248 seg.\n",
      "\t(Chisato: 5 liebres)\n",
      "\t(Chisato: 6 liebres)\n",
      "\n",
      "\t(Anya: 5 liebres)\n",
      "\t(Anya: 6 liebres)\n",
      "\t(Chisato: 7 liebres)\n",
      "\t(Chisato: 8 liebres)\n",
      "\t(Naruto: 4 ovejas)\n",
      "\t(Anya: 7 liebres)\n",
      "\tAnya a dormir...\n",
      "\tAnya se durmió después de 4.016746997833252 seg.\n",
      "\t(Naruto: 5 ovejas)\t(Chisato: 9 liebres)\n",
      "\t(Chisato: 10 liebres)\n",
      "\n",
      "\tNaruto a dormir...\n",
      "\tNaruto se durmió después de 5.018253803253174 seg.\n",
      "Minions: ¡NARUTO SE DURMIÓ!\n",
      "Minions: ¡KOMI SE DURMIÓ!\n",
      "Minions: ¡ANYA SE DURMIÓ!\n",
      "\t(Chisato: 11 liebres)\n",
      "\t(Chisato: 12 liebres)\n",
      "Minions: ¡(casi todos) los protagonistas se durmieron! ¡A festejar!\n",
      "Minions: 🎵🎶🎵🎶🎵🎶🎵🎶🎵🎶🎵🎶🎵🎶🎵🎶\n",
      "\t(Chisato: 13 liebres)Minions: 🎵🎶🎵🎶🎵🎶🎵🎶🎵🎶🎵🎶🎵🎶🎵🎶\n",
      "\n",
      "\t(Chisato: 14 liebres)\n",
      "Minions: 🎵🎶🎵🎶🎵🎶🎵🎶🎵🎶🎵🎶🎵🎶🎵🎶\t(Chisato: 15 liebres)\n",
      "\t(Chisato: 16 liebres)\n",
      "\n",
      "\t(Chisato: 17 liebres)Minions: 🎵🎶🎵🎶🎵🎶🎵🎶🎵🎶🎵🎶🎵🎶🎵🎶\n",
      "\n",
      "\t(Chisato: 18 liebres)\n",
      "Minions: 🎵🎶🎵🎶🎵🎶🎵🎶🎵🎶🎵🎶🎵🎶🎵🎶\n",
      "\t(Chisato: 19 liebres)\n",
      "\t(Chisato: 20 liebres)\n",
      "\tChisato a dormir...\n",
      "\tChisato se durmió después de 10.03629994392395 seg.\n",
      "Minions: 🎵🎶🎵🎶🎵🎶🎵🎶🎵🎶🎵🎶🎵🎶🎵🎶\n",
      "\n",
      "Minions: Ojalá no nos hayan escuchado...\n"
     ]
    }
   ],
   "source": [
    "import threading\n",
    "import time\n",
    "\n",
    "\n",
    "# Usamos la definicion de los Thread declarados en el ejemplo anterior\n",
    "# Se crean los threads usando la clase Thread.\n",
    "naruto = CuentaOvejas(\"Naruto\", 5)\n",
    "goku = CuentaOvejas(\"Goku\", 2)\n",
    "komi = CuentaLiebres(\"Komi\", 5)\n",
    "anya = CuentaLiebres(\"Anya\", 7)\n",
    "chisato = CuentaLiebres(\"Chisato\", 20)\n",
    "\n",
    "# Se inicializan los threads creados\n",
    "naruto.start()\n",
    "goku.start()\n",
    "komi.start()\n",
    "anya.start()\n",
    "chisato.start()\n",
    "\n",
    "print(\"\\nMinions: Los protagonistas se fueron a dormir...\")\n",
    "\n",
    "# Aquí incorporamos el método join() para bloquear el programa principal\n",
    "goku.join()  # No especificamos timeout, esperará lo que sea necesario\n",
    "print(\"Minions: ¡GOKU SE DURMIÓ!\")\n",
    "\n",
    "naruto.join() # No especificamos timeout, esperará lo que sea necesario\n",
    "print(\"Minions: ¡NARUTO SE DURMIÓ!\")\n",
    "\n",
    "komi.join() # No especificamos timeout, esperará lo que sea necesario\n",
    "print(\"Minions: ¡KOMI SE DURMIÓ!\")\n",
    "\n",
    "anya.join() # No especificamos timeout, esperará lo que sea necesario\n",
    "print(\"Minions: ¡ANYA SE DURMIÓ!\")\n",
    "\n",
    "chisato.join(1)  # Esperaremos máximo 1 segundo después del último dormido, ya es muy tarde\n",
    "print(\"Minions: ¡(casi todos) los protagonistas se durmieron! ¡A festejar!\")\n",
    "\n",
    "# En este punto, el programa ha esperado por los cuatro threads que creamos\n",
    "# Estas líneas serán ejecutadas después de que los threads hayan terminado\n",
    "for _ in range(6):\n",
    "    print(\"Minions: 🎵🎶🎵🎶🎵🎶🎵🎶🎵🎶🎵🎶🎵🎶🎵🎶\")\n",
    "    time.sleep(1)\n",
    "    \n",
    "print(\"\\nMinions: Ojalá no nos hayan escuchado...\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "🤔 **¿Qué ocurre si tenemos lo siguiente?**\n",
    "\n",
    "```python\n",
    "thread_1.start()\n",
    "thread_1.join()\n",
    "thread_2.start()\n",
    "thread_2.join()\n",
    "thread_3.start()\n",
    "thread_3.join()\n",
    "```\n",
    "\n",
    "¿Tendremos multiples *threads* ejecutandose? 🤔 🤔\n",
    "\n",
    "La respuesta es **no**. En este código, se está empezando un *thread* y se esta esperando que finalice (`.join()`) antes de continuar con la siguiente linea de código que se encarga de empezar el siguiente *thread*. Cuando trabajamos con *threads* hay que tener mucho cuidado en la ubicación de los `.start()` y los `.join()`. El comportamiento estándar sería empezar todos los *threads* y luego comenzar con los `join()` para esperarlos."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### `is_alive()`"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Es posible identificar si un *thread* todavía está en funcionamiento mediante el uso del método `is_alive()`. Por lo general este método se implementa para saber el estado del *thread* después del uso de `join()`, al cual se le ha definido su tiempo máximo de espera."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\tNaruto tiene sueño...\n",
      "\tGoku tiene sueño...\n",
      "\tKomi tiene sueño...\n",
      "\tAnya tiene sueño...\n",
      "\tChisato tiene sueño...\n",
      "\n",
      "Minions: Los protagonistas se fueron a dormir...\n",
      "\t(Naruto: 1 oveja)\n",
      "\t(Goku: 1 oveja)\n",
      "\t(Komi: 1 liebre)\n",
      "\t(Komi: 2 liebres)\n",
      "\t(Anya: 1 liebre)\n",
      "\t(Anya: 2 liebres)\n",
      "\t(Chisato: 1 liebre)\n",
      "\t(Chisato: 2 liebres)\n",
      "\t(Naruto: 2 ovejas)\t(Komi: 3 liebres)\n",
      "\t(Komi: 4 liebres)\n",
      "\n",
      "\t(Goku: 2 ovejas)\n",
      "\tGoku a dormir...\n",
      "\tGoku se durmió después de 2.009692907333374 seg.\n",
      "\t(Anya: 3 liebres)\n",
      "\t(Anya: 4 liebres)\n",
      "\t(Chisato: 3 liebres)\n",
      "\t(Chisato: 4 liebres)\n",
      "Minions: ¡GOKU SE DURMIÓ!\n",
      "\t(Komi: 5 liebres)\t(Naruto: 3 ovejas)\n",
      "\t(Anya: 5 liebres)\n",
      "\t(Anya: 6 liebres)\n",
      "\t(Chisato: 5 liebres)\n",
      "\t(Chisato: 6 liebres)\n",
      "\n",
      "\tKomi a dormir...\n",
      "\tKomi se durmió después de 3.0137557983398438 seg.\n",
      "\t(Anya: 7 liebres)\t(Naruto: 4 ovejas)\n",
      "\t(Chisato: 7 liebres)\n",
      "\t(Chisato: 8 liebres)\n",
      "\n",
      "\tAnya a dormir...\n",
      "\tAnya se durmió después de 4.019599914550781 seg.\n",
      "\t(Chisato: 9 liebres)\n",
      "\t(Chisato: 10 liebres)\n",
      "\t(Naruto: 5 ovejas)\n",
      "\tNaruto a dormir...\n",
      "\tNaruto se durmió después de 5.027347087860107 seg.\n",
      "Minions: ¡NARUTO SE DURMIÓ!\n",
      "Minions: ¡KOMI SE DURMIÓ!\n",
      "Minions: ¡ANYA SE DURMIÓ!\n",
      "\t(Chisato: 11 liebres)\n",
      "\t(Chisato: 12 liebres)\n",
      "Minions: Chisato sigue despierta 😞. A la casa amigos!!!\n",
      "\t(Chisato: 13 liebres)\n",
      "\t(Chisato: 14 liebres)\n",
      "\t(Chisato: 15 liebres)\n",
      "\t(Chisato: 16 liebres)\n",
      "\t(Chisato: 17 liebres)\n",
      "\t(Chisato: 18 liebres)\n",
      "\t(Chisato: 19 liebres)\n",
      "\t(Chisato: 20 liebres)\n",
      "\tChisato a dormir...\n",
      "\tChisato se durmió después de 10.046483993530273 seg.\n"
     ]
    }
   ],
   "source": [
    "import threading\n",
    "import time\n",
    "\n",
    "\n",
    "# Usamos la definicion de los Thread declarados en el ejemplo anterior\n",
    "# Se crean los threads usando la clase Thread.\n",
    "naruto = CuentaOvejas(\"Naruto\", 5)\n",
    "goku = CuentaOvejas(\"Goku\", 2)\n",
    "komi = CuentaLiebres(\"Komi\", 5)\n",
    "anya = CuentaLiebres(\"Anya\", 7)\n",
    "chisato = CuentaLiebres(\"Chisato\", 20)\n",
    "\n",
    "# Se inicializan los threads creados\n",
    "naruto.start()\n",
    "goku.start()\n",
    "komi.start()\n",
    "anya.start()\n",
    "chisato.start()\n",
    "\n",
    "print(\"\\nMinions: Los protagonistas se fueron a dormir...\")\n",
    "\n",
    "# Aquí incorporamos el método join() para bloquear el programa principal\n",
    "goku.join()  # No especificamos timeout, esperará lo que sea necesario\n",
    "print(\"Minions: ¡GOKU SE DURMIÓ!\")\n",
    "\n",
    "naruto.join() # No especificamos timeout, esperará lo que sea necesario\n",
    "print(\"Minions: ¡NARUTO SE DURMIÓ!\")\n",
    "\n",
    "komi.join() # No especificamos timeout, esperará lo que sea necesario\n",
    "print(\"Minions: ¡KOMI SE DURMIÓ!\")\n",
    "\n",
    "anya.join() # No especificamos timeout, esperará lo que sea necesario\n",
    "print(\"Minions: ¡ANYA SE DURMIÓ!\")\n",
    "\n",
    "chisato.join(1)  # Esperaremos máximo 1 segundo después del último dormido, ya es muy tarde\n",
    "\n",
    "if chisato.is_alive():\n",
    "    print(\"Minions: Chisato sigue despierta 😞. A la casa amigos!!!\")\n",
    "else:\n",
    "    print(\"Minions: ¡Todos los protagonistas se durmieron! ¡A festejar!\")\n",
    "    for i in range(6):\n",
    "        print(\"Ayudantes: 🎵🎶🎵🎶🎵🎶🎵🎶🎵🎶🎵🎶🎵🎶🎵🎶\")\n",
    "        time.sleep(1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(Minions vuelven)\n",
      "Minions: Esperemos un poco más...\n",
      "Minions: ¡Goku se quedó dormid@!\n",
      "Minions: ¡Chisato se quedó dormid@!\n",
      "Minions: ¡Komi se quedó dormid@!\n",
      "Minions: ¡Naruto se quedó dormid@!\n",
      "Minions: ¡Anya se quedó dormid@!\n"
     ]
    }
   ],
   "source": [
    "print(\"(Minions vuelven)\")\n",
    "print(\"Minions: Esperemos un poco más...\")\n",
    "time.sleep(2)\n",
    "for protagonista in [goku, chisato, komi, naruto, anya]:\n",
    "    if protagonista.is_alive():\n",
    "        print(f\"Minions: {protagonista.name} aún está despiert@ 😞\")\n",
    "    else:\n",
    "        print(f\"Minions: ¡{protagonista.name} se quedó dormid@!\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### *Daemons*\n",
    "\n",
    "En los ejemplos anteriores, el programa espera a que todos los *threads* terminen su trabajo antes de terminar su ejecución. Los ***daemon threads*** son aquellos que, **a pesar de que estén aún corriendo, no impiden que el programa principal termine**.\n",
    "\n",
    "Si no existieran los *daemon threads*, tendríamos que preocuparnos de tener en cuenta todos nuestros *threads* y preocuparnos de terminarlos a todos para que el programa principal pueda terminar. Con los *daemon threads*, podemos ponerlos a correr y olvidarnos de ellos, y cuando el programa termina, los *daemon threads* serán terminados inmediatamente. El programa principal terminará cuando todos los *threads no-daemon* hayan finalizado.\n",
    "\n",
    "Para especificar que un *thread* debe ser *daemon*, se especifica el parámetro `daemon=True` en el constructor del *thread*. Una forma alternativa es escribir el atributo `daemon` del *thread*, después de haberlo creado.\n",
    "\n",
    "#### *Warning*: este notebook no refleja el efecto de *daemon*.\n",
    "\n",
    "Debido al funcionamiento interno de los Jupyter Notebooks, no se puede notar el efecto de utilizar esta propiedad de los *threads* dentro en las celdas del *notebook*. Es por esto que el siguiente código se encuentra en el archivo adjunto `daemon_threads.py`, ubicado en la carpeta `scripts`, para ser ejecutado **fuera de este *notebook*, desde una consola**.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```python\n",
    "import threading\n",
    "import time\n",
    "\n",
    "\n",
    "def dormilon() -> None:\n",
    "    print(f\"{threading.current_thread().name} tiene sueño...\")\n",
    "    time.sleep(2)\n",
    "    print(f\"{threading.current_thread().name} se durmió.\")\n",
    "\n",
    "    \n",
    "def con_insomnio() -> None:\n",
    "    print(f\"{threading.current_thread().name} tiene sueño...\")\n",
    "    time.sleep(10)\n",
    "    print(f\"{threading.current_thread().name} se durmió.\")\n",
    "\n",
    "\n",
    "# Forma 1 de hacer un thread daemon\n",
    "dormilon = threading.Thread(name=\"Dormilón\", target=dormilon, daemon=True)\n",
    "# Forma 2 de hacer un thread daemon\n",
    "con_insomnio = threading.Thread(name=\"Con insomnio\", target=con_insomnio)\n",
    "con_insomnio.daemon = True\n",
    "\n",
    "# Se inicializan los threads\n",
    "dormilon.start()\n",
    "con_insomnio.start()\n",
    "\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La siguiente figura muestra la salida en la consola para el programa **con** los *threads* configurados como *daemon*. Los *threads* se interrumpen abruptamente, luego de que el programa principal termina su ejecución.\n",
    "\n",
    "![daemon](imgs/daemon.png)\n",
    "\n",
    "Comparemos el resultado anterior con la salida en la consola para el programa **sin** los *threads* configurados como *daemon* (`daemon=False`), como muestra la imagen siguiente. Podemos ver que la ejecución de los *threads* fue completada y que el programa no se cierra hasta que los *threads* terminan.\n",
    "\n",
    "![non_daemon](imgs/non-daemon.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En la penúltima imagen, podemos apreciar que el programa principal termina antes que los *daemon threads*. Es posible hacer que el programa espere a un *daemon thread* mediante el uso de `join()`, lo que tendría sentido cuando debemos esperar algún resultado del *thread* para el resto del programa."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Dormilón tiene sueño...\n",
      "Con insomnio tiene sueño...\n",
      "Dormilón se durmió.\n",
      "Con insomnio se durmió.\n",
      "[02:58:16] Hola :) acabas de ejecutar el timer 2\n",
      "[02:58:19] ¡Hola!, acabas de ejecutar el timer 1\n",
      "[02:58:45] Hola :) acabas de ejecutar el timer 2\n",
      "[02:58:48] ¡Hola!, acabas de ejecutar el timer 1\n"
     ]
    }
   ],
   "source": [
    "import threading\n",
    "import time\n",
    "\n",
    "\n",
    "def dormilon() -> None:\n",
    "    print(f\"{threading.current_thread().name} tiene sueño...\")\n",
    "    time.sleep(2)\n",
    "    print(f\"{threading.current_thread().name} se durmió.\")\n",
    "\n",
    "    \n",
    "def con_insomnio() -> None:\n",
    "    print(f\"{threading.current_thread().name} tiene sueño...\")\n",
    "    time.sleep(10)\n",
    "    print(f\"{threading.current_thread().name} se durmió.\")\n",
    "\n",
    "\n",
    "# Forma 1 de hacer un thread daemon\n",
    "dormilon = threading.Thread(name=\"Dormilón\", target=dormilon, daemon=True)\n",
    "# Forma 2 de hacer un thread daemon\n",
    "con_insomnio = threading.Thread(name=\"Con insomnio\", target=con_insomnio)\n",
    "con_insomnio.daemon = True\n",
    "\n",
    "# Se inicializan los threads\n",
    "dormilon.start()\n",
    "con_insomnio.start()\n",
    "\n",
    "# Esperamos los threads.\n",
    "# Lo esperamos por una cantidad indefinida de tiempo\n",
    "dormilon.join()\n",
    "# Esperamos sólo 5 segundos\n",
    "con_insomnio.join(5)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Un aspecto importante a notar es que una vez que se ha llamado el método `start()` un *thread* no puede cambiar desde *daemon* a *no-daemon*, o vicecersa. Si tratamos de hacerlo, se lanza una excepción del tipo `RuntimeError`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Exception in thread Dormilón:\n",
      "Traceback (most recent call last):\n",
      "  File \"/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/threading.py\", line 1073, in _bootstrap_inner\n",
      "    self.run()\n",
      "  File \"/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/ipykernel/ipkernel.py\", line 766, in run_closure\n",
      "    _threading_Thread_run(self)\n",
      "  File \"/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/threading.py\", line 1010, in run\n",
      "    self._target(*self._args, **self._kwargs)\n",
      "TypeError: 'Thread' object is not callable\n"
     ]
    },
    {
     "ename": "RuntimeError",
     "evalue": "cannot set daemon status of active thread",
     "output_type": "error",
     "traceback": [
      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[0;31mRuntimeError\u001b[0m                              Traceback (most recent call last)",
      "Cell \u001b[0;32mIn[7], line 5\u001b[0m\n\u001b[1;32m      2\u001b[0m dormilon\u001b[38;5;241m.\u001b[39mstart()\n\u001b[1;32m      4\u001b[0m \u001b[38;5;66;03m# Noo!\u001b[39;00m\n\u001b[0;32m----> 5\u001b[0m \u001b[43mdormilon\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdaemon\u001b[49m \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mFalse\u001b[39;00m\n\u001b[1;32m      7\u001b[0m dormilon\u001b[38;5;241m.\u001b[39mjoin()\n",
      "File \u001b[0;32m/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/threading.py:1256\u001b[0m, in \u001b[0;36mThread.daemon\u001b[0;34m(self, daemonic)\u001b[0m\n\u001b[1;32m   1254\u001b[0m     \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mRuntimeError\u001b[39;00m(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mdaemon threads are disabled in this interpreter\u001b[39m\u001b[38;5;124m'\u001b[39m)\n\u001b[1;32m   1255\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_started\u001b[38;5;241m.\u001b[39mis_set():\n\u001b[0;32m-> 1256\u001b[0m     \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mRuntimeError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mcannot set daemon status of active thread\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m   1257\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_daemonic \u001b[38;5;241m=\u001b[39m daemonic\n",
      "\u001b[0;31mRuntimeError\u001b[0m: cannot set daemon status of active thread"
     ]
    }
   ],
   "source": [
    "dormilon = threading.Thread(name=\"Dormilón\", target=dormilon, daemon=True)\n",
    "dormilon.start()\n",
    "\n",
    "# Noo!\n",
    "dormilon.daemon = False\n",
    "\n",
    "dormilon.join()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Al hacer una subclase de `Thread`, que queremos que sea *daemon*, podemos indicar que el *thread* es *daemon* durante su inicialización, usando el atributo `daemon`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Daemon thread: Empezando...\n",
      "Daemon thread: Terminando...\n"
     ]
    }
   ],
   "source": [
    "class Daemon(threading.Thread):\n",
    "    \n",
    "    def __init__(self) -> None:\n",
    "        super().__init__()\n",
    "        # Cuando inicializamos el thread lo declaramos como daemon\n",
    "        self.daemon = True\n",
    "    \n",
    "    def run(self):\n",
    "        print(\"Daemon thread: Empezando...\")\n",
    "        time.sleep(2)\n",
    "        print(\"Daemon thread: Terminando...\")\n",
    "\n",
    "        \n",
    "daemon = Daemon()\n",
    "daemon.start()\n",
    "daemon.join()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahora que ya entendemos que un thread daemon se puede interrumpir su ejecución si el programa principal se acaba. Vamos a revisar el siguiente código que está en `daemon_threads_print_error`, dentro de la carpeta `scripts`.\n",
    "\n",
    "```python\n",
    "from threading import Thread\n",
    "import time\n",
    "\n",
    "def presentar(i): \n",
    "    print(\"Hola soy el thread {}\".format(i, i))\n",
    "\n",
    "def main():\n",
    "    # Crear threads\n",
    "    threads = []\n",
    "    for i in range(25):\n",
    "        threads.append(Thread(target=presentar, daemon=True, args=(i+1,)))\n",
    "\n",
    "    # Empezar los threads\n",
    "    for thread in threads:\n",
    "        thread.start()\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "    print(\"Empezar programa, crearé 25 threads\")\n",
    "    main()\n",
    "```\n",
    "\n",
    "En este código, se crean 25 *thread daemon* los cuales se van a presentar y se acaba todo. Si ejecutamos este archivo en consola, un posible resultado es:\n",
    "\n",
    "\n",
    "![daemon_error](imgs/daemon_error.png)\n",
    "\n",
    "La última parte da miedo... ¿que es eso de `Fatal Python error`? ¿por qué no vemos el `print` del **thread 23**?\n",
    "\n",
    "Este error se debe a que un *thread* estaba junto en proceso de imprimir el string en consola cuando fue interrumpido por el programa principal. Dado que imprimir en consola implica enviarle al sistema operativo una acción, se genera este error porque fue gatillada una interrumpición al sistema operativo por parte del hilo princiapl. Este error es normal que ocurra si es que estamos dejando que algún *daemon thread* maneje `prints` y luego cortamos el programa principal de forma abrupta."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## *Timers*\n",
    "\n",
    "La clase `Timer` es una subclase de la clase `Thread` y permite **ejecutar un proceso o acción después que ha pasado un determinado tiempo**. Es decir, al ser ejecutado, espera el tiempo que se le especifica y luego ejecuta las instrucciones determinadas. `Timer` requiere, como parámetros básicos, el tiempo en segundos después del cual debe ejecutarse el *thread*, la función a ejecutar, y los argumentos de la función a ejecutar.\n",
    "\n",
    "#### *Warning*: este notebook podría no reflejar el efecto de un *Timer*. Recomendamos crear un archivo .py con el código a continuación y ejecutarlo para ver el efecto esperado.\n",
    "\n",
    "Según la versión de jupyter notebook que dispongan, puede que no se muestre el efecto de utilizar esta propiedad de los *threads* en las celdas del notebook, es decir, que los *threads* no esperen el tiempo necesario antes de ejecutarse. En caso que ocurra eso, recomendamos probar el código de la siguiente celda en una consola y ejecutar desde ahí.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Se han creado 2 Timers\n"
     ]
    }
   ],
   "source": [
    "from os.path import join\n",
    "import time\n",
    "\n",
    "def tiempo_actual() -> str:\n",
    "    return time.strftime(\"%H:%M:%S\")\n",
    "\n",
    "\n",
    "def mi_timer(ruta_archivo: str) -> None:\n",
    "    with open(ruta_archivo) as archivo:\n",
    "        for linea in archivo:\n",
    "            print(f\"[{tiempo_actual()}] {linea.strip()}\")\n",
    "\n",
    "t1 = threading.Timer(4.0, mi_timer, args=(join(\"files\", \"mensaje_01.txt\"), ))\n",
    "t2 = threading.Timer(1.0, mi_timer, kwargs={\"ruta_archivo\": join(\"files\", \"mensaje_02.txt\")})\n",
    "print(\"Se han creado 2 Timers\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[02:58:15] Activando timer 1 que comenzará luego de 4 segundos\n",
      "[02:58:15] Activando timer 2 que comenzará luego de 1 segundos\n"
     ]
    }
   ],
   "source": [
    "\n",
    "print(f\"[{tiempo_actual()}] Activando timer 1 que comenzará luego de 4 segundos\")\n",
    "t1.start() # el thread t1 comenzará después de 4 seconds\n",
    "\n",
    "print(f\"[{tiempo_actual()}] Activando timer 2 que comenzará luego de 1 segundos\")\n",
    "t2.start() # el thread t2 comenzará después de 1 seconds"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Una ventaja del Timer es que, una vez hecho el `.start()`, podemos cancelar la ejecución del `Timer` con el método `.cancel()`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[02:58:23] Activando timer 1 que comenzará luego de 4 segundos\n",
      "[02:58:26] No vamos ver nada del contenido del timer 1 🙅‍♂️\n"
     ]
    }
   ],
   "source": [
    "t1 = threading.Timer(4.0, mi_timer, args=(\"files/mensaje_01.txt\",))\n",
    "\n",
    "print(f\"[{tiempo_actual()}] Activando timer 1 que comenzará luego de 4 segundos\")\n",
    "t1.start() # el thread t1 comenzará después de 4 seconds\n",
    "\n",
    "time.sleep(3)\n",
    "\n",
    "print(f\"[{tiempo_actual()}] No vamos ver nada del contenido del timer 1 🙅‍♂️\")\n",
    "t1.cancel()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cómo se mencionó antes, un `Timer` es solo un thread que espera una cantidad de tiempo antes de empezar. Por lo tanto, podemos simular este comportamiento con la misma clase `Thread`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[02:58:44] Activando timer 1 que comenzará luego de 4 segundos\n",
      "[02:58:44] Activando timer 2 que comenzará luego de 1 segundos\n"
     ]
    }
   ],
   "source": [
    "import time\n",
    "\n",
    "def tiempo_actual() -> str:\n",
    "    return time.strftime(\"%H:%M:%S\")\n",
    "    \n",
    "class MiTimer(threading.Thread):\n",
    "    \n",
    "    def __init__(self, tiempo_espera: int, ruta_archivo: str) -> None:\n",
    "        super().__init__()\n",
    "        self.tiempo_espera = tiempo_espera\n",
    "        self.ruta_archivo = ruta_archivo\n",
    "        self._cancel = False\n",
    "\n",
    "    def cancel(self) -> None:\n",
    "        self._cancel = True\n",
    "    \n",
    "    def run(self) -> None:\n",
    "        time.sleep(self.tiempo_espera)\n",
    "        if self._cancel == True:\n",
    "            return\n",
    "            \n",
    "        with open(self.ruta_archivo) as archivo:\n",
    "            for linea in archivo:\n",
    "                print(f\"[{tiempo_actual()}] {linea.strip()}\")\n",
    "        \n",
    "        \n",
    "t1 = MiTimer(4, \"files/mensaje_01.txt\")\n",
    "t2 = MiTimer(1, ruta_archivo = \"files/mensaje_02.txt\")\n",
    "\n",
    "print(f\"[{tiempo_actual()}] Activando timer 1 que comenzará luego de 4 segundos\")\n",
    "t1.start() # el thread t1 comenzará después de 4 seconds\n",
    "\n",
    "print(f\"[{tiempo_actual()}] Activando timer 2 que comenzará luego de 1 segundos\")\n",
    "t2.start() # el thread t2 comenzará después de 1 seconds"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[02:58:50] Activando timer 1 que comenzará luego de 4 segundos\n",
      "[02:58:50] Activando timer 2 que comenzará luego de 1 segundos\n",
      "[02:58:50] Vamos a cancelar la ejecución de los 2 timers\n"
     ]
    }
   ],
   "source": [
    "t1 = MiTimer(4, \"files/mensaje_01.txt\")\n",
    "t2 = MiTimer(1, ruta_archivo = \"files/mensaje_02.txt\")\n",
    "\n",
    "print(f\"[{tiempo_actual()}] Activando timer 1 que comenzará luego de 4 segundos\")\n",
    "t1.start() # el thread t1 comenzará después de 4 seconds\n",
    "\n",
    "print(f\"[{tiempo_actual()}] Activando timer 2 que comenzará luego de 1 segundos\")\n",
    "t2.start() # el thread t2 comenzará después de 1 seconds\n",
    "\n",
    "print(f\"[{tiempo_actual()}] Vamos a cancelar la ejecución de los 2 timers\")\n",
    "t1.cancel()\n",
    "t2.cancel()"
   ]
  }
 ],
 "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
}
