aiologic is a locking library for tasks synchronization and their communication. It provides primitives that are both async-aware and thread-aware, and can be used for interaction between:
- async codes (async <-> async) in one thread as regular async primitives
- async codes (async <-> async) in multiple threads (!)
- async code and sync one (async <-> sync) in one thread (!)
- async code and sync one (async <-> sync) in multiple threads (!)
- sync codes (sync <-> sync) in one thread as regular sync primitives
- sync codes (sync <-> sync) in multiple threads as regular sync primitives
Let's take a look at the example:
import asyncio
from threading import Thread
import aiologic
lock = aiologic.Lock()
async def func(i: int, j: int) -> None:
print(f"thread={i} task={j} start")
async with lock:
await asyncio.sleep(1)
print(f"thread={i} task={j} end")
async def main(i: int) -> None:
await asyncio.gather(func(i, 0), func(i, 1))
Thread(target=asyncio.run, args=[main(0)]).start()
Thread(target=asyncio.run, args=[main(1)]).start()It prints something like this:
thread=0 task=0 start thread=1 task=0 start thread=0 task=1 start thread=1 task=1 start thread=0 task=0 end thread=1 task=0 end thread=0 task=1 end thread=1 task=1 end
As you can see, tasks from different event loops are all able to acquire
aiologic.Lock. In the same case if you use asyncio.Lock, it
will raise a RuntimeError. And threading.Lock will cause a
deadlock.
- Python 3.8+ support
- CPython and PyPy support
- Experimental Nuitka support
- Pickling and weakrefing support
- Cancellation and timeouts support
- Optional Trio-style 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
- Lock-free implementation (with some exceptions)
- Bundled stub files
Synchronization primitives:
- Events: one-time, reusable, and countdown
- Barriers: single-use, cyclic, and reusable
- Semaphores: counting, bounded, and binary
- Capacity limiters: borrowable, and reentrant
- Locks: ownable, and reentrant
- Readers-writer locks (external)
- Condition variables
Communication primitives:
- Queues: FIFO, LIFO, and priority
Non-blocking primitives:
- Flags
- Resource guards
- Futures (external)
Supported concurrency libraries:
- asyncio, curio, trio, and anyio (coroutine-based)
- eventlet, and gevent (greenlet-based)
- threading (thread-based)
The GIL ensures sequential consistency of all operations due to the synchronizes-with relation, and this allows the execution of Python code to be interpreted as one of the possible serialized sequences of all threads' operations. In addition, there are also effectively atomic operations (those at the C level that do not release the GIL), and the pure-Python implementation of this library takes advantage of this.
In free-threaded Python, the same subset of effectively atomic operations we use remains thread-safe; however, one nuance is that sequential consistency is no longer guaranteed, and we may observe weaker ordering on relaxed operations. But the good news is that:
- Load operations (
slot.__get__(),value = dict[key], and so on) that return arbitrary objects are typically acquire operations or stronger. - Store operations (
slot.__set__(),dict[key] = value, and so on) that set arbitrary objects are typically release operations or stronger. - Read-modify-write operations (
slot.__delete__(),del dict[key],dict.setdefault(key, value), and so on) typically have a consistent side effect (that is, two or more threads cannot both delete the same slot/item or get/set different defaults unless there were concurrent store operations).
The release-acquire ordering establishes the same synchronizes-with relationship and thereby guarantees that a thread performing a load operation will not be able to see a partially initialized object (the release semantics prevent reordering of the initialization and the store operation; the acquire semantics ensure that the cache is updated). Otherwise, we would have to use locks for every transfer of a newly created object between threads.
Historically, the pure-Python implementation was primarily designed for the GIL (sequential consistency), but since version 0.18.0, it has explicitly targeted:
- x86-TSO and stronger for metafunctions (import machinery, etc.).
- Weaker memory models for primitives (via per-object locks).
If you need stricter guarantees, please prefer the build with extension modules when it is available.
Install from PyPI (stable):
pip install aiologicOr from GitHub (latest):
pip install git+https://github.com/x42005e1f/aiologic.gitYou can also use other package managers, such as uv.
Read the Docs: https://aiologic.readthedocs.io (official)
GitHub Discussions: https://github.com/x42005e1f/aiologic/discussions (ideas, questions)
GitHub Issues: https://github.com/x42005e1f/aiologic/issues (bug tracker)
You can also send an email to 0x42005e1f@gmail.com with any feedback.
The aiologic library is REUSE-compliant and is offered under multiple licenses:
- All original source code is licensed under ISC.
- All original test code is licensed under 0BSD.
- All documentation is licensed under CC-BY-4.0.
- All configuration is licensed under CC0-1.0.
For more accurate information, check the individual files.