{"id":20170036,"url":"https://github.com/charbonnierg/asyncio-examples","last_synced_at":"2025-03-03T04:15:32.040Z","repository":{"id":49565455,"uuid":"376797588","full_name":"charbonnierg/asyncio-examples","owner":"charbonnierg","description":null,"archived":false,"fork":false,"pushed_at":"2021-06-15T06:47:04.000Z","size":18,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"next","last_synced_at":"2025-01-13T15:26:17.416Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/charbonnierg.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-06-14T11:21:03.000Z","updated_at":"2021-06-15T06:47:07.000Z","dependencies_parsed_at":"2022-09-21T14:01:19.265Z","dependency_job_id":null,"html_url":"https://github.com/charbonnierg/asyncio-examples","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/charbonnierg%2Fasyncio-examples","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/charbonnierg%2Fasyncio-examples/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/charbonnierg%2Fasyncio-examples/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/charbonnierg%2Fasyncio-examples/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/charbonnierg","download_url":"https://codeload.github.com/charbonnierg/asyncio-examples/tar.gz/refs/heads/next","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":241605820,"owners_count":19989612,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-14T01:16:08.972Z","updated_at":"2025-03-03T04:15:32.013Z","avatar_url":"https://github.com/charbonnierg.png","language":"Python","readme":"# quara-concurrency\n\n## Introduction\n\nThis repository contains a python package as well as code examples that leverage [`asyncio`](https://docs.python.org/3.8/library/asyncio.html) python library. It is meant to learn about `asyncio` and is not meant to be used in production (not yet at least).\n\n\u003e Note: Readers that never heard about *\"concurrency\"* or *\"asynchronous programming\"* are encouraged to read this [really nice introcution to asynchronous code and concurrency](https://fastapi.tiangolo.com/async/#technical-details)from [FastAPI](https://fastapi.tiangolo.com) documentation.\n\n## `asyncio`: The bright side\n\n- `asyncio` is a library to write **concurrent** code using the **async**/**await** syntax.\n\n- `asyncio` is used as a foundation for multiple Python asynchronous frameworks that provide high-performance network and web-servers, database connection libraries, distributed task queues, etc.\n\n- `asyncio` is often a perfect fit for IO-bound and high-level structured network code. \n\n![asyncio schema](https://cdn2.hubspot.net/hubfs/424565/_02_Paxos_Engineering/Event-Loop.png)\n\n## `asyncio`: The dark side\n\nWhile `asyncio` can be used to bring concurrency and be extremely useful when developing web-servers or network related libraries, it also has several downsides.\n\n- **`asyncio` is not friendly with Python REPL**: One of the nice things about python is that you can always fire up the interpreter and step through code easily. For asyncio code, you need to run your code using an event loop. `await` keyword cannot be used outside a function, and running a coroutine requires a running event loop. A trivial example such as sleeping is much more complicated with `asyncio`:\n\n    ```python\n    import asyncio\n\n    # Define a coroutine function\n    async def main():\n        \"\"\"We must define a function to use `await` keyword and execute asyncio.sleep coroutine\"\"\"\n        # It does not block the thread, instead, it let other coroutines have a chance to run while sleeping\n        await asyncio.sleep(1)\n\n    # Run the main coroutine function (this will start the event loop in the main thread) - Python 3.7+ only\n    asyncio.run(main())\n    ```\n\n    than without:\n\n    ```python\n    import time\n\n    # Sleep 1 second. It blocks the main thread\n    time.sleep(1)\n    ```\n\n- **`asyncio` is all or nothing** (at least without playing with threads!): If an event loop is running in the main thread, almost all code must be asynchronous in order not to block the event loop. Indeed, all code still runs in a single thread by default, and while waiting for a blocking function, coroutines cannot be given a chance to run. Consider the following code:\n\n    \u003e NOTE: This code is intended to be WRONG. Do not copy/paste in your application carelessly\n\n    ```python\n    import asyncio\n    from queue import Queue\n\n    # Create a queue (this is a synchronous Queue, not the asynchronous queue available in asyncio.Queue)\n    queue = Queue()\n\n    # Define a coroutine function\n    async def some_task():\n        # Start an infinite loop\n        while True:\n            print(\"Doing some work\")\n            # Sleep 0.1 seconds before reentering loop\n            await asyncio.sleep(0.1)\n\n\n    # Define main coroutine\n    async def main():\n        # Create the task\n        asyncio.create_task(some_task())\n        # Get a value from the queue\n        try:\n            queue.get()\n        except KeyboardInterrupt:\n            print(\"Exiting\")\n\n\n    if __name__ == \"__main__\":\n        # Run the main coroutine function (this will start the event loop in the main thread) - Python 3.7+ only\n        asyncio.run(main())\n    ```\n\n    `some_task()` coroutine function should run in the event loop and print \"Doing some work\" every 0.1 second. But since the coroutine is running in the main thread, and main thread is blocked by `queue.get()` statement, the task never has a chance to run. If you cancel the program using `Ctrl+C`, you will see the task being run once, I.E, after `queue.get()` is cancelled, and before `event_loop` is closed.\n\n- **always thinking about the event loop is hard**: Are you **awaiting** the result of several **coroutines** and then performing some action on that data? If so, you should be using `asyncio.gather`. Or, perhaps you want to `asyncio.wait` on **task** completion? Maybe you want your **future** to `run_until_complete` or your **loop** to `run_forever`? Did you forget to put `async` in front of a function definition that uses `await`? That will throw an error. Did you forget to put `await` in front of an asynchronous function? That will return a coroutine that was never invoked, and intentionally not throw an error!\n\n- **`asyncio` doesn’t play that nicely with threads**: If you’re using a library that depends on threading you’re going to have to come up with a workaround, same things goes with asynchronous queues.\n\n- **everything is harder**: Libraries are often buggier and less used/documented (though hopefully that will change over time). You’re not going to find as many helpful error messages on Stack Overflow. Something as easy as writing asynchronous unit tests is non-trivial. There’s also more opportunities to mess up.\n\n### Be prepared\n\nWhen developping with `asyncio`, it is **MANDATORY** to rely on a linter such as `flake8`, a type checker such as `mypy` and run python with the `-X dev` flag to enable  the [**Python Development Mode**](https://docs.python.org/3/library/devmode.html).\n\nNote that the development mode can also be enabled by setting the environment variable `PYTHON_DEV_MODE` to 1.\n\n## About `quara-concurrency`\n\nThe `quara-concurrency` library exposes a single class: `AsyncioThread` which can be used to start a new thread, with a running event loop.\n\nThis thread can then be used to:\n- schedule coroutines to the event loop from any other thread\n- submit blocking functions to a thread pool executor from any other thread\n\nIt comes with a bunch of methods to avoid errors related to unsafe thread usage. Reading those methods is helpful to better understand `asyncio` concepts and tools.\n\n### Example usage\n\n1. Import `AsyncioThread` and create a new thread:\n\n    ```python\n    from quara.concurrency import AsyncioThread\n\n\n    thread = AsyncioThread()\n    ```\n    \u003e Note: At this point, only the `__init__()` method of `AsyncioThread` has been called.\n\n2. Start and stop the thread manually:\n\n    ```python\n    thread.start()\n    ```\n\n    \u003e Note: `AsyncioThread` inherits from `threading.Thread`. The method `AsyncioThread.start` comes directly from `threading.Thread.start`. The `AsyncioThread.run` method is called once thread is started. Thread will be alive until the `AsyncioThread.run` method raises an error or finishes successfully. The `Asyncio.run` method first create some tasks into an event loop, then run the event loop forever. It means that in order to stop this thread gracefully, event loop must be stopped.\n\n    ```python\n    thread.stop_threadsafe()\n    ```\n\n    \u003e Note: We used `AsyncioThread.stop_threadsafe` method to stop the thread because an event loop must be stopped from within a coroutine. Since the event loop we want to stop is running within the `AsyncioThread` instance, we must use `run_threadsafe()` method to schedule a new task to stop it from another thread. The `Asyncio.stop_threadsafe` method submit the coroutine function `AsyncioThread.stop` into the thread event loop. When running code within the thread, it is possible to create an `asyncio` task that runs the `AsyncioThread.stop` coroutine function. Such an example can be found in `AsyncioThread._signal_handler` method.\n\n3. Start and stop the thread using a context manager:\n\n    ```python\n    from quara.concurrency import AsyncioThread\n\n\n    with AsyncioThread() as thread:\n        # At this point thread is started\n        pass\n    # And now thread is stopped\n    ```\n\n    \u003e Note: Take a look at `AsyncioThread.__enter__` and `AsyncioThread.__exit__` methods to see what's happening.\n\n4. Run a coroutine within the `AsyncioThread` event loop:\n\n    ```python\n    import asyncio\n    from quara.concurrency import AsyncioThread\n\n\n    with AsyncioThread() as thread:\n        # Submit the coroutine to the thread event loop\n        future = thread.run_threadsafe(asyncio.sleep(1))\n        # Wait for returned value and assign it to result variable\n        result = future.result()\n        # `asyncio.sleep` function returns None\n        assert result is None\n    ```\n\n    \u003e Note: `AsyncioThread.run_threadsafe` does not return the result of the submitted coroutine, but returns a `concurrent.futures.Future` instead. This `Future` instance can be used to wait for and fetch the coroutine returned value.\n\n5. Start an `asyncio` task within the `AsyncioThread` event loop using a decorator\n\n    ```python\n    import asyncio\n    import time\n    from quara.concurrency import AsyncioThread\n\n\n    thread = AsyncioThread()\n\n\n    @thread.task\n    async def some_task():\n        \"\"\"A dummy task that runs forever.\n\n        This task will stop only when cancelled.\n        A more realistic task would be to read a network socket, or wait for items in a queue then process them.\n        \"\"\"\n        # Let's loop infinitely\n        while True:\n            # And simulate that we're doing something\n            print(\"Doing some work\")\n            try:\n                await asyncio.sleep(0.5)\n            except asyncio.CancelledError:\n                print(\"Bye bye\")\n                # Do not forget to break else task will never end\n                break\n\n    # The task is started with the thread\n    thread.start()\n    # Let's wait a bit\n    time.sleep(1)\n    # The task is cancelled on thread stop\n    thread.stop_threadsafe()\n    ```\n\n6. Start an `asyncio` task within an already running `AsyncioThread`\n\n    ```python\n    import asyncio\n    import time\n    from quara.concurrency import AsyncioThread\n\n\n    async def some_task():\n        \"\"\"A dummy task that runs forever.\n\n        This task will stop only when cancelled.\n        A more realistic task would be to read a network socket, or wait for items in a queue then process them.\n        \"\"\"\n        # Let's loop infinitely\n        while True:\n            # And simulate that we're doing something\n            print(\"Doing some work\")\n            try:\n                await asyncio.sleep(0.5)\n            except asyncio.CancelledError:\n                print(\"Bye bye\")\n                # Do not forget to break else task will never end\n                break\n\n    with AsyncioThread() as thread:\n        # Create the task using a coroutine\n        thread.create_task_threadsafe(some_task())\n        # Let's wait a bit\n        time.sleep(1)\n\n    ```\n\n    \u003e Note: This one is pretty useful ! You can run as many tasks concurrently as you want 😙\n\n7. Execute a costly blocking function in a third thread managed by a `ThreadPoolExecutor` instance within a coroutine:\n\n    ```python\n    import time\n    from quara.concurrency import AsyncioThread\n\n\n    thread = AsyncioThread()\n\n\n    def costly_function(x: int):\n        time.sleep(x)\n        print(\"Bye bye\")            \n\n    async def some_task():\n        await thread.run_in_executor(costly_function, 1)\n\n    # thread will be stopped once context manager exits\n    with thread:\n        thread.run_threadsafe(some_task())\n    ```\n\n    Event if `time.sleep()` is a blocking function, it does not block the `AsyncioThread`. It is often required to run costly functions in an executor in order not to block the event loop (not all functions can be really asynchronous, I.E, CPU-bound functions).\n\n    \u003e Note: concurrency is not the same as parallelism. Only one task can run at a time, but tasks can be paused and resumed when `await` keyword is encountered.\n\n8. Execute a costly blocking function in a third thread managed by a `ThreadPoolExecutor` instance:\n\n    ```python\n    import time\n    from quara.concurrency import AsyncioThread\n\n\n    def costly_function(x: int):\n        time.sleep(x)\n        print(\"Bye bye\")\n\n    # You can specify maximum number of threads for the executor\n    with AsyncioThread(max_workers=8) as thread:\n        # Create the task using a coroutine\n        future = thread.run_in_executor_threadsafe(costly_function, 1)\n        # The context manager will wait for the future to finish before exiting\n        # If you don't want to wait, then don't use a context manager\n    ```\n\n    In this case, the function is still executed in a third thread, but is submitted from the main thread instead of the `AsyncioThread` instance.\n\n## References\n\n* [The Python Standard Library -- asyncio -- Asynchronous I/O](https://docs.python.org/3.8/library/asyncio.html)\n* [The Python Standard Library -- Development Tools -- Effects of the Python Development Mode](https://docs.python.org/3/library/devmode.html#effects-of-the-python-development-mode)\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcharbonnierg%2Fasyncio-examples","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcharbonnierg%2Fasyncio-examples","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcharbonnierg%2Fasyncio-examples/lists"}