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

  • 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

Supported concurrency libraries:

All synchronization, communication, and non-blocking primitives are implemented entirely on effectively atomic operations, which gives an incredible speedup on PyPy 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, so aiologic also works when running in a free-threaded mode.