aiologic


Nameaiologic JSON
Version 0.7.1 PyPI version JSON
download
home_pageNone
SummaryGIL-powered* locking library for Python
upload_time2024-10-20 21:48:37
maintainerNone
docs_urlNone
authorNone
requires_python>=3.8
licenseCopyright 2024 Ilya Egorov <0x42005e1f@gmail.com>. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
keywords
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            ========
aiologic
========

**aiologic** is an async-aware library for tasks synchronization and their
communication in different threads and different event loops. Let's take a look
at the example:

.. code:: python

    from threading import Thread

    import anyio

    from aiologic import Lock

    lock = Lock()


    async def func(i, j):
        print(f"started thread={i} task={j}")

        async with lock:
            await anyio.sleep(1)

        print(f"stopped thread={i} task={j}")


    async def main(i):
        async with anyio.create_task_group() as tasks:
            for j in range(2):
                tasks.start_soon(func, i, j)


    for i in range(2):
        Thread(target=anyio.run, args=[main, i]).start()

It prints something like this:

.. code-block::

    started thread=0 task=0
    started thread=1 task=0
    started thread=0 task=1
    started thread=1 task=1
    stopped thread=0 task=0
    stopped thread=1 task=0
    stopped thread=0 task=1
    stopped thread=1 task=1

As you can see, when using ``aiologic.Lock``, tasks from different event loops
are all able to acquire a lock. In the same case if you use ``anyio.Lock``, it
will raise a ``RuntimeError``. And ``threading.Lock`` will cause a deadlock.

Why?
====

Cooperative (coroutines, greenlets) and preemptive (threads) multitasking are
not usually used together. But there are situations when these so different
styles need to coexist:

* Interaction of two or more frameworks that cannot be run in the same event
  loop (e.g. a GUI framework with any other framework).
* Parallelization of code whose synchronous part cannot be easily delegated to
  a thread pool (e.g. a CPU-bound network application that needs low
  response times).
* Simultaneous use of incompatible concurrency libraries in different threads
  (e.g. due to legacy code).

Known solutions (only for some special cases) use one of the following ideas:

- Delegate waiting to a thread pool (executor), e.g. via ``run_in_executor()``.
- Delegate calling to an event loop, e.g. via
  ``call_soon_threadsafe()``.
- Perform polling via timeouts and non-blocking calls.

All these ideas have disadvantages. Polling consumes a lot of CPU resources,
actually blocks the event loop for a short time, and has poor responsiveness.
The ``call_soon_threadsafe()`` approach does not actually do any real work
until the event loop scheduler handles a callback, and in the case of a queue
only works when there is only one consumer. The ``run_in_executor()`` approach
requires a worker thread per call and has issues with cancellation and
timeouts:

.. code:: python

    import asyncio
    import threading

    from concurrent.futures import ThreadPoolExecutor

    executor = ThreadPoolExecutor(8)
    semaphore = threading.Semaphore(0)


    async def main():
        loop = asyncio.get_running_loop()

        for _ in range(8):
            try:
                await asyncio.wait_for(loop.run_in_executor(
                    executor,
                    semaphore.acquire,
                ), 0)
            except asyncio.TimeoutError:
                pass


    print('active threads:', threading.active_count())  # 1

    asyncio.run(main())

    print('active threads:', threading.active_count())  # 9 - wow, thread leak!

    # program will hang until you press Control-C

However, *aiologic* has none of these disadvantages. Using its approach based
on low-level events, it gives you much more than you can get with alternatives.
That's why it's there, and that's why you're here.

Features
========

* Python 3.8+ support
* `CPython <https://www.python.org/>`_ and `PyPy <https://pypy.org/>`_ support
* Pickling and weakrefing support
* Cancellation and timeouts support
* Optional `Trio-style checkpoints
  <https://trio.readthedocs.io/en/stable/reference-core.html#checkpoints>`_:

  * enabled by default for Trio itself
  * disabled by default for all others

* Only one checkpoint per asynchronous call:

  * exactly one context switch if checkpoints are enabled
  * zero or one context switch if checkpoints are disabled

* Fairness wherever possible (with some caveats)
* Thread safety wherever possible
* Zero required dependencies
* Lock-free implementation

Synchronization primitives:

* Semaphores: counting and bounded
* Locks: primitive, ownable and reentrant
* Capacity limiters
* Condition variables
* Barriers: single-use and cyclic
* Events: one-time and reusable
* Resource guards

Communication primitives:

* Queues: FIFO, LIFO and priority

Supported concurrency libraries:

* `asyncio <https://docs.python.org/3/library/asyncio.html>`_
  and `trio <https://trio.readthedocs.io>`_ (coroutine-based)
* `eventlet <https://eventlet.readthedocs.io>`_
  and `gevent <https://www.gevent.org/>`_ (greenlet-based)

All synchronization and communication primitives are implemented entirely on
effectively atomic operations, which gives `an incredible speedup on PyPy
<https://gist.github.com/x42005e1f/149d3994d5f7bd878def71d5404e6ea4>`_ compared
to alternatives from the threading module. All this works because of GIL, but
per-object locks also ensure that `the same operations are still atomic
<https://peps.python.org/pep-0703/#container-thread-safety>`_, so aiologic also
works when running in a `free-threaded mode
<https://docs.python.org/3.13/whatsnew/3.13.html#free-threaded-cpython>`_.

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "aiologic",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.8",
    "maintainer_email": null,
    "keywords": null,
    "author": null,
    "author_email": "Ilya Egorov <0x42005e1f@gmail.com>",
    "download_url": "https://files.pythonhosted.org/packages/06/9f/2be9d0b72bf3fc5105cd64401fbb160f7c8663f2b4bc307eee18766e38ae/aiologic-0.7.1.tar.gz",
    "platform": null,
    "description": "========\naiologic\n========\n\n**aiologic** is an async-aware library for tasks synchronization and their\ncommunication in different threads and different event loops. Let's take a look\nat the example:\n\n.. code:: python\n\n    from threading import Thread\n\n    import anyio\n\n    from aiologic import Lock\n\n    lock = Lock()\n\n\n    async def func(i, j):\n        print(f\"started thread={i} task={j}\")\n\n        async with lock:\n            await anyio.sleep(1)\n\n        print(f\"stopped thread={i} task={j}\")\n\n\n    async def main(i):\n        async with anyio.create_task_group() as tasks:\n            for j in range(2):\n                tasks.start_soon(func, i, j)\n\n\n    for i in range(2):\n        Thread(target=anyio.run, args=[main, i]).start()\n\nIt prints something like this:\n\n.. code-block::\n\n    started thread=0 task=0\n    started thread=1 task=0\n    started thread=0 task=1\n    started thread=1 task=1\n    stopped thread=0 task=0\n    stopped thread=1 task=0\n    stopped thread=0 task=1\n    stopped thread=1 task=1\n\nAs you can see, when using ``aiologic.Lock``, tasks from different event loops\nare all able to acquire a lock. In the same case if you use ``anyio.Lock``, it\nwill raise a ``RuntimeError``. And ``threading.Lock`` will cause a deadlock.\n\nWhy?\n====\n\nCooperative (coroutines, greenlets) and preemptive (threads) multitasking are\nnot usually used together. But there are situations when these so different\nstyles need to coexist:\n\n* Interaction of two or more frameworks that cannot be run in the same event\n  loop (e.g. a GUI framework with any other framework).\n* Parallelization of code whose synchronous part cannot be easily delegated to\n  a thread pool (e.g. a CPU-bound network application that needs low\n  response times).\n* Simultaneous use of incompatible concurrency libraries in different threads\n  (e.g. due to legacy code).\n\nKnown solutions (only for some special cases) use one of the following ideas:\n\n- Delegate waiting to a thread pool (executor), e.g. via ``run_in_executor()``.\n- Delegate calling to an event loop, e.g. via\n  ``call_soon_threadsafe()``.\n- Perform polling via timeouts and non-blocking calls.\n\nAll these ideas have disadvantages. Polling consumes a lot of CPU resources,\nactually blocks the event loop for a short time, and has poor responsiveness.\nThe ``call_soon_threadsafe()`` approach does not actually do any real work\nuntil the event loop scheduler handles a callback, and in the case of a queue\nonly works when there is only one consumer. The ``run_in_executor()`` approach\nrequires a worker thread per call and has issues with cancellation and\ntimeouts:\n\n.. code:: python\n\n    import asyncio\n    import threading\n\n    from concurrent.futures import ThreadPoolExecutor\n\n    executor = ThreadPoolExecutor(8)\n    semaphore = threading.Semaphore(0)\n\n\n    async def main():\n        loop = asyncio.get_running_loop()\n\n        for _ in range(8):\n            try:\n                await asyncio.wait_for(loop.run_in_executor(\n                    executor,\n                    semaphore.acquire,\n                ), 0)\n            except asyncio.TimeoutError:\n                pass\n\n\n    print('active threads:', threading.active_count())  # 1\n\n    asyncio.run(main())\n\n    print('active threads:', threading.active_count())  # 9 - wow, thread leak!\n\n    # program will hang until you press Control-C\n\nHowever, *aiologic* has none of these disadvantages. Using its approach based\non low-level events, it gives you much more than you can get with alternatives.\nThat's why it's there, and that's why you're here.\n\nFeatures\n========\n\n* Python 3.8+ support\n* `CPython <https://www.python.org/>`_ and `PyPy <https://pypy.org/>`_ support\n* Pickling and weakrefing support\n* Cancellation and timeouts support\n* Optional `Trio-style checkpoints\n  <https://trio.readthedocs.io/en/stable/reference-core.html#checkpoints>`_:\n\n  * enabled by default for Trio itself\n  * disabled by default for all others\n\n* Only one checkpoint per asynchronous call:\n\n  * exactly one context switch if checkpoints are enabled\n  * zero or one context switch if checkpoints are disabled\n\n* Fairness wherever possible (with some caveats)\n* Thread safety wherever possible\n* Zero required dependencies\n* Lock-free implementation\n\nSynchronization primitives:\n\n* Semaphores: counting and bounded\n* Locks: primitive, ownable and reentrant\n* Capacity limiters\n* Condition variables\n* Barriers: single-use and cyclic\n* Events: one-time and reusable\n* Resource guards\n\nCommunication primitives:\n\n* Queues: FIFO, LIFO and priority\n\nSupported concurrency libraries:\n\n* `asyncio <https://docs.python.org/3/library/asyncio.html>`_\n  and `trio <https://trio.readthedocs.io>`_ (coroutine-based)\n* `eventlet <https://eventlet.readthedocs.io>`_\n  and `gevent <https://www.gevent.org/>`_ (greenlet-based)\n\nAll synchronization and communication primitives are implemented entirely on\neffectively atomic operations, which gives `an incredible speedup on PyPy\n<https://gist.github.com/x42005e1f/149d3994d5f7bd878def71d5404e6ea4>`_ compared\nto alternatives from the threading module. All this works because of GIL, but\nper-object locks also ensure that `the same operations are still atomic\n<https://peps.python.org/pep-0703/#container-thread-safety>`_, so aiologic also\nworks when running in a `free-threaded mode\n<https://docs.python.org/3.13/whatsnew/3.13.html#free-threaded-cpython>`_.\n",
    "bugtrack_url": null,
    "license": "Copyright 2024 Ilya Egorov <0x42005e1f@gmail.com>.  Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.  THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ",
    "summary": "GIL-powered* locking library for Python",
    "version": "0.7.1",
    "project_urls": {
        "Source": "https://github.com/x42005e1f/aiologic"
    },
    "split_keywords": [],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "b948a94c0c3213a1738fd9ea6a0ba56ba508c2eb1f713b5d78ff105262c1f953",
                "md5": "1cb7396c777e502496aa03e316bd0e96",
                "sha256": "da61f632e671a93e4dc59ad627c57808dc391a4fe77198bb8f1eb0dc71d8c63f"
            },
            "downloads": -1,
            "filename": "aiologic-0.7.1-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "1cb7396c777e502496aa03e316bd0e96",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8",
            "size": 37191,
            "upload_time": "2024-10-20T21:48:35",
            "upload_time_iso_8601": "2024-10-20T21:48:35.495336Z",
            "url": "https://files.pythonhosted.org/packages/b9/48/a94c0c3213a1738fd9ea6a0ba56ba508c2eb1f713b5d78ff105262c1f953/aiologic-0.7.1-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "069f2be9d0b72bf3fc5105cd64401fbb160f7c8663f2b4bc307eee18766e38ae",
                "md5": "37949efb830f6176e71da251a985c77f",
                "sha256": "9d4f1f846995f9fc538184a0b81d76f1784aaaad2e888117b631f4926b89a924"
            },
            "downloads": -1,
            "filename": "aiologic-0.7.1.tar.gz",
            "has_sig": false,
            "md5_digest": "37949efb830f6176e71da251a985c77f",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8",
            "size": 29513,
            "upload_time": "2024-10-20T21:48:37",
            "upload_time_iso_8601": "2024-10-20T21:48:37.510809Z",
            "url": "https://files.pythonhosted.org/packages/06/9f/2be9d0b72bf3fc5105cd64401fbb160f7c8663f2b4bc307eee18766e38ae/aiologic-0.7.1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-10-20 21:48:37",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "x42005e1f",
    "github_project": "aiologic",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": false,
    "lcname": "aiologic"
}
        
Elapsed time: 0.46802s