aiologic
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.
Features
Python 3.8+ 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
Condition variables
Communication primitives:
Queues: FIFO, LIFO, and priority
Non-blocking primitives:
Flags
Resource guards
Supported concurrency libraries:
threading (thread-based)
Free threading
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.
The pure-Python implementation is primarily designed for the GIL (sequential consistency) and targets x86-TSO or stronger (x86, x86-64, s390x, etc.) where ordering may be in question in the current free-threading implementation. If you need stricter guarantees, please prefer the build with extension modules when it is available.