{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<p>\n",
    "<font size='5' face='Georgia, Arial'>IIC-2233 Apunte Programación Avanzada</font><br>\n",
    "<font size='1'> Equipo Docente IIC2233 2026</font>\n",
    "</p>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Tabla de contenidos\n",
    "\n",
    "1. [Ejecución de código de ejemplo del material que usa PyQt](#Ejecución-de-código-de-ejemplo-del-material-que-usa-PyQt)\n",
    "2. [*Threads* y PyQt](#Threads-y-PyQt)\n",
    "3. [*QThread*](#QThread)\n",
    "    1. [*QThread* con su propia señal](#QThread-con-su-propia-señal)\n",
    "    2. [Precaución con \"donde\" se conecta una señal](#Precaución-con-\"donde\"-se-conecta-una-señal)\n",
    "    3. [Múltiples *QThread*](#Múltiples-QThread)\n",
    "    4. [*QMutex*](#QMutex)\n",
    "5. [Ejemplo Aplicado](#Ejemplo-Aplicado)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejecución de código de ejemplo del material que usa PyQt\n",
    "\n",
    "Lamentablemente, Jupyter no fue creado con la intención de ejecutar código de GUI de escritorio, por lo que se hace difícil ejecutar e interactuar con las interfaces a través de esta herramienta. Para entender los ejemplos de código relacionados con PyQt, **recomendamos fuertemente** que ejecutes los *scripts* de interfaces gráficas desde tu propio computador, y **NO** en este *notebook*. Para esto, se agregó una carpeta llamada `scripts/` que tiene cada código que se mostrará en este y los siguientes _notebooks_ de esta semana.\n",
    "\n",
    "**Importante:** Al momento de ejecutar el código, asegúrate de que tu terminal esté posicionada en el mismo directorio donde está el archivo `.py` a ejecutar."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## *Threads* y PyQt"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esta semana pasaremos de una programación secuencial con un único hilo (*thread*) de ejecución con solo interacción por consola y archivos, a un modelo *multithreaded* orientado a interactuar con el usuario mediante distintos tipos de *inputs* gráficos.\n",
    "\n",
    "El uso de *theading* en conjunto a PyQt es absolutamente compatible, y, si queremos hacer programas de mayor complejidad, a veces es necesario.\n",
    "\n",
    "Al igual que para programas con un solo *thread*, podemos hacer uso de señales personalizadas en una aplicación *multithreaded*. El siguiente ejemplo muestra cómo crear una señal que controla la acción del *thread* sobre el formulario mediante el método `actualizar_labels()`. El *thread* por su parte, recibe como parámetro la señal y emite mensajes.\n",
    "\n",
    "Este código se encuentra en el archivo `1-pyqt-qthreads/1_thread.py`"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```python\n",
    "import sys\n",
    "from threading import Thread\n",
    "from time import sleep\n",
    "from PyQt6.QtCore import pyqtSignal\n",
    "from PyQt6.QtWidgets import QApplication, QWidget, QLabel, QPushButton\n",
    "\n",
    "\n",
    "class MiThread(Thread):\n",
    "    \"\"\"\n",
    "    Esta clase representa un thread personalizado que será utilizado durante\n",
    "    la ejecución de la GUI.\n",
    "    \"\"\"\n",
    "\n",
    "    def __init__(self, senal_actualizar: pyqtSignal) -> None:\n",
    "        super().__init__()\n",
    "        self.senal_actualizar = senal_actualizar\n",
    "\n",
    "    def run(self) -> None:\n",
    "        for i in range(10):\n",
    "            sleep(0.5)\n",
    "            self.senal_actualizar.emit(str(i))\n",
    "        self.senal_actualizar.emit(\"Status: thread terminado\")\n",
    "\n",
    "\n",
    "class MiVentana(QWidget):\n",
    "    # Creamos una señal para manejar la respuesta del thread\n",
    "    senal_thread = pyqtSignal(str)\n",
    "\n",
    "    def __init__(self) -> None:\n",
    "        super().__init__()\n",
    "        self.thread = None\n",
    "        # Conectamos la señal al método que maneja\n",
    "        self.senal_thread.connect(self.actualizar_label)\n",
    "        self.init_gui()\n",
    "\n",
    "    def init_gui(self) -> None:\n",
    "        # Configuramos los widgets de la interfaz\n",
    "        self.label = QLabel(\"Status: esperando thread\", self)\n",
    "        self.boton = QPushButton(\"Ejecutar Thread\", self)\n",
    "        self.boton.clicked.connect(self.ejecutar_thread)\n",
    "\n",
    "        self.label.setGeometry(10, 10, 230, 30)\n",
    "        self.boton.setGeometry(10, 50, 230, 30)\n",
    "\n",
    "        # Configuramos las propiedades de la ventana.\n",
    "        self.setWindowTitle(\"Ejemplo thread\")\n",
    "        self.setGeometry(50, 50, 250, 200)\n",
    "        self.show()\n",
    "\n",
    "    def ejecutar_thread(self) -> None:\n",
    "        \"\"\"\n",
    "        Este método crea un thread cada vez que se presiona el botón en la\n",
    "        interfaz. El thread recibirá como argumento la señal sobre la cual\n",
    "        debe operar.\n",
    "        \"\"\"\n",
    "        if self.thread is None or not self.thread.is_alive():\n",
    "            self.thread = MiThread(self.senal_thread)\n",
    "            self.thread.start()\n",
    "\n",
    "    def actualizar_label(self, texto: str) -> None:\n",
    "        \"\"\"\n",
    "        Este método actualiza el label según los datos enviados desde el\n",
    "        thread a través del argumento texto. Para este ejemplo, el método\n",
    "        recibe un texto, pero podría también no recibir nada.\n",
    "        \"\"\"\n",
    "        self.label.setText(texto)\n",
    "\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "\n",
    "    def hook(type_, value, traceback):\n",
    "        print(type_)\n",
    "        print(traceback)\n",
    "\n",
    "    sys.__excepthook__ = hook\n",
    "    app = QApplication([])\n",
    "    ventana = MiVentana()\n",
    "    sys.exit(app.exec())\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con esto, y lo que sabemos del módulo `threading`, nos basta para generar comportamiento *multithread* en conjunto con nuestras interfaces, pero hay más. Debido a que PyQt se construye sobre la **arquitectura basada en manejo de eventos**, *threads* comunes y corrientes pueden generar problemas de concurrencia con esta arquitectura, por lo que PyQt provee su propia implementación de *threads*: el `QThread`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## QThread"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En términos simples, es un `Thread` como los del módulo `threading`. Provee prácticamente las mismas funcionalidades que ya conocemos, pero está creado dentro del ambiente de PyQt, por lo que es mucho más compatible con su arquitectura interna. Es parte del módulo `PyQt6.QtCore`, por lo que deberás importarlo desde ese módulo. A continuación se muestra una recreación del ejemplo anterior utilizando `QThread` en vez de `Thread`.\n",
    "\n",
    "Este código se encuentra en el archivo `1-pyqt-qthreads/2_qthread.py`"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```python\n",
    "import sys\n",
    "from time import sleep\n",
    "from PyQt6.QtCore import pyqtSignal, QThread\n",
    "from PyQt6.QtWidgets import QApplication, QWidget, QLabel, QPushButton\n",
    "\n",
    "\n",
    "class MiThread(QThread):\n",
    "    \"\"\"\n",
    "    Esta clase representa un thread personalizado que será utilizado durante\n",
    "    la ejecución de la GUI.\n",
    "    \"\"\"\n",
    "\n",
    "    def __init__(self, senal_actualizar: pyqtSignal) -> None:\n",
    "        super().__init__()\n",
    "        self.senal_actualizar = senal_actualizar\n",
    "\n",
    "    def run(self) -> None:\n",
    "        for i in range(10):\n",
    "            sleep(0.5)\n",
    "            self.senal_actualizar.emit(str(i))\n",
    "\n",
    "        sleep(0.5)\n",
    "        self.senal_actualizar.emit(\"Status: Qthread terminado\")\n",
    "\n",
    "\n",
    "class MiVentana(QWidget):\n",
    "    # Creamos una señal para manejar la respuesta del thread\n",
    "    senal_thread = pyqtSignal(str)\n",
    "\n",
    "    def __init__(self) -> None:\n",
    "        super().__init__()\n",
    "        self.thread = None\n",
    "        # Conectamos la señal del thread al método que maneja\n",
    "        self.senal_thread.connect(self.actualizar_label)\n",
    "\n",
    "        self.init_gui()\n",
    "\n",
    "    def init_gui(self) -> None:\n",
    "        # Configuramos los widgets de la interfaz\n",
    "        self.label = QLabel(\"Status: esperando Qthread\", self)\n",
    "        self.boton = QPushButton(\"Ejecutar QThread\", self)\n",
    "        self.boton.clicked.connect(self.ejecutar_thread)\n",
    "\n",
    "        self.label.setGeometry(10, 10, 230, 30)\n",
    "        self.boton.setGeometry(10, 50, 230, 30)\n",
    "\n",
    "        # Configuramos las propiedades de la ventana.\n",
    "        self.setWindowTitle(\"Ejemplo Qthread\")\n",
    "        self.setGeometry(50, 50, 250, 200)\n",
    "        self.show()\n",
    "\n",
    "    def ejecutar_thread(self) -> None:\n",
    "        \"\"\"\n",
    "        Este método crea un thread cada vez que se presiona el botón en la\n",
    "        interfaz. El thread recibirá como argumento la señal sobre la cual\n",
    "        debe operar.\n",
    "        \"\"\"\n",
    "        # Aquí debemos ocupar isRunning en lugar de is_alive\n",
    "        if self.thread is None or not self.thread.isRunning():\n",
    "            self.thread = MiThread(self.senal_thread)\n",
    "            self.thread.start()\n",
    "\n",
    "    def actualizar_label(self, texto: str) -> None:\n",
    "        \"\"\"\n",
    "        Este método actualiza el label según los datos enviados desde el\n",
    "        thread a través del argumento texto. Para este ejemplo, el método\n",
    "        recibe un texto, pero podría también no recibir nada.\n",
    "        \"\"\"\n",
    "        self.label.setText(texto)\n",
    "\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "\n",
    "    def hook(type_, value, traceback):\n",
    "        print(type_)\n",
    "        print(traceback)\n",
    "\n",
    "    sys.__excepthook__ = hook\n",
    "    app = QApplication([])\n",
    "    ventana = MiVentana()\n",
    "    sys.exit(app.exec())\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Se puede apreciar que se define de la misma forma que un `Thread`: se llama a `__init__` de la superclase de la cual se hereda, se define el método `run` que define el comportamiento al ser ejecutado, y este se ejecuta al llamar el método `start`. Una diferencia leve que se puede notar es que el método `is_alive` no existe en `QThread`, pero existe un equivalente llamado `isRunning`. En general, se puede encontrar un equivalente para todo método de `Thread` en `QThread`, y se pueden encontrar buscando documentación de la clase."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### QThread con su propia señal\n",
    "\n",
    "Una ventaja que tiene el uso de `QThreads` es que ellos pueden tener sus propias señales definidas como atributos de clases. Esto **no es posible con Thread** dado que la creación de señales es exclusivo para objetos que sean parte del ecosistema PyQt. En el siguiente código se replicará el mismo ejemplo anterior, pero ahora la señal estará definida dentro de `MiThread(Qthread)` en vez de `MiVentana(QWidget)`. La única diferencia es que, una vez creado un `MiThread`, hay que conectar su señal con el método específico. Este código se encuentra en el archivo `1-pyqt-qthreads/3_qthread_con_signal.py`"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```python\n",
    "import sys\n",
    "from time import sleep\n",
    "from PyQt6.QtCore import pyqtSignal, QThread\n",
    "from PyQt6.QtWidgets import QApplication, QWidget, QLabel, QPushButton\n",
    "\n",
    "\n",
    "class MiThread(QThread):\n",
    "    \"\"\"\n",
    "    Esta clase representa un thread personalizado que será utilizado durante\n",
    "    la ejecución de la GUI.\n",
    "    \"\"\"\n",
    "\n",
    "    # Creamos una señal para manejar la respuesta del thread\n",
    "    senal_thread = pyqtSignal(str)\n",
    "\n",
    "    def __init__(self) -> None:\n",
    "        super().__init__()\n",
    "\n",
    "    def run(self) -> None:\n",
    "        for i in range(10):\n",
    "            sleep(0.5)\n",
    "            self.senal_thread.emit(str(i))\n",
    "\n",
    "        sleep(0.5)\n",
    "        self.senal_thread.emit(\"Status: Qthread terminado\")\n",
    "\n",
    "\n",
    "class MiVentana(QWidget):\n",
    "\n",
    "    def __init__(self) -> None:\n",
    "        super().__init__()\n",
    "        self.thread = None\n",
    "        self.init_gui()\n",
    "\n",
    "    def init_gui(self) -> None:\n",
    "        # Configuramos los widgets de la interfaz\n",
    "        self.label = QLabel(\"Status: esperando Qthread\", self)\n",
    "        self.boton = QPushButton(\"Ejecutar QThread\", self)\n",
    "        self.boton.clicked.connect(self.ejecutar_thread)\n",
    "\n",
    "        self.label.setGeometry(10, 10, 230, 30)\n",
    "        self.boton.setGeometry(10, 50, 230, 30)\n",
    "\n",
    "        # Configuramos las propiedades de la ventana.\n",
    "        self.setWindowTitle(\"Ejemplo Qthread\")\n",
    "        self.setGeometry(50, 50, 250, 200)\n",
    "        self.show()\n",
    "\n",
    "    def ejecutar_thread(self) -> None:\n",
    "        \"\"\"\n",
    "        Este método crea un thread cada vez que se presiona el botón en la\n",
    "        interfaz. El thread recibirá como argumento la señal sobre la cual\n",
    "        debe operar.\n",
    "        \"\"\"\n",
    "        # Aquí debemos ocupar isRunning en lugar de is_alive\n",
    "        if self.thread is None or not self.thread.isRunning():\n",
    "            self.thread = MiThread()\n",
    "            # Conectamos la señal del thread al método que maneja\n",
    "            self.thread.senal_thread.connect(self.actualizar_label)\n",
    "            self.thread.start()\n",
    "\n",
    "    def actualizar_label(self, texto: str) -> None:\n",
    "        \"\"\"\n",
    "        Este método actualiza el label según los datos enviados desde el\n",
    "        thread a través del argumento texto. Para este ejemplo, el método\n",
    "        recibe un texto, pero podría también no recibir nada.\n",
    "        \"\"\"\n",
    "        self.label.setText(texto)\n",
    "\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "\n",
    "    def hook(type_, value, traceback):\n",
    "        print(type_)\n",
    "        print(traceback)\n",
    "\n",
    "    sys.__excepthook__ = hook\n",
    "    app = QApplication([])\n",
    "    ventana = MiVentana()\n",
    "    sys.exit(app.exec())\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Precaución con \"donde\" se conecta una señal\n",
    "\n",
    "En el siguiente ejemplo, vamos a hacer que el código principal defina y ejecute un `Thread` (T1) y que dicho `Thread` defina y ejecute otro `Thread` (T2). Por lo tanto, tendremos 3 hilos: el principal, el de T1 y T2. Luego, vamos a conectar una señal en el hilo principal y otra en el hilo de T1. Finalmente, vamos a hacer que tanto T1 y T2 ejecuten las 2 señales. ¿Se ejecutarán todas como esperamos?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```python\n",
    "from PyQt6.QtCore import QThread, pyqtSignal\n",
    "from PyQt6.QtWidgets import QApplication\n",
    "import sys\n",
    "import time\n",
    "\n",
    "\n",
    "class Thread1(QThread):\n",
    "    signal_t1 = pyqtSignal(str)\n",
    "\n",
    "    def __init__(self):\n",
    "        super().__init__()\n",
    "        self.t2 = None\n",
    "\n",
    "    def run(self):\n",
    "        print(\"[Thread 1] Crear Thread 2\")\n",
    "        self.t2 = Thread2(self.signal_t1)\n",
    "\n",
    "        print(\"[Thread 1] Conectar Thread 2 (signar_t2) con print de T2\")\n",
    "        self.t2.signal_t2.connect(self.t2.print)\n",
    "\n",
    "        print(\"[Thread 1] Ejecutar Thread 2\")\n",
    "        self.t2.start()\n",
    "\n",
    "        print(\"[Thread 1] Emitir 2 señales: t1.signar_1 y t2.signal_2\")\n",
    "        self.signal_t1.emit(\"signal_t1 emitida desde Thread1\")\n",
    "        self.t2.signal_t2.emit(\"signal_t2 emitida desde Thread1\")\n",
    "\n",
    "        # Espero medio segundo y cierro el programa\n",
    "        time.sleep(0.5)\n",
    "        sys.exit(1)\n",
    "\n",
    "    def print(self, s):\n",
    "        print(\"\\t[Print de T1] -> \", s)\n",
    "\n",
    "\n",
    "class Thread2(QThread):\n",
    "    signal_t2 = pyqtSignal(str)\n",
    "\n",
    "    def __init__(self, signal_padre: pyqtSignal) -> None:\n",
    "        super().__init__()\n",
    "        self.signal_t1 = signal_padre\n",
    "\n",
    "    def run(self):\n",
    "        print(\"[Thread 2] Emitir 2 señales: t1.signar_1 y t2.signal_2\")\n",
    "        self.signal_t1.emit(\"signal_t1 emitida desde Thread2\")\n",
    "        self.signal_t2.emit(\"signal_t2 emitida desde Thread2\")\n",
    "\n",
    "    def print(self, s):\n",
    "        print(\"\\t[Print de T2] -> \", s)\n",
    "\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "    app = QApplication([])\n",
    "    print(\"[MAIN] Crear Thread 1\")\n",
    "    hilo = Thread1()\n",
    "    print(\"[MAIN] Conectar Thread 1 (signar_t1) con print de T1\")\n",
    "    hilo.signal_t1.connect(hilo.print)\n",
    "    print(\"[MAIN] Ejecutar T1\")\n",
    "    hilo.start()\n",
    "    sys.exit(app.exec())\n",
    "```\n",
    "\n",
    "Si ejecutamos este código, el resultado será:\n",
    "\n",
    "```cmd\n",
    "[MAIN] Crear Thread 1\n",
    "[MAIN] Conectar Thread 1 (signar_t1) con print de T1\n",
    "[MAIN] Ejecutar T1\n",
    "[Thread 1] Crear Thread 2\n",
    "[Thread 1] Conectar Thread 2 (signar_t2) con print de T2\n",
    "[Thread 1] Ejecutar Thread 2\n",
    "[Thread 1] Emitir 2 señales: t1.signar_1 y t2.signal_2\n",
    "[Thread 2] Emitir 2 señales: t1.signar_1 y t2.signal_2\n",
    "   [Print de T2] ->  signal_t2 emitida desde Thread1\n",
    "   [Print de T1] ->  signal_t1 emitida desde Thread1\n",
    "   [Print de T1] ->  signal_t1 emitida desde Thread2\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En este ejemplo, se intentó emitir 4 señales, solo se ejecutaron 3. ¿Por qué?\n",
    "\n",
    "Analizando con detalle, en el hilo principal (Main) se conectó `signal_t1` con el `print de T1`. Por lo tanto, cualquiera que emita la señal t1, logrará ejecutar el `print de T1`.\n",
    "\n",
    "Respecto a la conexión de `signal_t2`, esta **no** fue conectada en el hilo principal, sino dentro del hilo de T1, en otras palabras, dentro del \"mundo\" o \"dominio\" de T1, esa conexión existe, pero fuera de dicho dominio, no se conoce dicha conexión. Por este motivo:\n",
    "* Cuando T1 emite la señal `signal_t2`, se ejecuta el `print de T2` ya que justamente esa señal sí está conectada en el dominio de T1. \n",
    "* Cuando T2 emite la misma señal (`signal_t2`), en el dominio de T2 no existe esa conexión y tampoco es una conexión realizada en el hilo principal (Main). Por este motivo, no se ejecutó el `print de t2` cuando T2 emitió la señal `signal_t2`.\n",
    "\n",
    "Por todo lo anterior, **se recomienda que toda señal sea conectada en el hilo principal (Main) y no lo haga un _thread_ secundario.**"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "### Múltiples *QThread*\n",
    "\n",
    "Los ejemplos anteriores tenían 1 solo definido, pero ahora queremos tener una cantidad variable de `QThread` ejecutándose. Para esto, lo que debemos asegurarnos es **siempre** guardar una referencia, en memoria, al `QThread` y por lo mismo vamos a definir una lista donde se guardará cada `QThread` creado Este código se encuentra en el archivo `1-pyqt-qthreads/5_multiples_qthreads.py`\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```python\n",
    "import sys\n",
    "from time import sleep\n",
    "from PyQt6.QtCore import pyqtSignal, QThread\n",
    "from PyQt6.QtWidgets import QApplication, QWidget, QLabel, QPushButton\n",
    "\n",
    "\n",
    "class MiThread(QThread):\n",
    "    # Se define para la clase MiThread,\n",
    "    # para que cada instancia tenga una propia\n",
    "    senal_actualizar = pyqtSignal(int, str)\n",
    "\n",
    "    def __init__(self, indice: int, tiempo: float) -> None:\n",
    "        super().__init__()\n",
    "        self.indice = indice\n",
    "        self.tiempo = tiempo\n",
    "\n",
    "    def run(self) -> None:\n",
    "        for i in range(10):\n",
    "            sleep(self.tiempo)\n",
    "            self.senal_actualizar.emit(self.indice, str(i))\n",
    "\n",
    "        sleep(self.tiempo)\n",
    "        self.senal_actualizar.emit(self.indice, \"Status: QThread terminado\")\n",
    "\n",
    "\n",
    "class MiVentana(QWidget):\n",
    "    def __init__(self) -> None:\n",
    "        super().__init__()\n",
    "        self.threads = []\n",
    "        self.init_gui()\n",
    "\n",
    "    def init_gui(self) -> None:\n",
    "        # Configuramos los widgets de la interfaz\n",
    "        # Definimos un montón de labels que corresponderán a un thread cada uno\n",
    "        self.labels = {\n",
    "            i: QLabel(\"Status: esperando QThread\", self) for i in range(1, 6)\n",
    "        }\n",
    "        self.boton = QPushButton(\"Ejecutar QThreads\", self)\n",
    "        self.boton.clicked.connect(self.ejecutar_threads)\n",
    "\n",
    "        for i in range(1, 6):\n",
    "            self.labels[i].setGeometry(10, (i - 1) * 30, 330, 30)\n",
    "\n",
    "        self.boton.setGeometry(10, 150, 330, 30)\n",
    "        # Configuramos las propiedades de la ventana.\n",
    "        self.setWindowTitle(\"Ejemplo Multiples QThreads\")\n",
    "        self.setGeometry(50, 50, 350, 200)\n",
    "        self.show()\n",
    "\n",
    "    def ejecutar_threads(self) -> None:\n",
    "        \"\"\"\n",
    "        Este método crea cinco threads cada vez que se presiona el botón en la\n",
    "        interfaz. Los threads recibirán como argumento el índice del label\n",
    "        que les corresponde y el tiempo que toman entre cada iteración.\n",
    "        \"\"\"\n",
    "        for thread in self.threads:\n",
    "            if thread.isRunning():\n",
    "                return\n",
    "\n",
    "        self.threads = []\n",
    "        for i in range(1, 6):\n",
    "            thread = MiThread(i, i / 10)\n",
    "            # Se conecta la señal emitida por el thread a un método\n",
    "            # de la ventana\n",
    "            thread.senal_actualizar.connect(self.actualizar_labels)\n",
    "            self.threads.append(thread)\n",
    "            thread.start()\n",
    "\n",
    "    def actualizar_labels(self, indice: int, texto: str) -> None:\n",
    "        \"\"\"\n",
    "        Este método actualiza el label correspondiente según los datos\n",
    "        enviados desde un thread a través del índice y aplica el texto.\n",
    "        \"\"\"\n",
    "        self.labels[indice].setText(texto)\n",
    "\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "\n",
    "    def hook(type_, value, traceback):\n",
    "        print(type_)\n",
    "        print(traceback)\n",
    "\n",
    "    sys.__excepthook__ = hook\n",
    "    app = QApplication([])\n",
    "    ventana = MiVentana()\n",
    "    sys.exit(app.exec())\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### `QMutex`"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Tal como vimos anteriormente, PyQt ofrece `QThread` como una versión propia del `Thread`. Esto mismo ocurre con los _locks_. Este objeto que nos permite controlar el acceso de múltiples _threads_ a una zona crítica de código también tiene su versión en PyQt. Este objeto se llama `QMutex` y mediante los métodos `lock()` y `unlock()` podemos garantizar que cierto fragmento de código solo sea ejecutado por 1 _thread_ a la vez.\n",
    "\n",
    "A continuación mostraremos un ejemplo donde usaremos 6 _threads_ que actualizarán la ventana en diferentes tiempos. No obstante, el primer `for` que actualiza la ventana estará dentro de un `QMutex`, por lo que solo 1 _thread_ podrá ejecutar dicho código a la vez, mientras que un segundo `for`. que actualiza la ventana, no tendrá. `QMutex`. Esto hará que mientras uno o más threads estén ejecutando su segunda actualización, solo 1 podrá realizar la primera.\n",
    "\n",
    "Este código se encuentra en el archivo `1-pyqt-qthreads/6_qmutex.py`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```python\n",
    "import sys\n",
    "from time import sleep\n",
    "from PyQt6.QtCore import pyqtSignal, QThread, QMutex\n",
    "from PyQt6.QtWidgets import QApplication, QWidget, QLabel, QPushButton\n",
    "\n",
    "\n",
    "class MiThread(QThread):\n",
    "    # Se define para la clase MiThread,\n",
    "    # para que cada instancia tenga una propia\n",
    "    senal_actualizar = pyqtSignal(int, str)\n",
    "\n",
    "    def __init__(self, indice: int, tiempo: float, mutex: QMutex) -> None:\n",
    "        super().__init__()\n",
    "        self.indice = indice\n",
    "        self.tiempo = tiempo\n",
    "        self.mutex = mutex\n",
    "\n",
    "    def run(self) -> None:\n",
    "        self.mutex.lock()\n",
    "        for i in range(10):\n",
    "            sleep(self.tiempo)\n",
    "            self.senal_actualizar.emit(self.indice, str(i) + \" - crítico\")\n",
    "        self.mutex.unlock()\n",
    "\n",
    "        for i in range(50):\n",
    "            sleep(self.tiempo)\n",
    "            self.senal_actualizar.emit(self.indice, str(i) + \" - no crítico\")\n",
    "\n",
    "        sleep(self.tiempo)\n",
    "        self.senal_actualizar.emit(self.indice, \"Status: QThread terminado\")\n",
    "\n",
    "\n",
    "class MiVentana(QWidget):\n",
    "    def __init__(self) -> None:\n",
    "        super().__init__()\n",
    "        self.threads = []\n",
    "        self.mutex = QMutex()\n",
    "        self.init_gui()\n",
    "\n",
    "    def init_gui(self) -> None:\n",
    "        # Configuramos los widgets de la interfaz\n",
    "        # Definimos un montón de labels que corresponderán a un thread cada uno\n",
    "        self.labels = {\n",
    "            i: QLabel(\"Status: esperando Qthread\", self) for i in range(1, 6)\n",
    "        }\n",
    "        self.boton = QPushButton(\"Ejecutar QThreads\", self)\n",
    "        self.boton.clicked.connect(self.ejecutar_threads)\n",
    "\n",
    "        for i in range(1, 6):\n",
    "            self.labels[i].setGeometry(10, (i - 1) * 30, 230, 30)\n",
    "\n",
    "        self.boton.setGeometry(10, 150, 230, 30)\n",
    "        # Configuramos las propiedades de la ventana.\n",
    "        self.setWindowTitle(\"Ejemplo QMutex\")\n",
    "        self.setGeometry(50, 50, 250, 200)\n",
    "        self.show()\n",
    "\n",
    "    def ejecutar_threads(self) -> None:\n",
    "        \"\"\"\n",
    "        Este método crea cinco threads cada vez que se presiona el botón en la\n",
    "        interfaz. Los threads recibirán como argumento el índice del label\n",
    "        que les corresponde y el tiempo que toman entre cada iteración.\n",
    "        \"\"\"\n",
    "        for thread in self.threads:\n",
    "            if thread.isRunning():\n",
    "                return\n",
    "\n",
    "        self.threads = []\n",
    "        for i in range(1, 6):\n",
    "            thread = MiThread(i, i / 20, self.mutex)\n",
    "            # Se conecta la señal emitida por el thread a un método\n",
    "            # de la ventana\n",
    "            thread.senal_actualizar.connect(self.actualizar_labels)\n",
    "            self.threads.append(thread)\n",
    "            thread.start()\n",
    "\n",
    "    def actualizar_labels(self, indice: int, texto: str) -> None:\n",
    "        \"\"\"\n",
    "        Este método actualiza el label correspondiente según los datos\n",
    "        enviados desde un thread a través del índice y aplica el texto.\n",
    "        \"\"\"\n",
    "        self.labels[indice].setText(texto)\n",
    "\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "\n",
    "    def hook(type_, value, traceback):\n",
    "        print(type_)\n",
    "        print(traceback)\n",
    "\n",
    "    sys.__excepthook__ = hook\n",
    "    app = QApplication([])\n",
    "    ventana = MiVentana()\n",
    "    sys.exit(app.exec())\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejemplo Aplicado"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahora, vamos a aplicar conceptos de modularización, _back-end_ y _front-end_, `QThreads` para generar el siguiente ejemplo:\n",
    "\n",
    "![](img/ejemplo_aplicado.gif)\n",
    "\n",
    "\n",
    "En este ejemplo, tendremos diferentes cuadrados que van de un extremo a otro a diferente velocidad. Además, se puede hacer click en un cuadrado para cambiar su color y detener su movimiento. (Mini desafío: intenta detener todos los cuadrados).\n",
    "\n",
    "Para esto, dentro del directorio `scripts` hay un directorio llamado `ejemplo_aplicado`. En este tenemos los siguientes archivos:\n",
    "\n",
    "* `main.py`: es el archivo principal a ejecutar.\n",
    "* `parametros_general.py`: son constantes que se ocuparán tanto en _back-end_ y _front-end_.\n",
    "\n",
    "**backend**\n",
    "* `backend/logica_qthread.py`: es el archivo principal del _back-end_ donde se aplica toda la lógica del movimiento de los cuadrados. En este archivo se ocupan `QThread` para modelar cada cuadrado.\n",
    "* `backend/logica_thread.py`: es el archivo principal del _back-end_ donde se aplica toda la lógica del movimiento de los cuadrados. En este archivo se ocupan `threading.Thread` para modelar cada cuadrado. La única diferencia con `logica_qthread` es la herencia y que se debe explicitar que cada cuadrado es un _daemon_. \n",
    "* `backend/bonus_logica_qtimer.py`: es el mismo archivo que `logica_qthread.py` pero se utilizan `QTimer` en vez de `QThread`. Se recomienda ignorar este archivo para este ejemplo. Se utilizará en el contenido _bonus_.\n",
    "* `backend/parametros_backend.py`: son constantes que se ocuparán únicamente en _back-end_.\n",
    "\n",
    "**frontend**\n",
    "* `frontend/ventana.py`: es el archivo principal del _front-end_ donde se crea la ventana y todo elemento visual.\n",
    "* `frontend/parametros_backend.py`: son constantes que se ocuparán únicamente en _front-end_.\n",
    "\n",
    "Para la correcta ejecución de este archivo, debes abrir tu terminal, desplazarte con `cd` hasta el directorio `scripts/ejemplo_aplicado/` y luego escribir `python3 main.py`."
   ]
  }
 ],
 "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.11.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
