{"id":591090,"date":"2025-06-30T01:09:00","date_gmt":"2025-06-30T00:09:00","guid":{"rendered":"https:\/\/blog.jetbrains.com\/?post_type=pycharm&#038;p=591090"},"modified":"2025-08-12T01:36:06","modified_gmt":"2025-08-12T00:36:06","slug":"python-mas-rapido-simultaneidad-con-async-await-y-threading","status":"publish","type":"pycharm","link":"https:\/\/blog.jetbrains.com\/es\/pycharm\/2025\/06\/python-mas-rapido-simultaneidad-con-async-await-y-threading\/","title":{"rendered":"Python m\u00e1s r\u00e1pido: simultaneidad con async\/await y threading"},"content":{"rendered":"<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" class=\"alignnone size-full wp-image-591111\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/08\/PC-social-BlogFeatured-1280x720-2x-2.png\" alt=\"\" width=\"2560\" height=\"1440\" \/><\/figure>\n<p>Si lleva tiempo codificando con Python, y en especial si ha usado marcos de trabajo y bibliotecas como FastAPI y discord.py, probablemente haya usado <code>async\/await<\/code> o <code>asyncio<\/code>. Quiz\u00e1s haya o\u00eddo afirmaciones del tipo \u00abel multihilo en Python no existe\u00bb y quiz\u00e1s tambi\u00e9n conozca el (tristemente) famoso GIL de Python. Vista la negaci\u00f3n acerca del multihilo de Python, quiz\u00e1s se est\u00e9 preguntando cu\u00e1l es realmente la diferencia entre <code>async\/await<\/code> y el multihilo, especialmente al programar con Python. Si es as\u00ed, \u00a1este art\u00edculo es para usted!<\/p>\n<h2 id=\"what-is-multithreading\" class=\"wp-block-heading\">\u00bfQu\u00e9 es el multihilo?<\/h2>\n<p>En programaci\u00f3n, el multihilo se refiere a la capacidad de un programa para ejecutar varias tareas secuenciales (llamadas hilos o subprocesos) de forma simult\u00e1nea. Estos subprocesos pueden ejecutarse en uno o en varios n\u00facleos del procesador. Sin embargo, debido a la limitaci\u00f3n del Global Interpreter Lock (GIL), en Python el multihilo solo se procesa en un n\u00facleo. La excepci\u00f3n es nogil (tambi\u00e9n llamada thread-free) en Python, que elimina el GIL y de la cual hablaremos en la segunda parte de esta serie. En este art\u00edculo del blog, asumiremos que el GIL siempre est\u00e1 presente.<\/p>\n<h2 id=\"what-is-concurrency\" class=\"wp-block-heading\">\u00bfQu\u00e9 es la simultaneidad?<\/h2>\n<p>La simultaneidad en programaci\u00f3n implica que el ordenador est\u00e1 haciendo m\u00e1s de una cosa a la vez, o parece estar haci\u00e9ndolo, aunque todas las tareas se ejecuten en un \u00fanico procesador. Al gestionar los recursos y las interacciones entre las distintas partes de un programa, es posible que todas las tareas avancen de forma independiente y en intervalos de tiempo solapados.<\/p>\n<h3 id=\"both-asyncio-and-threading-appear-concurrent-in-python\" class=\"wp-block-heading\">Tanto <code>asyncio<\/code> como <code>threading<\/code> se comportan de forma simult\u00e1nea en Python<\/h3>\n<p>En t\u00e9rminos generales, las bibliotecas de Python <code>asyncio<\/code> y <code>threading<\/code> permiten la apariencia de simultaneidad. Sin embargo, sus CPU no est\u00e1n haciendo varias cosas a la vez exactamente, sino que solo lo parece.<\/p>\n<p>Imagine que est\u00e1 organizando una cena de varios platos para sus invitados y algunos de los platos tardan en cocinarse, como la tarta que hay que meter en el horno o la sopa que se est\u00e1 cociendo a fuego lento. Mientras esperamos a que se terminen de cocinar, no nos quedamos de brazos cruzados, sino que hacemos otra cosa. Esto es parecido a la simultaneidad en Python. A veces, el proceso de Python est\u00e1 esperando a que algo termine. Por ejemplo, el sistema operativo gestiona algunos procesos de entrada\/salida (E\/S) y, en este lapso, el proceso de Python est\u00e1 en pausa. Entonces, podemos usar async para permitir que otro proceso de Python se ejecute mientras tanto.<\/p>\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" class=\"wp-image-574536\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/06\/image-23.png\" alt=\"Multihilo vs. asyncio en Python\" width=\"1600\" height=\"896\" \/><\/figure>\n<h3 id=\"the-difference-is-who-is-in-charge\" class=\"wp-block-heading\">La diferencia es qui\u00e9n se encarga<\/h3>\n<p>Si tanto <code>asyncio<\/code> como <code>threading<\/code> se comportan de forma simult\u00e1nea, \u00bfen qu\u00e9 se diferencian? La principal diferencia es una cuesti\u00f3n de qui\u00e9n y cu\u00e1ndo se encarga del proceso que se est\u00e1 ejecutando. En el caso de <code>async\/await<\/code>, el enfoque a veces se conoce como \u00absimultaneidad cooperativa\u00bb. Una corrutina o un futuro cede su control a otra corrutina o futuro para darles una oportunidad. Por otro lado, en <code>threading<\/code>, el gestor del sistema operativo controlar\u00e1 qu\u00e9 proceso se est\u00e1 ejecutando.<\/p>\n<p>La simultaneidad cooperativa es como una reuni\u00f3n en la que se va pasando un micr\u00f3fono para que la gente tome la palabra. Quien tenga el micr\u00f3fono puede hablar y, cuando haya terminado o no tenga nada m\u00e1s que a\u00f1adir, pasar\u00e1 el micr\u00f3fono a la siguiente persona. Por el contrario, el multihilo es una reuni\u00f3n en la que hay un moderador que decide qui\u00e9n tiene la palabra en cada momento.\u00a0<\/p>\n<h2 id=\"writing-concurrent-code-in-python\" class=\"wp-block-heading\">Escribir c\u00f3digo simult\u00e1neo en Python<\/h2>\n<p>Veamos c\u00f3mo funciona la simultaneidad en Python escribiendo algunos ejemplos de c\u00f3digo. Crearemos una simulaci\u00f3n de un restaurante de comida r\u00e1pida usando tanto <code>asyncio<\/code> como <code>threading<\/code>.<\/p>\n<h3 id=\"how-async-await-works-in-python\" class=\"wp-block-heading\">C\u00f3mo funciona <code>async\/await<\/code> en Python<\/h3>\n<p>El paquete de <code>asyncio<\/code> se introdujo en Python 3.4, mientras que las palabras clave <code>async<\/code> y <code>await<\/code> se introdujeron en Python 3.5. Una de las principales cosas que hacen posible <code>async\/await<\/code> es el uso de corrutinas. Las corrutinas en Python son, en realidad, generadores reutilizados que poder poner en pausa y pasar de nuevo a la funci\u00f3n principal.<\/p>\n<p>Ahora, imagine un restaurante de hamburguesas donde solo haya una persona trabajando. Los pedidos se preparan seg\u00fan el orden en que van entrando y no se pueden realizar acciones as\u00edncronas:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import time\n\n\ndef make_burger(order_num):\n    print(f\"Preparing burger #{order_num}...\")\n    time.sleep(5) # time for making the burger\n    print(f\"Burger made #{order_num}\")\n\n\ndef main():\n    for i in range(3):\n        make_burger(i)\n\n\nif __name__ == \"__main__\":\n    s = time.perf_counter()\n    main()\n    elapsed = time.perf_counter() - s\n    print(f\"Orders completed in {elapsed:0.2f} seconds.\")\n<\/pre>\n<p>Esto tardar\u00e1 un rato en estar listo:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"raw\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">Preparing burger #0...\n\nBurger made #0\n\nPreparing burger #1...\n\nBurger made #1\n\nPreparing burger #2...\n\nBurger made #2\n\nOrders completed in 15.01 seconds.<\/pre>\n<p>Ahora, imaginemos que el restaurante contrata a m\u00e1s personal para que sea posible realizar el trabajo de forma simult\u00e1nea:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import asyncio\n\nimport time\n\nasync def make_burger(order_num):\n\n\u00a0\u00a0\u00a0\u00a0print(f\"Preparing burger #{order_num}...\")\n\n\u00a0\u00a0\u00a0\u00a0await asyncio.sleep(5) # time for making the burger\n\n\u00a0\u00a0\u00a0\u00a0print(f\"Burger made #{order_num}\")\n\nasync def main():\n\n\u00a0\u00a0\u00a0\u00a0order_queue = []\n\n\u00a0\u00a0\u00a0\u00a0for i in range(3):\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0order_queue.append(make_burger(i))\n\n\u00a0\u00a0\u00a0\u00a0await asyncio.gather(*(order_queue))\n\nif __name__ == \"__main__\":\n\n\u00a0\u00a0\u00a0\u00a0s = time.perf_counter()\n\n\u00a0\u00a0\u00a0\u00a0asyncio.run(main())\n\n\u00a0\u00a0\u00a0\u00a0elapsed = time.perf_counter() - s\n\n\u00a0\u00a0\u00a0\u00a0print(f\"Orders completed in {elapsed:0.2f} seconds.\")<\/pre>\n<p>Veamos en qu\u00e9 se diferencian:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"raw\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">Preparing burger #0...\n\nPreparing burger #1...\n\nPreparing burger #2...\n\nBurger made #0\n\nBurger made #1\n\nBurger made #2\n\nOrders completed in 5.00 seconds.<\/pre>\n<p>Con las funciones proporcionadas por <code>asyncio<\/code>, como <code>run<\/code> y <code>gather<\/code>, y las palabras clave <code>async<\/code> y <code>await<\/code>, hemos creado corrutinas que pueden preparar hamburguesas de forma simult\u00e1nea.<\/p>\n<p>Ahora, vayamos un poco m\u00e1s all\u00e1 y creemos una simulaci\u00f3n m\u00e1s complicada. Vamos a imaginar que solo tenemos dos trabajadores y solo podemos hacer dos hamburguesas a la vez.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import asyncio\n\nimport time\n\norder_queue = asyncio.Queue()\n\ndef take_order():\n\n\u00a0\u00a0for i in range(3):\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0order_queue.put_nowait(make_burger(i))\n\nasync def make_burger(order_num):\n\n\u00a0\u00a0print(f\"Preparing burger #{order_num}...\")\n\n\u00a0\u00a0await asyncio.sleep(5)\u00a0 # time for making the burger\n\n\u00a0\u00a0print(f\"Burger made #{order_num}\")\n\nclass Staff:\n\n\u00a0\u00a0def __init__(self, name):\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0self.name = name\n\n\u00a0\u00a0async def working(self):\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0while order_queue.qsize() &gt; 0:\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0print(f\"{self.name} is working...\")\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0task = await order_queue.get()\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0await task\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0print(f\"{self.name} finished a task...\")\n\nasync def main():\n\n\u00a0\u00a0staff1 = Staff(name=\"John\")\n\n\u00a0\u00a0staff2 = Staff(name=\"Jane\")\n\n\u00a0\u00a0take_order()\n\n\u00a0\u00a0await asyncio.gather(staff1.working(), staff2.working())\n\nif __name__ == \"__main__\":\n\n\u00a0\u00a0s = time.perf_counter()\n\n\u00a0\u00a0asyncio.run(main())\n\n\u00a0\u00a0elapsed = time.perf_counter() - s\n\n\u00a0\u00a0print(f\"Orders completed in {elapsed:0.2f} seconds.\")<\/pre>\n<p>Aqu\u00ed utilizaremos una cola para retener las tareas y el personal las ir\u00e1 recogiendo.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"raw\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">John is working...\n\nPreparing burger #0...\n\nJane is working...\n\nPreparing burger #1...\n\nBurger made #0\n\nJohn finished a task...\n\nJohn is working...\n\nPreparing burger #2...\n\nBurger made #1\n\nJane finished a task...\n\nBurger made #2\n\nJohn finished a task...\n\nOrders completed in 10.00 seconds.<\/pre>\n<p>En este ejemplo, utilizamos <code>asyncio.Queue<\/code> para almacenar las tareas, pero ser\u00e1 m\u00e1s \u00fatil si tenemos varios tipos de tareas, como se muestra en el siguiente ejemplo.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import asyncio\n\nimport time\n\ntask_queue = asyncio.Queue()\n\norder_num = 0\n\nasync def take_order():\n\n\u00a0\u00a0\u00a0global order_num\n\n\u00a0\u00a0\u00a0order_num += 1\n\n\u00a0\u00a0\u00a0print(f\"Order burger and fries for order #{order_num:04d}:\")\n\n\u00a0\u00a0\u00a0burger_num = input(\"Number of burgers:\")\n\n\u00a0\u00a0\u00a0for i in range(int(burger_num)):\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0await task_queue.put(make_burger(f\"{order_num:04d}-burger{i:02d}\"))\n\n\u00a0\u00a0\u00a0fries_num = input(\"Number of fries:\")\n\n\u00a0\u00a0\u00a0for i in range(int(fries_num)):\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0await task_queue.put(make_fries(f\"{order_num:04d}-fries{i:02d}\"))\n\n\u00a0\u00a0\u00a0print(f\"Order #{order_num:04d} queued.\")\n\n\u00a0\u00a0\u00a0await task_queue.put(take_order())\n\nasync def make_burger(order_num):\n\n\u00a0\u00a0\u00a0print(f\"Preparing burger #{order_num}...\")\n\n\u00a0\u00a0\u00a0await asyncio.sleep(5)\u00a0 # time for making the burger\n\n\u00a0\u00a0\u00a0print(f\"Burger made #{order_num}\")\n\nasync def make_fries(order_num):\n\n\u00a0\u00a0\u00a0print(f\"Preparing fries #{order_num}...\")\n\n\u00a0\u00a0\u00a0await asyncio.sleep(2)\u00a0 # time for making fries\n\n\u00a0\u00a0\u00a0print(f\"Fries made #{order_num}\")\n\nclass Staff:\n\n\u00a0\u00a0\u00a0def __init__(self, name):\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0self.name = name\n\n\u00a0\u00a0\u00a0async def working(self):\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0while True:\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0if task_queue.qsize() &gt; 0:\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0print(f\"{self.name} is working...\")\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0task = await task_queue.get()\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0await task\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0print(f\"{self.name} finish task...\")\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0else:\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0await asyncio.sleep(1) #rest\n\nasync def main():\n\n\u00a0\u00a0\u00a0task_queue.put_nowait(take_order())\n\n\u00a0\u00a0\u00a0staff1 = Staff(name=\"John\")\n\n\u00a0\u00a0\u00a0staff2 = Staff(name=\"Jane\")\n\n\u00a0\u00a0\u00a0await asyncio.gather(staff1.working(), staff2.working())\n\nif __name__ == \"__main__\":\n\n\u00a0\u00a0\u00a0s = time.perf_counter()\n\n\u00a0\u00a0\u00a0asyncio.run(main())\n\n\u00a0\u00a0\u00a0elapsed = time.perf_counter() - s\n\n\u00a0\u00a0\u00a0print(f\"Orders completed in {elapsed:0.2f} seconds.\")<\/pre>\n<p>En este ejemplo, hay varias tareas, como hacer patatas fritas, que lleva menos tiempo, o tomar nota de los pedidos, que implica recibir informaci\u00f3n del usuario.\u00a0<\/p>\n<p>Tenga en cuenta que el programa deja de esperar la informaci\u00f3n del usuario e incluso el personal que no est\u00e1 tomando nota del pedido deja de trabajar en segundo plano. Esto ocurre porque la funci\u00f3n <code>input<\/code> no es async y, por lo tanto, no se la espera. Recuerde que el control en c\u00f3digo async solo se libera cuando se le espera. Para solucionarlo, podemos sustituir<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">input(\"Number of burgers:\")<\/pre>\n<p>Por\u00a0<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">await asyncio.to_thread(input, \"Number of burgers:\")<\/pre>\n<p>Y haremos lo mismo en el caso de las patatas fritas (f\u00edjese en el siguiente c\u00f3digo). Observe que ahora el programa se ejecuta en un bucle infinito. Si queremos detenerlo, podemos bloquear de forma deliberada el programa con una entrada no v\u00e1lida.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import asyncio\n\nimport time\n\ntask_queue = asyncio.Queue()\n\norder_num = 0\n\nasync def take_order():\n\n\u00a0\u00a0\u00a0global order_num\n\n\u00a0\u00a0\u00a0order_num += 1\n\n\u00a0\u00a0\u00a0print(f\"Order burger and fries for order #{order_num:04d}:\")\n\n\u00a0\u00a0\u00a0burger_num = await asyncio.to_thread(input, \"Number of burgers:\")\n\n\u00a0\u00a0\u00a0for i in range(int(burger_num)):\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0await task_queue.put(make_burger(f\"{order_num:04d}-burger{i:02d}\"))\n\n\u00a0\u00a0\u00a0fries_num = await asyncio.to_thread(input, \"Number of fries:\")\n\n\u00a0\u00a0\u00a0for i in range(int(fries_num)):\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0await task_queue.put(make_fries(f\"{order_num:04d}-fries{i:02d}\"))\n\n\u00a0\u00a0\u00a0print(f\"Order #{order_num:04d} queued.\")\n\n\u00a0\u00a0\u00a0await task_queue.put(take_order())\n\nasync def make_burger(order_num):\n\n\u00a0\u00a0\u00a0print(f\"Preparing burger #{order_num}...\")\n\n\u00a0\u00a0\u00a0await asyncio.sleep(5)\u00a0 # time for making the burger\n\n\u00a0\u00a0\u00a0print(f\"Burger made #{order_num}\")\n\nasync def make_fries(order_num):\n\n\u00a0\u00a0\u00a0print(f\"Preparing fries #{order_num}...\")\n\n\u00a0\u00a0\u00a0await asyncio.sleep(2)\u00a0 # time for making fries\n\n\u00a0\u00a0\u00a0print(f\"Fries made #{order_num}\")\n\nclass Staff:\n\n\u00a0\u00a0\u00a0def __init__(self, name):\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0self.name = name\n\n\u00a0\u00a0\u00a0async def working(self):\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0while True:\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0if task_queue.qsize() &gt; 0:\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0print(f\"{self.name} is working...\")\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0task = await task_queue.get()\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0await task\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0print(f\"{self.name} finish task...\")\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0else:\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0await asyncio.sleep(1) #rest\n\nasync def main():\n\n\u00a0\u00a0\u00a0task_queue.put_nowait(take_order())\n\n\u00a0\u00a0\u00a0staff1 = Staff(name=\"John\")\n\n\u00a0\u00a0\u00a0staff2 = Staff(name=\"Jane\")\n\n\u00a0\u00a0\u00a0await asyncio.gather(staff1.working(), staff2.working())\n\nif __name__ == \"__main__\":\n\n\u00a0\u00a0\u00a0s = time.perf_counter()\n\n\u00a0\u00a0\u00a0asyncio.run(main())\n\n\u00a0\u00a0\u00a0elapsed = time.perf_counter() - s\n\n\u00a0\u00a0\u00a0print(f\"Orders completed in {elapsed:0.2f} seconds.\")<\/pre>\n<p>Con <code>asyncio.to_thread<\/code>, hemos puesto la funci\u00f3n <code>input<\/code> en un subproceso independiente (<a href=\"https:\/\/docs.python.org\/3\/library\/asyncio-task.html#running-in-threads\" target=\"_blank\" rel=\"noopener\">vea esta referencia<\/a>). Sin embargo, debe tener en cuenta que este truco solo desbloquea tareas limitadas por E\/S si el GIL de Python est\u00e1 presente.<\/p>\n<p>Si ejecuta el c\u00f3digo anterior, tambi\u00e9n ver\u00e1 que la E\/S est\u00e1ndar en el terminal aparece desordenada. La E\/S del usuario y el registro de lo que est\u00e1 sucediendo deben estar separados. Podemos poner el registro en un log para inspeccionarlo m\u00e1s tarde.\u00a0<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import asyncio\n\nimport logging\n\nimport time\n\nlogger = logging.getLogger(__name__)\n\nlogging.basicConfig(filename='pyburger.log', level=logging.INFO)\n\ntask_queue = asyncio.Queue()\n\norder_num = 0\n\nclosing = False\n\nasync def take_order():\n\n\u00a0\u00a0\u00a0global order_num, closing\n\n\u00a0\u00a0\u00a0try:\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0order_num += 1\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0logger.info(f\"Taking Order #{order_num:04d}...\")\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0print(f\"Order burger and fries for order #{order_num:04d}:\")\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0burger_num = await asyncio.to_thread(input, \"Number of burgers:\")\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0for i in range(int(burger_num)):\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0await task_queue.put(make_burger(f\"{order_num:04d}-burger{i:02d}\"))\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0fries_num = await asyncio.to_thread(input, \"Number of fries:\")\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0for i in range(int(fries_num)):\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0await task_queue.put(make_fries(f\"{order_num:04d}-fries{i:02d}\"))\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0logger.info(f\"Order #{order_num:04d} queued.\")\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0print(f\"Order #{order_num:04d} queued, please wait.\")\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0await task_queue.put(take_order())\n\n\u00a0\u00a0\u00a0except ValueError:\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0print(\"Goodbye!\")\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0logger.info(\"Closing down... stop taking orders and finish all tasks.\")\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0closing = True\n\nasync def make_burger(order_num):\n\n\u00a0\u00a0\u00a0logger.info(f\"Preparing burger #{order_num}...\")\n\n\u00a0\u00a0\u00a0await asyncio.sleep(5)\u00a0 # time for making the burger\n\n\u00a0\u00a0\u00a0logger.info(f\"Burger made #{order_num}\")\n\nasync def make_fries(order_num):\n\n\u00a0\u00a0\u00a0logger.info(f\"Preparing fries #{order_num}...\")\n\n\u00a0\u00a0\u00a0await asyncio.sleep(2)\u00a0 # time for making fries\n\n\u00a0\u00a0\u00a0logger.info(f\"Fries made #{order_num}\")\n\nclass Staff:\n\n\u00a0\u00a0\u00a0def __init__(self, name):\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0self.name = name\n\n\u00a0\u00a0\u00a0async def working(self):\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0while True:\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0if task_queue.qsize() &gt; 0:\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0logger.info(f\"{self.name} is working...\")\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0task = await task_queue.get()\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0await task\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0task_queue.task_done()\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0logger.info(f\"{self.name} finish task.\")\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0elif closing:\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0return\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0else:\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0await asyncio.sleep(1) #rest\n\nasync def main():\n\n\u00a0\u00a0\u00a0global task_queue\n\n\u00a0\u00a0\u00a0task_queue.put_nowait(take_order())\n\n\u00a0\u00a0\u00a0staff1 = Staff(name=\"John\")\n\n\u00a0\u00a0\u00a0staff2 = Staff(name=\"Jane\")\n\n\u00a0\u00a0\u00a0print(\"Welcome to Pyburger!\")\n\n\u00a0\u00a0\u00a0logger.info(\"Ready for business!\")\n\n\u00a0\u00a0\u00a0await asyncio.gather(staff1.working(), staff2.working())\n\n\u00a0\u00a0\u00a0logger.info(\"All tasks finished. Closing now.\")\n\nif __name__ == \"__main__\":\n\n\u00a0\u00a0\u00a0s = time.perf_counter()\n\n\u00a0\u00a0\u00a0asyncio.run(main())\n\n\u00a0\u00a0\u00a0elapsed = time.perf_counter() - s\n\n\u00a0\u00a0\u00a0logger.info(f\"Orders completed in {elapsed:0.2f} seconds.\")<\/pre>\n<p>En este bloque de c\u00f3digo final, hemos registrado la informaci\u00f3n de la simulaci\u00f3n en <code>pyburger.log<\/code> y reservado el terminal para mensajes dirigidos a los clientes. Tambi\u00e9n detectamos entradas no v\u00e1lidas durante el proceso de tomar nota del pedido y cambiamos el indicador <code>closing<\/code> a <code>True<\/code> si la entrada no era v\u00e1lida, asumiendo que el usuario quer\u00eda salir. Cuando el indicador <code>closing<\/code> se establece en <code>True<\/code>, el trabajador har\u00e1 <code>return<\/code> (regresar\u00e1), lo que pondr\u00e1 fin al bucle <code>while<\/code> infinito de la corrutina.<\/p>\n<h3 id=\"how-does-threading-work-in-python\" class=\"wp-block-heading\">\u00bfC\u00f3mo funcionan los <code>threading<\/code> en Python?<\/h3>\n<p>En el ejemplo anterior, pusimos una tarea de E\/S en otro subproceso. Se estar\u00e1 preguntando si podemos poner todas las tareas en subprocesos separados y dejar que se ejecuten de forma simult\u00e1nea. Vamos a probar usando <code>threading<\/code> en lugar de <code>asyncio<\/code>.<\/p>\n<p>F\u00edjese en el siguiente c\u00f3digo, donde creamos hamburguesas de forma simult\u00e1nea sin ninguna limitaci\u00f3n:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import asyncio\n\nimport time\n\nasync def make_burger(order_num):\n\n\u00a0\u00a0\u00a0\u00a0print(f\"Preparing burger #{order_num}...\")\n\n\u00a0\u00a0\u00a0\u00a0await asyncio.sleep(5) # time for making the burger\n\n\u00a0\u00a0\u00a0\u00a0print(f\"Burger made #{order_num}\")\n\nasync def main():\n\n\u00a0\u00a0\u00a0\u00a0order_queue = []\n\n\u00a0\u00a0\u00a0\u00a0for i in range(3):\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0order_queue.append(make_burger(i))\n\n\u00a0\u00a0\u00a0\u00a0await asyncio.gather(*(order_queue))\n\nif __name__ == \"__main__\":\n\n\u00a0\u00a0\u00a0\u00a0s = time.perf_counter()\n\n\u00a0\u00a0\u00a0\u00a0asyncio.run(main())\n\n\u00a0\u00a0\u00a0\u00a0elapsed = time.perf_counter() - s\n\n\u00a0\u00a0\u00a0\u00a0print(f\"Orders completed in {elapsed:0.2f} seconds.\")\n\n```\n\nInstead of creating async coroutines to make the burgers, we can just send functions down different threads like this:\n\n```\n\nimport threading\n\nimport time\n\ndef make_burger(order_num):\n\n\u00a0\u00a0\u00a0print(f\"Preparing burger #{order_num}...\")\n\n\u00a0\u00a0\u00a0time.sleep(5) # time for making the burger\n\n\u00a0\u00a0\u00a0print(f\"Burger made #{order_num}\")\n\ndef main():\n\n\u00a0\u00a0\u00a0order_queue = []\n\n\u00a0\u00a0\u00a0for i in range(3):\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0task = threading.Thread(target=make_burger, args=(i,))\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0order_queue.append(task)\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0task.start()\n\n\u00a0\u00a0\u00a0for task in order_queue:\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0task.join()\n\nif __name__ == \"__main__\":\n\n\u00a0\u00a0\u00a0s = time.perf_counter()\n\n\u00a0\u00a0\u00a0main()\n\n\u00a0\u00a0\u00a0elapsed = time.perf_counter() - s\n\n\u00a0\u00a0\u00a0print(f\"Orders completed in {elapsed:0.2f} seconds.\")<\/pre>\n<p>En el primer bucle <code>for<\/code> de <code>main<\/code>, las tareas se crean en diferentes subprocesos y se ponen en marcha r\u00e1pidamente. El segundo bucle <code>for<\/code> se asegura de que todas las hamburguesas est\u00e9n hechas antes de que el programa siga adelante (es decir, antes de que vuelva a <code>main<\/code>).<\/p>\n<p>Se complica cuando solo hay dos personas trabajando. Los trabajadores est\u00e1n representados con un subproceso y seleccionar\u00e1n tareas de una lista normal donde se almacenan todas.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import threading\n\nimport time\n\norder_queue = []\n\ndef take_order():\n\n\u00a0\u00a0\u00a0for i in range(3):\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0order_queue.append(make_burger(i))\n\ndef make_burger(order_num):\n\n\u00a0\u00a0\u00a0def making_burger():\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0print(f\"Preparing burger #{order_num}...\")\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time.sleep(5)\u00a0 # time for making the burger\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0print(f\"Burger made #{order_num}\")\n\n\u00a0\u00a0\u00a0return making_burger\n\ndef working():\n\n\u00a0\u00a0\u00a0\u00a0\u00a0while len(order_queue) &gt; 0:\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0print(f\"{threading.current_thread().name} is working...\")\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0task = order_queue.pop(0)\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0task()\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0print(f\"{threading.current_thread().name} finish task...\")\n\ndef main():\n\n\u00a0\u00a0\u00a0take_order()\n\n\u00a0\u00a0\u00a0staff1 = threading.Thread(target=working, name=\"John\")\n\n\u00a0\u00a0\u00a0staff1.start()\n\n\u00a0\u00a0\u00a0staff2 = threading.Thread(target=working, name=\"Jane\")\n\n\u00a0\u00a0\u00a0staff2.start()\n\n\u00a0\u00a0\u00a0staff1.join()\n\n\u00a0\u00a0\u00a0staff2.join()\n\nif __name__ == \"__main__\":\n\n\u00a0s = time.perf_counter()\n\n\u00a0main()\n\n\u00a0elapsed = time.perf_counter() - s\n\n\u00a0print(f\"Orders completed in {elapsed:0.2f} seconds.\")<\/pre>\n<p>Al ejecutar el c\u00f3digo anterior, puede producirse un error en uno de los subprocesos que indique que est\u00e1 intentando seleccionar una tarea de una lista vac\u00eda. Quiz\u00e1s se pregunte por qu\u00e9 ocurre esto, ya que tenemos una condici\u00f3n en el bucle <code>while<\/code> que hace que contin\u00fae solo si la tarea <code>task_queue<\/code> no est\u00e1 vac\u00eda. Sin embargo, sigue apareciendo el error porque hemos encontrado condiciones de carrera.<\/p>\n<h3 id=\"race-conditions\" class=\"wp-block-heading\">Condiciones de carrera<\/h3>\n<p>Las condiciones de carrera pueden producirse cuando varios subprocesos intentan acceder al mismo recurso o a los mismos datos a la vez y causan problemas en el sistema. El momento y el orden en que se accede al recurso son importantes para la l\u00f3gica del programa, y un momento impredecible o la intercalaci\u00f3n de varios subprocesos accediendo y modificando datos compartidos puede causar errores.<\/p>\n<p>Para resolver la condici\u00f3n de carrera en nuestro programa, vamos a implementar un bloqueo a la tarea <code>task_queue<\/code>:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">queue_lock = threading.Lock()<\/pre>\n<p>Para que funcione, necesitamos asegurarnos de que tenemos derechos de acceso a la cola al comprobar su longitud y seleccionar tareas. Mientras tengamos los derechos, no habr\u00e1 otros subprocesos que puedan acceder a la cola:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">def working():\n\n\u00a0\u00a0\u00a0while True:\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0with queue_lock:\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0if len(order_queue) == 0:\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0return\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0else:\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0task = order_queue.pop(0)\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0print(f\"{threading.current_thread().name} is working...\")\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0task()\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0print(f\"{threading.current_thread().name} finish task...\")\n\n```\n\nBased on what we have learned so far, we can complete our final code with threading like this:\n\n```\n\nimport logging\n\nimport threading\n\nimport time\n\nlogger = logging.getLogger(__name__)\n\nlogging.basicConfig(filename=\"pyburger_threads.log\", level=logging.INFO)\n\nqueue_lock = threading.Lock()\n\ntask_queue = []\n\norder_num = 0\n\nclosing = False\n\ndef take_order():\n\n\u00a0\u00a0\u00a0global order_num, closing\n\n\u00a0\u00a0\u00a0try:\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0order_num += 1\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0logger.info(f\"Taking Order #{order_num:04d}...\")\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0print(f\"Order burger and fries for order #{order_num:04d}:\")\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0burger_num = input(\"Number of burgers:\")\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0for i in range(int(burger_num)):\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0with queue_lock:\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0task_queue.append(make_burger(f\"{order_num:04d}-burger{i:02d}\"))\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0fries_num = input(\"Number of fries:\")\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0for i in range(int(fries_num)):\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0with queue_lock:\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0task_queue.append(make_fries(f\"{order_num:04d}-fries{i:02d}\"))\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0logger.info(f\"Order #{order_num:04d} queued.\")\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0print(f\"Order #{order_num:04d} queued, please wait.\")\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0with queue_lock:\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0task_queue.append(take_order)\n\n\u00a0\u00a0\u00a0except ValueError:\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0print(\"Goodbye!\")\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0logger.info(\"Closing down... stop taking orders and finish all tasks.\")\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0closing = True\n\ndef make_burger(order_num):\n\n\u00a0\u00a0\u00a0def making_burger():\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0logger.info(f\"Preparing burger #{order_num}...\")\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time.sleep(5)\u00a0 # time for making the burger\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0logger.info(f\"Burger made #{order_num}\")\n\n\u00a0\u00a0\u00a0return making_burger\n\ndef make_fries(order_num):\n\n\u00a0\u00a0\u00a0def making_fries():\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0logger.info(f\"Preparing fried #{order_num}...\")\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time.sleep(2)\u00a0 # time for making fries\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0logger.info(f\"Fries made #{order_num}\")\n\n\u00a0\u00a0\u00a0return making_fries\n\ndef working():\n\n\u00a0\u00a0\u00a0while True:\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0with queue_lock:\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0if len(task_queue) == 0:\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0if closing:\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0return\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0else:\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0task = None\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0else:\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0task = task_queue.pop(0)\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0if task:\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0logger.info(f\"{threading.current_thread().name} is working...\")\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0task()\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0logger.info(f\"{threading.current_thread().name} finish task...\")\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0else:\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time.sleep(1)\u00a0 # rest\n\ndef main():\n\n\u00a0\u00a0\u00a0print(\"Welcome to Pyburger!\")\n\n\u00a0\u00a0\u00a0logger.info(\"Ready for business!\")\n\n\u00a0\u00a0\u00a0task_queue.append(take_order)\n\n\u00a0\u00a0\u00a0staff1 = threading.Thread(target=working, name=\"John\")\n\n\u00a0\u00a0\u00a0staff1.start()\n\n\u00a0\u00a0\u00a0staff2 = threading.Thread(target=working, name=\"Jane\")\n\n\u00a0\u00a0\u00a0staff2.start()\n\n\u00a0\u00a0\u00a0staff1.join()\n\n\u00a0\u00a0\u00a0staff2.join()\n\n\u00a0\u00a0\u00a0logger.info(\"All tasks finished. Closing now.\")\n\nif __name__ == \"__main__\":\n\n\u00a0\u00a0\u00a0s = time.perf_counter()\n\n\u00a0\u00a0\u00a0main()\n\n\u00a0\u00a0\u00a0elapsed = time.perf_counter() - s\n\n\u00a0\u00a0\u00a0logger.info(f\"Orders completed in {elapsed:0.2f} seconds.\")<\/pre>\n<p>Si compara los dos fragmentos de c\u00f3digo utilizando <code>asyncio<\/code> y <code>threading<\/code>, los resultados deber\u00edan ser similares. Quiz\u00e1s se pregunte cu\u00e1l es mejor y por qu\u00e9 deber\u00eda elegir uno u otro.<\/p>\n<p>En la pr\u00e1ctica, escribir c\u00f3digo <code>asyncio<\/code> es m\u00e1s f\u00e1cil que el multihilo porque no tenemos que preocuparnos por las posibles condiciones de carrera y los interbloqueos. Las corrutinas pasan controles de forma predeterminada, por lo que no se necesitan bloqueos. Sin embargo, los subprocesos de Python pueden ejecutarse en paralelo, aunque no siempre con el GIL presente. Podemos volver sobre esto cuando hablemos de Python nogil (sin subprocesos) en el pr\u00f3ximo art\u00edculo del blog.<\/p>\n<h2 id=\"benefiting-from-concurrency\" class=\"wp-block-heading\">Ventajas de la simultaneidad<\/h2>\n<p>\u00bfPor qu\u00e9 utilizar la simultaneidad en programaci\u00f3n? Por una sencilla raz\u00f3n: la velocidad. Como hemos visto anteriormente, las tareas pueden completarse m\u00e1s r\u00e1pidamente si podemos reducir el tiempo de espera. Hay varios tipos de espera en computaci\u00f3n y, en cada caso, tendemos a usar diferentes m\u00e9todos para ahorrar tiempo.<\/p>\n<h3 id=\"i-o-bound-tasks\" class=\"wp-block-heading\">Tareas ligadas a la E\/S<\/h3>\n<p>Se considera que una tarea o programa est\u00e1 ligado a la entrada\/salida (E\/S) cuando su velocidad de ejecuci\u00f3n est\u00e1 limitada principalmente por la velocidad de las operaciones de E\/S, como la lectura de un archivo o de la red, o la espera de la informaci\u00f3n que introduzca del usuario. Las operaciones de E\/S suelen ser m\u00e1s lentas que otras operaciones de la CPU y, por tanto, las tareas que implican muchas de ellas pueden ser bastante m\u00e1s lentas. Algunos ejemplos de ello son la lectura de datos de una base de datos, la gesti\u00f3n de peticiones web o el trabajo con archivos de gran tama\u00f1o.<\/p>\n<p>El uso de la simultaneidad <code>async\/await<\/code> puede ayudar a optimizar el tiempo de espera durante las tareas vinculadas a E\/S desbloqueando la secuencia de procesamiento y permitiendo que se lleven a cabo otras tareas mientras se espera.<\/p>\n<p>La simultaneidad <code>async\/await<\/code> tiene ventajas en muchas aplicaciones de Python, como las aplicaciones web que necesitan mucha comunicaci\u00f3n con bases de datos y el manejo de peticiones web. Las interfaces gr\u00e1ficas de usuario tambi\u00e9n pueden beneficiarse de la simultaneidad <code>async\/await<\/code> al permitir que se lleven a cabo tareas en segundo plano mientras el usuario interact\u00faa con la aplicaci\u00f3n.<\/p>\n<h3 id=\"cpu-bound-tasks\" class=\"wp-block-heading\">Tareas ligadas a la CPU<\/h3>\n<p>Una tarea o programa se considera ligado a la CPU cuando su velocidad de ejecuci\u00f3n est\u00e1 limitada principalmente por la velocidad de la CPU. Algunos ejemplos t\u00edpicos son el procesamiento de im\u00e1genes o v\u00eddeo, como el cambio de tama\u00f1o o la edici\u00f3n, y los c\u00e1lculos matem\u00e1ticos complejos, como la multiplicaci\u00f3n de matrices o el entrenamiento de modelos de aprendizaje autom\u00e1tico.<\/p>\n<p>A diferencia de las tareas de E\/S, las tareas de CPU no suelen poder optimizarse con simultaneidad <code>async\/await<\/code>, ya que la CPU ya est\u00e1 ocupada trabajando en las tareas. Si tiene m\u00e1s de una CPU en la m\u00e1quina, o si puede descargar algunas de estas tareas a una o m\u00e1s GPU, las tareas ligadas a la CPU pueden terminarse m\u00e1s r\u00e1pidamente creando m\u00e1s subprocesos y con multiprocesamiento. El multiprocesamiento puede optimizar el uso de las CPU y las GPU, por lo que muchos modelos de aprendizaje autom\u00e1tico e inteligencia artificial se entrenan actualmente en varias GPU.<\/p>\n<p>Sin embargo, esto es dif\u00edcil de llevar a cabo con c\u00f3digo Python puro, ya que Python est\u00e1 dise\u00f1ado para proporcionar capas abstractas de forma que los usuarios no tengan que controlar los procesos de computaci\u00f3n de nivel inferior. Adem\u00e1s, el GIL de Python limita el uso compartido de los recursos de Python entre varios subprocesos del ordenador. Recientemente, Python 3.13 incorpor\u00f3 la posibilidad de eliminar el GIL, lo que permite lograr un multihilo real. En el pr\u00f3ximo art\u00edculo del blog, hablaremos del GIL y de la posibilidad de prescindir de \u00e9l.<\/p>\n<p>A veces, ninguno de los m\u00e9todos mencionados anteriormente es capaz de acelerar suficientemente las tareas ligadas a la CPU. En ese caso, puede que sea necesario dividir las tareas ligadas a la CPU en otras m\u00e1s peque\u00f1as para que puedan ejecutarse de forma simult\u00e1nea en varios subprocesos, varios procesadores o incluso varias m\u00e1quinas. Esto es el procesamiento paralelo y puede que tenga que reescribir el c\u00f3digo por completo para implementarlo. En Python, el paquete <code>multiprocessing<\/code> ofrece simultaneidad local y remota, que puede utilizarse para evitar la limitaci\u00f3n del GIL. Tambi\u00e9n veremos algunos ejemplos de ello en el pr\u00f3ximo art\u00edculo del blog.<\/p>\n<h2 id=\"debugging-concurrent-code-in-py-charm\" class=\"wp-block-heading\">Depuraci\u00f3n de c\u00f3digo simult\u00e1neo en PyCharm<\/h2>\n<p>Depurar c\u00f3digo async o simult\u00e1neo puede ser dif\u00edcil, ya que el programa no se ejecuta en secuencia, lo que significa que es dif\u00edcil ver d\u00f3nde y cu\u00e1ndo se est\u00e1 ejecutando el c\u00f3digo. Muchos desarrolladores utilizan <code>print<\/code> para ayudar a trazar el flujo del c\u00f3digo, pero no recomendamos este enfoque, ya que es poco preciso y utilizarlo para investigar un programa complejo, como uno simult\u00e1neo, no es nada f\u00e1cil. Adem\u00e1s, despu\u00e9s no siempre es f\u00e1cil poner orden.<\/p>\n<p>Muchos IDE tienen depuradores, que son geniales para inspeccionar variables y el flujo del programa. Los depuradores tambi\u00e9n proporcionan un seguimiento claro de la pila a trav\u00e9s de varios subprocesos. Veamos c\u00f3mo podemos rastrear la tarea <code>task_queue<\/code> de nuestro ejemplo de simulaci\u00f3n de restaurante en <a href=\"https:\/\/www.jetbrains.com\/pycharm\/\" target=\"_blank\" rel=\"noopener\" data-type=\"link\" data-id=\"https:\/\/www.jetbrains.com\/pycharm\/data-science\/\">PyCharm<\/a>.<\/p>\n<p>Primero, pondremos algunos puntos de interrupci\u00f3n en el c\u00f3digo. Puede hacerlo haciendo clic en el n\u00famero de la l\u00ednea donde quiere que el depurador haga una pausa. El n\u00famero de l\u00ednea se convertir\u00e1 en un punto rojo, indicando que all\u00ed se establece un punto de interrupci\u00f3n. Pondremos puntos de interrupci\u00f3n en las l\u00edneas 23, 27 y 65, donde se modifica la tarea <code>task_queue<\/code> en varios subprocesos.<\/p>\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" class=\"wp-image-574369\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/06\/image-8.png\" alt=\"\" width=\"1600\" height=\"720\" \/><\/figure>\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" class=\"wp-image-574380\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/06\/image-9.png\" alt=\"\" width=\"1600\" height=\"554\" \/><\/figure>\n<p>A continuaci\u00f3n, podemos ejecutar el programa en modo de depuraci\u00f3n haciendo clic en el peque\u00f1o icono de error de la parte superior derecha.<\/p>\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" class=\"wp-image-574391\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/06\/image-10.png\" alt=\"\" width=\"860\" height=\"258\" \/><\/figure>\n<p>Tras hacer clic en el icono, se abrir\u00e1 la ventana <em>Debug<\/em>. El programa se ejecutar\u00e1 hasta que llegue al primer punto de interrupci\u00f3n resaltado en el c\u00f3digo.<\/p>\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" class=\"wp-image-574402\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/06\/image-11.png\" alt=\"\" width=\"1600\" height=\"1002\" \/><\/figure>\n<p>Aqu\u00ed vemos que el subproceso <code>John<\/code> est\u00e1 intentando retomar la tarea y la l\u00ednea 65 est\u00e1 resaltada. En este punto, la l\u00ednea resaltada a\u00fan no se ha ejecutado. Esto es \u00fatil cuando queremos inspeccionar las variables antes de entrar en el punto de interrupci\u00f3n.<\/p>\n<p>Vamos a inspeccionar lo que hay en la tarea <code>task_queue<\/code>. Puede hacerlo simplemente comenzando a escribir en la ventana <em>Debug<\/em>, como se muestra a continuaci\u00f3n.<\/p>\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" class=\"wp-image-574414\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/06\/image-12.png\" alt=\"\" width=\"1600\" height=\"468\" \/><\/figure>\n<p>Seleccione o escriba \u00abtask_queue\u00bb y, a continuaci\u00f3n, pulse <em>Intro<\/em>. Ver\u00e1 que la tarea <code>take_order<\/code> est\u00e1 en la cola.<\/p>\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" class=\"wp-image-574425\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/06\/image-13.png\" alt=\"\" width=\"1600\" height=\"527\" \/><\/figure>\n<p>Ahora, vamos a ejecutar el punto de interrupci\u00f3n pulsando el bot\u00f3n <em>Step in<\/em>, como se muestra a continuaci\u00f3n.<\/p>\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" class=\"wp-image-574436\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/06\/image-14.png\" alt=\"\" width=\"1274\" height=\"488\" \/><\/figure>\n<p>Despu\u00e9s de pulsarlo e inspeccionar la ventana de <em>Special Variables<\/em> que aparece, vemos que ahora la variable de tarea es <code>take_order<\/code> en el subproceso <code>John<\/code>.<\/p>\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" class=\"wp-image-574447\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/06\/image-15.png\" alt=\"\" width=\"1600\" height=\"522\" \/><\/figure>\n<p>Al consultar de nuevo la tarea <code>task_queue<\/code>, vemos que ahora la lista est\u00e1 vac\u00eda.<\/p>\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" class=\"wp-image-574458\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/06\/image-16.png\" alt=\"\" width=\"1600\" height=\"451\" \/><\/figure>\n<p>Ahora, vamos a pulsar el bot\u00f3n <em>Resume Program<\/em> y a dejar que el programa se ejecute.<\/p>\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" class=\"wp-image-574469\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/06\/image-17.png\" alt=\"\" width=\"994\" height=\"610\" \/><\/figure>\n<p>Cuando el programa llegue a la parte de entrada del usuario, PyCharm nos llevar\u00e1 a la ventana <em>Console<\/em> para que podamos proporcionar la entrada. Pongamos que queremos dos hamburguesas. Escriba \u00ab2\u00bb y pulse <em>Intro<\/em>.<\/p>\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" class=\"wp-image-574480\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/06\/image-18.png\" alt=\"\" width=\"1288\" height=\"616\" \/><\/figure>\n<p>Ahora, llegamos al segundo punto de interrupci\u00f3n. Si hacemos clic en <em>Threads &amp; Variables<\/em> para volver a esa ventana, veremos que <code>burger_num<\/code> es dos, tal como lo introdujimos.<\/p>\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" class=\"wp-image-574491\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/06\/image-19.png\" alt=\"\" width=\"1600\" height=\"1002\" \/><\/figure>\n<p>Ahora, vamos a entrar en el punto de interrupci\u00f3n e inspeccionar la tarea <code>task_queue<\/code>, como hicimos antes. Vemos que se ha a\u00f1adido una tarea <code>make_burger<\/code>.<\/p>\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" class=\"wp-image-574502\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/06\/image-20.png\" alt=\"\" width=\"1600\" height=\"405\" \/><\/figure>\n<p>Dejamos que el programa se ejecute de nuevo y, si entramos en el punto de interrupci\u00f3n cuando se detiene, vemos que <code>Jane<\/code> est\u00e1 recogiendo la tarea.<\/p>\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" class=\"wp-image-574513\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/06\/image-21.png\" alt=\"\" width=\"1600\" height=\"1002\" \/><\/figure>\n<p>Si quiere, puede inspeccionar el resto del c\u00f3digo. Cuando haya terminado, pulse el bot\u00f3n rojo <em>Stop<\/em> en la parte superior de la ventana.<\/p>\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" class=\"wp-image-574524\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/06\/image-22.png\" alt=\"\" width=\"848\" height=\"244\" \/><\/figure>\n<p>Con el depurador en PyCharm, puede seguir la ejecuci\u00f3n del programa a trav\u00e9s de varios subprocesos e inspeccionar diferentes variables muy f\u00e1cilmente.<\/p>\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n<h2 id=\"conclusion\" class=\"wp-block-heading\">Conclusi\u00f3n<\/h2>\n<p>Ahora, hemos aprendido lo b\u00e1sico de la simultaneidad en Python y esperamos que, con la pr\u00e1ctica, sea capaz de dominarla. En el pr\u00f3ximo art\u00edculo del blog, echaremos un vistazo a la GIL de Python, qu\u00e9 papel desempe\u00f1a y qu\u00e9 cambia cuando est\u00e1 ausente.<\/p>\n<p>PyCharm cuenta con herramientas potentes para trabajar con c\u00f3digo Python simult\u00e1neo. Como demostramos en este art\u00edculo del blog, el depurador permite inspeccionar paso a paso el c\u00f3digo async y thread, lo que le ayuda rastrear el flujo de ejecuci\u00f3n, supervisar los recursos compartidos y detectar los problemas. Con puntos de interrupci\u00f3n intuitivos, vistas de variables en tiempo real, integraci\u00f3n perfecta de la consola para la entrada del usuario y una s\u00f3lida compatibilidad de registro, PyCharm facilita la escritura, las pruebas y la depuraci\u00f3n de aplicaciones con confianza y claridad.<\/p>\n<div class=\"buttons\">\n<div class=\"buttons__row\"><a class=\"btn\" href=\"https:\/\/www.jetbrains.com\/pycharm\/\" target=\"\" rel=\"noopener\">Descargue PyCharm ahora<\/a><\/div>\n<\/div>\n\n\n<p>A<em>rt\u00edculo original en ingl\u00e9s de:<\/em><\/p>\n\n\n    <div class=\"about-author \">\n        <div class=\"about-author__box\">\n            <div class=\"row\">\n                <div class=\"about-author__box-img\">\n                    <img decoding=\"async\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/01\/CheukTingHo-Kimono-e1738750639162-200x200.jpg\" width=\"200\" height=\"200\" alt=\"Cheuk Ting Ho\" loading=\"lazy\"  class=\"avatar avatar-200 wp-user-avatar wp-user-avatar-200 photo avatar-default\">\n                <\/div>\n                <div class=\"about-author__box-text\">\n                                            <h4>Cheuk Ting Ho<\/h4>\n                                                        <\/div>\n            <\/div>\n        <\/div>\n    <\/div>\n","protected":false},"author":1086,"featured_media":591111,"comment_status":"closed","ping_status":"closed","template":"","categories":[1401,2347,8377],"tags":[6939,5377],"cross-post-tag":[],"acf":[],"_links":{"self":[{"href":"https:\/\/blog.jetbrains.com\/es\/wp-json\/wp\/v2\/pycharm\/591090"}],"collection":[{"href":"https:\/\/blog.jetbrains.com\/es\/wp-json\/wp\/v2\/pycharm"}],"about":[{"href":"https:\/\/blog.jetbrains.com\/es\/wp-json\/wp\/v2\/types\/pycharm"}],"author":[{"embeddable":true,"href":"https:\/\/blog.jetbrains.com\/es\/wp-json\/wp\/v2\/users\/1086"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.jetbrains.com\/es\/wp-json\/wp\/v2\/comments?post=591090"}],"version-history":[{"count":4,"href":"https:\/\/blog.jetbrains.com\/es\/wp-json\/wp\/v2\/pycharm\/591090\/revisions"}],"predecessor-version":[{"id":591124,"href":"https:\/\/blog.jetbrains.com\/es\/wp-json\/wp\/v2\/pycharm\/591090\/revisions\/591124"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.jetbrains.com\/es\/wp-json\/wp\/v2\/media\/591111"}],"wp:attachment":[{"href":"https:\/\/blog.jetbrains.com\/es\/wp-json\/wp\/v2\/media?parent=591090"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.jetbrains.com\/es\/wp-json\/wp\/v2\/categories?post=591090"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.jetbrains.com\/es\/wp-json\/wp\/v2\/tags?post=591090"},{"taxonomy":"cross-post-tag","embeddable":true,"href":"https:\/\/blog.jetbrains.com\/es\/wp-json\/wp\/v2\/cross-post-tag?post=591090"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}