{ "cells": [ { "cell_type": "markdown", "id": "03c941ae", "metadata": {}, "source": [ "# `asyncio` Beispiel\n", "\n", "Ab IPython≥7.0 könnt ihr `asyncio` direkt in Jupyter Notebooks verwenden; seht auch [IPython 7.0, Async REPL](https://blog.jupyter.org/ipython-7-0-async-repl-a35ce050f7f7)." ] }, { "cell_type": "markdown", "id": "a728279f", "metadata": {}, "source": [ "Wenn ihr die Fehlermeldung `RuntimeError: This event loop is already running` erhaltet, hilft euch vielleicht [nest-asyncio] weiter.\n", "\n", "Ihr könnt das Paket in eurer Jupyter- oder JupyterHub-Umgebung installieren mit\n", "\n", "``` bash\n", "$ uv add nest-asyncio\n", "```\n", "\n", "Ihr könnt es dann in euer Notebook importieren und verwenden mit:" ] }, { "cell_type": "code", "execution_count": 1, "id": "45aacd96", "metadata": { "execution": { "iopub.execute_input": "2026-05-21T22:44:51.004264Z", "iopub.status.busy": "2026-05-21T22:44:51.003962Z", "iopub.status.idle": "2026-05-21T22:44:51.009895Z", "shell.execute_reply": "2026-05-21T22:44:51.009544Z", "shell.execute_reply.started": "2026-05-21T22:44:51.004245Z" } }, "outputs": [], "source": [ "import nest_asyncio\n", "\n", "\n", "nest_asyncio.apply()" ] }, { "cell_type": "markdown", "id": "3d44ea02", "metadata": {}, "source": [ "
\n", "\n", "**Zum Weiterlesen**\n", "\n", "* Lynn Root: [asyncio: We Did It Wrong](https://www.roguelynn.com/words/asyncio-we-did-it-wrong/)\n", "* Mike Driscoll: [An Intro to asyncio](https://www.blog.pythonlibrary.org/2016/07/26/python-3-an-intro-to-asyncio/)\n", "* Yeray Diaz: [Asyncio Coroutine Patterns: Beyond await](https://medium.com/python-pandemonium/asyncio-coroutine-patterns-beyond-await-a6121486656f)\n", "
" ] }, { "cell_type": "markdown", "id": "bc3e0b72", "metadata": {}, "source": [ "## Einfaches *Hello world*-Beispiel" ] }, { "cell_type": "code", "execution_count": 2, "id": "479b2752", "metadata": { "execution": { "iopub.execute_input": "2026-05-21T22:44:51.011173Z", "iopub.status.busy": "2026-05-21T22:44:51.011056Z", "iopub.status.idle": "2026-05-21T22:44:52.018287Z", "shell.execute_reply": "2026-05-21T22:44:52.017233Z", "shell.execute_reply.started": "2026-05-21T22:44:51.011163Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Hello\n", "world\n" ] } ], "source": [ "import asyncio\n", "\n", "\n", "async def hello():\n", " print(\"Hello\")\n", " await asyncio.sleep(1)\n", " print(\"world\")\n", "\n", "\n", "await hello()" ] }, { "cell_type": "markdown", "id": "c3d8764e", "metadata": {}, "source": [ "## Ein bisschen näher an einem realen Beispiel" ] }, { "cell_type": "code", "execution_count": 3, "id": "fc1761e7", "metadata": { "execution": { "iopub.execute_input": "2026-05-21T22:44:52.020942Z", "iopub.status.busy": "2026-05-21T22:44:52.020239Z", "iopub.status.idle": "2026-05-21T22:44:59.151221Z", "shell.execute_reply": "2026-05-21T22:44:59.149987Z", "shell.execute_reply.started": "2026-05-21T22:44:52.020904Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Publishing 1/10\n", "Publishing 2/10\n", "consuming 1\n", "Publishing 3/10\n", "consuming 2\n", "Publishing 4/10\n", "Publishing 5/10\n", "consuming 3\n", "consuming 4\n", "Publishing 6/10\n", "Publishing 7/10\n", "consuming 5\n", "Publishing 8/10\n", "consuming 6\n", "Publishing 9/10\n", "Publishing 10/10\n", "consuming 7\n", "consuming 8\n", "consuming 9\n", "consuming 10\n" ] } ], "source": [ "import random\n", "\n", "\n", "async def publish(queue, n):\n", " for x in range(1, n + 1):\n", " # publish an item\n", " print(f\"Publishing {x}/{n}\")\n", " # simulate i/o operation using sleep\n", " await asyncio.sleep(random.random())\n", " item = str(x)\n", " # put the item in the queue\n", " await queue.put(item)\n", "\n", " # indicate the publisher is done\n", " await queue.put(None)\n", "\n", "\n", "async def consume(queue):\n", " while True:\n", " # wait for an item from the publisher\n", " item = await queue.get()\n", " if item is None:\n", " # the publisher emits None to indicate that it is done\n", " break\n", "\n", " # process the item\n", " print(f\"consuming {item}\")\n", " # simulate i/o operation using sleep\n", " await asyncio.sleep(random.random())\n", "\n", "\n", "background_tasks = set()\n", "loop = asyncio.get_event_loop()\n", "queue = asyncio.Queue()\n", "publishing = asyncio.ensure_future(publish(queue, 10), loop=loop)\n", "background_tasks.add(publishing)\n", "publishing.add_done_callback(background_tasks.discard)\n", "loop.run_until_complete(consume(queue))" ] }, { "cell_type": "markdown", "id": "8d340991", "metadata": {}, "source": [ "## Ausnahmebehandlung\n", "\n", "
\n", "\n", "**Siehe auch:**\n", " \n", "* [set_exception_handler](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.set_exception_handler)\n", "
" ] }, { "cell_type": "code", "execution_count": 4, "id": "54d7107d", "metadata": { "execution": { "iopub.execute_input": "2026-05-21T22:44:59.153440Z", "iopub.status.busy": "2026-05-21T22:44:59.152722Z", "iopub.status.idle": "2026-05-21T22:44:59.161116Z", "shell.execute_reply": "2026-05-21T22:44:59.160324Z", "shell.execute_reply.started": "2026-05-21T22:44:59.153399Z" } }, "outputs": [], "source": [ "import logging\n", "import signal\n", "\n", "\n", "logger = logging.getLogger(\"stream_logger\")\n", "\n", "\n", "def handle_exception(context):\n", " msg = context.get(\"Exception\", context[\"message\"])\n", " logger.error(f\"Caught exception: {msg}\")\n", " logger.info(\"Shutting down…\")\n", "\n", "\n", "def main():\n", " loop = asyncio.get_event_loop()\n", " # May want to catch other signals too\n", " signals = (signal.SIGHUP, signal.SIGTERM, signal.SIGINT)\n", " for s in signals:\n", " loop.add_signal_handler(\n", " s,\n", " lambda s=s: asyncio.create_task(loop, signal=s),\n", " )\n", " loop.set_exception_handler(handle_exception)\n", " queue = asyncio.Queue()\n", " try:\n", " publish_task = loop.create_task(publish(queue))\n", " background_tasks.add(publish_task)\n", " publish_task.add_done_callback(background_tasks.discard)\n", " consume_task = loop.create_task(consume(queue))\n", " background_tasks.add(consume_task)\n", " consume_task.add_done_callback(background_tasks.discard)\n", " loop.run_forever()\n", " finally:\n", " loop.close()" ] }, { "cell_type": "markdown", "id": "8b4270b5", "metadata": {}, "source": [ "## Testen mit `pytest`" ] }, { "cell_type": "markdown", "id": "47a0aa94", "metadata": {}, "source": [ "### Beispiel:\n", "\n", "Beim Testen muss man häufig Coroutinen simulieren, die innerhalb der zu testenden Funktion aufgerufen werden.\n", "\n", "Dafür benötigen wir\n", "\n", "* [pytest](https://docs.pytest.org/en/stable/)\n", "* [pytest-asyncio](https://pytest-asyncio.readthedocs.io/en/stable/)\n", "* [pytest-mock](https://pytest-mock.readthedocs.io/en/latest/)\n", "\n", "Da die Bibliothek pytest-mock jedoch keine asynchronen Mocks unterstützt, müssen wir eine Umgehungslösung finden:" ] }, { "cell_type": "code", "execution_count": 5, "id": "e7b841d6-2c0b-48ee-83dc-c969f4db5d0d", "metadata": { "execution": { "iopub.execute_input": "2026-05-21T22:44:59.161983Z", "iopub.status.busy": "2026-05-21T22:44:59.161835Z", "iopub.status.idle": "2026-05-21T22:44:59.213134Z", "shell.execute_reply": "2026-05-21T22:44:59.212841Z", "shell.execute_reply.started": "2026-05-21T22:44:59.161969Z" } }, "outputs": [], "source": [ "import asyncio\n", "\n", "import pytest\n", "\n", "\n", "@pytest.fixture\n", "def mock_coroutine(mocker, monkeypatch):\n", "\n", " def _mock_coroutine_pair(to_patch=None):\n", " mock = mocker.Mock()\n", "\n", " async def coroutine(*args, **kwargs):\n", " return mock(*args, **kwargs)\n", "\n", " if to_patch:\n", " monkeypatch.setattr(to_patch, _mock_coroutine_pair)\n", "\n", " return mock, _mock_coroutine_pair\n", "\n", " return _mock_coroutine_pair" ] }, { "cell_type": "markdown", "id": "a4944dda-144e-47da-9f69-777460b14ccd", "metadata": {}, "source": [ "Und für [asyncio.Queue](https://docs.python.org/3/library/asyncio-queue.html#queue) habe ich folgende Fixtures:" ] }, { "cell_type": "code", "execution_count": 6, "id": "1a578e1d-9eba-4454-9f28-0e7d494a0635", "metadata": { "execution": { "iopub.execute_input": "2026-05-21T22:44:59.213825Z", "iopub.status.busy": "2026-05-21T22:44:59.213676Z", "iopub.status.idle": "2026-05-21T22:44:59.215729Z", "shell.execute_reply": "2026-05-21T22:44:59.215463Z", "shell.execute_reply.started": "2026-05-21T22:44:59.213813Z" } }, "outputs": [], "source": [ "@pytest.fixture\n", "def mock_queue(mocker, monkeypatch):\n", " queue = mocker.Mock()\n", " monkeypatch.setattr(asyncio, \"Queue\", queue)\n", " return queue.return_value\n", "\n", "\n", "@pytest.fixture\n", "def mock_get(mock_coroutine):\n", " mock_get, _ = mock_coroutine()\n", " return mock_get" ] }, { "cell_type": "code", "execution_count": 7, "id": "a196e119", "metadata": { "execution": { "iopub.execute_input": "2026-05-21T22:44:59.216244Z", "iopub.status.busy": "2026-05-21T22:44:59.216172Z", "iopub.status.idle": "2026-05-21T22:44:59.218180Z", "shell.execute_reply": "2026-05-21T22:44:59.217968Z", "shell.execute_reply.started": "2026-05-21T22:44:59.216236Z" } }, "outputs": [], "source": [ "@pytest.mark.asyncio\n", "async def test_consume(mock_get, mock_queue):\n", " mock_get.side_effect = Exception(\"Break while loop\")\n", "\n", " with pytest.raises(Exception, match=\"Break while loop\"):\n", " await consume(mock_queue)" ] }, { "cell_type": "markdown", "id": "1ea35d70", "metadata": {}, "source": [ "### Bibliotheken von Drittanbietern\n", "\n", "* [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) hat hilfreiche Dinge wie Test-Fixtures für `event_loop`, `unused_tcp_port`, und `unused_tcp_port_factory`; und die Möglichkeit zum Erstellen eurer eigenen [asynchronen Fixtures](https://pytest-asyncio.readthedocs.io/en/latest/reference/fixtures/index.html).\n", "* [asynctest](https://asynctest.readthedocs.io/en/latest/index.html) verfügt über hilfreiche Werkzeuge, einschließlich Coroutine-Mocks und [exhaust_callbacks](https://asynctest.readthedocs.io/en/latest/asynctest.helpers.html#asynctest.helpers.exhaust_callbacks) so dass wir `await task` nicht manuell erstellen müssen.\n", "* [aiohttp](https://docs.aiohttp.org/en/stable/) hat ein paar wirklich nette eingebaute Test-Utilities." ] }, { "cell_type": "markdown", "id": "8f9acdb7", "metadata": {}, "source": [ "## Debugging\n", "\n", "`asyncio` hat bereits einen [debug mode](https://docs.python.org/3.6/library/asyncio-dev.html#debug-mode-of-asyncio) in der Standardbibliothek. Ihr könnt ihn einfach mit der Umgebungsvariablen `PYTHONASYNCIODEBUG` oder im Code mit `loop.set_debug(True)` aktivieren." ] }, { "cell_type": "markdown", "id": "6ef4d1c5", "metadata": {}, "source": [ "### Verwendet den Debug-Modus zum Identifizieren langsamer asynchroner Aufrufe\n", "\n", "Der Debug-Modus von `asyncio` hat einen kleinen eingebauten Profiler. Wenn der Debug-Modus aktiviert ist, protokolliert `asyncio` alle asynchronen Aufrufe, die länger als 100 Millisekunden dauern." ] }, { "cell_type": "markdown", "id": "f4daa532", "metadata": {}, "source": [ "### Debugging im Produktivbetrieb mit `aiodebug`\n", "\n", "[aiodebug](https://github.com/qntln/aiodebug) ist eine kleine Bibliothek zum Überwachen und Testen von Asyncio-Programmen." ] }, { "cell_type": "markdown", "id": "584b01ce", "metadata": {}, "source": [ "#### Beispiel" ] }, { "cell_type": "code", "execution_count": 8, "id": "79686fa8", "metadata": { "execution": { "iopub.execute_input": "2026-05-21T22:44:59.218567Z", "iopub.status.busy": "2026-05-21T22:44:59.218505Z", "iopub.status.idle": "2026-05-21T22:44:59.222312Z", "shell.execute_reply": "2026-05-21T22:44:59.221979Z", "shell.execute_reply.started": "2026-05-21T22:44:59.218560Z" } }, "outputs": [], "source": [ "import aiodebug.log_slow_callbacks\n", "\n", "from aiodebug.logging_compat import get_logger\n", "\n", "\n", "logger = get_logger(__name__)\n", "\n", "aiodebug.log_slow_callbacks.enable(\n", " 0.05,\n", " on_slow_callback=lambda task_name, duration: logger.warning(\n", " \"Task blocked async loop for too long\",\n", " extra={\"task_name\": task_name, \"duration\": duration},\n", " ),\n", ")" ] }, { "cell_type": "markdown", "id": "dac368ac", "metadata": {}, "source": [ "## Logging\n", "\n", "[aiologger](https://github.com/async-worker/aiologger) ermöglicht eine nicht-blockierendes Logging." ] }, { "cell_type": "markdown", "id": "acddce02", "metadata": {}, "source": [ "## Asynchrone Widgets\n", "\n", "
\n", "\n", "**Siehe auch**\n", " \n", "* [Asynchronous Widgets](https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20Asynchronous.html)" ] }, { "cell_type": "code", "execution_count": 9, "id": "6925cd9d", "metadata": { "execution": { "iopub.execute_input": "2026-05-21T22:44:59.222729Z", "iopub.status.busy": "2026-05-21T22:44:59.222654Z", "iopub.status.idle": "2026-05-21T22:44:59.224649Z", "shell.execute_reply": "2026-05-21T22:44:59.224329Z", "shell.execute_reply.started": "2026-05-21T22:44:59.222722Z" } }, "outputs": [], "source": [ "def wait_for_change(widget, value):\n", " future = asyncio.Future()\n", "\n", " def getvalue(change):\n", " # make the new value available\n", " future.set_result(change.new)\n", " widget.unobserve(getvalue, value)\n", "\n", " widget.observe(getvalue, value)\n", " return future" ] }, { "cell_type": "code", "execution_count": 10, "id": "fb57d28a", "metadata": { "execution": { "iopub.execute_input": "2026-05-21T22:44:59.225592Z", "iopub.status.busy": "2026-05-21T22:44:59.225387Z", "iopub.status.idle": "2026-05-21T22:44:59.263470Z", "shell.execute_reply": "2026-05-21T22:44:59.263060Z", "shell.execute_reply.started": "2026-05-21T22:44:59.225568Z" } }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "3ff313ef13a34af7b89d368ce3b981ec", "version_major": 2, "version_minor": 0 }, "text/plain": [ "IntSlider(value=0)" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" }, { "name": "stdout", "output_type": "stream", "text": [ "did work 0\n", "async function continued with value 2\n", "did work 1\n", "async function continued with value 4\n", "did work 2\n", "async function continued with value 8\n", "did work 3\n", "async function continued with value 14\n", "did work 4\n", "async function continued with value 23\n", "did work 5\n", "async function continued with value 33\n", "did work 6\n", "async function continued with value 44\n", "did work 7\n", "async function continued with value 58\n", "did work 8\n", "async function continued with value 73\n", "did work 9\n", "async function continued with value 88\n" ] } ], "source": [ "from ipywidgets import IntSlider\n", "\n", "\n", "slider = IntSlider()\n", "\n", "\n", "async def f():\n", " for i in range(10):\n", " print(f\"did work {i}\")\n", " x = await wait_for_change(slider, \"value\")\n", " print(f\"async function continued with value {x}\")\n", "\n", "\n", "task = asyncio.ensure_future(f())\n", "background_tasks = set()\n", "\n", "background_tasks.add(task)\n", "\n", "task.add_done_callback(background_tasks.discard)\n", "\n", "slider" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3.13 Kernel", "language": "python", "name": "python313" }, "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.13.0" }, "latex_envs": { "LaTeX_envs_menu_present": true, "autoclose": false, "autocomplete": true, "bibliofile": "biblio.bib", "cite_by": "apalike", "current_citInitial": 1, "eqLabelWithNumbers": true, "eqNumInitial": 1, "hotkeys": { "equation": "Ctrl-E", "itemize": "Ctrl-I" }, "labels_anchors": false, "latex_user_defs": false, "report_style_numbering": false, "user_envs_cfg": false }, "widgets": { "application/vnd.jupyter.widget-state+json": { "state": { "29325f5d03794dd2973a53aff0e10de5": { "model_module": "@jupyter-widgets/base", "model_module_version": "2.0.0", "model_name": "LayoutModel", "state": {} }, "3ff313ef13a34af7b89d368ce3b981ec": { "model_module": "@jupyter-widgets/controls", "model_module_version": "2.0.0", "model_name": "IntSliderModel", "state": { "behavior": "drag-tap", "layout": "IPY_MODEL_29325f5d03794dd2973a53aff0e10de5", "style": "IPY_MODEL_a916773d25cf41cc9800fd8359c9321d", "value": 100 } }, "a916773d25cf41cc9800fd8359c9321d": { "model_module": "@jupyter-widgets/controls", "model_module_version": "2.0.0", "model_name": "SliderStyleModel", "state": { "description_width": "" } } }, "version_major": 2, "version_minor": 0 } } }, "nbformat": 4, "nbformat_minor": 5 }