Skip to main content

latchkit

One-shot synchronization primitives for threads and asyncio: a count-down latch, a gate and a fire-once event, each with abort-with-reason, typed errors and timeouts that behave. Zero dependencies, fully typed.

Install

pip install latchkit

Usage

Wait for a fixed number of workers to finish before moving on:

import threading

from latchkit import CountDownLatch

latch = CountDownLatch(3)


def worker(i: int) -> None:
    with latch.arrival():  # counts down when the block exits, even on error
        ...  # do the work


for i in range(3):
    threading.Thread(target=worker, args=(i,)).start()

latch.wait()  # blocks until all three have counted down

wait takes a timeout and returns whether the latch opened in time; wait_or_raise raises LatchTimeoutError (a TimeoutError) instead:

if not latch.wait(timeout=5.0):
    print("still waiting")
latch.wait_or_raise(timeout=5.0)

When something goes wrong, abort instead of leaving everyone blocked. Every current and future waiter raises BrokenLatchError with your reason:

from latchkit import BrokenLatchError

latch.abort(RuntimeError("database unreachable"))
try:
    latch.wait()
except BrokenLatchError as error:
    print(error.reason)  # the RuntimeError, also chained as __cause__

A Gate is a manual switch that, once opened, stays open for every waiter. opening() guards a run-once initialisation: it opens on success and aborts on any failure (cancellation included), so waiters never hang on a crashed initialiser:

from latchkit import Gate

ready = Gate()

with ready.opening():
    ...  # load config, warm caches

ready.wait()  # returns at once, now and forever

A OneShotEvent fires exactly once and tells the caller who won the race:

from latchkit import OneShotEvent

claimed = OneShotEvent()
if claimed.set():  # True for the first caller only
    print("this thread won")

Every primitive has an asyncio twin with the same API. Signalling is thread-safe, so worker threads can release coroutines:

import asyncio

from latchkit import AsyncCountDownLatch


async def main() -> None:
    latch = AsyncCountDownLatch(4)
    loop = asyncio.get_running_loop()
    for _ in range(4):
        loop.run_in_executor(None, latch.count_down)
    await latch.wait(timeout=10)


asyncio.run(main())

The guide covers each primitive, abort and errors, asyncio and thread bridging, timeouts, and the exact memory-ordering guarantees.

API at a glance

Thread-based asyncio What it is
CountDownLatch(n) AsyncCountDownLatch(n) Opens after n signals: count_down, try_count_up, remaining, is_open, arrival()
Gate() AsyncGate() Opens once on demand: open, is_open, opening()
OneShotEvent() AsyncOneShotEvent() Fires once, names the winner: set, is_set

All six share wait(timeout), wait_or_raise(timeout), abort(reason), is_broken and abort_reason. Errors: LatchError, BrokenLatchError, LatchTimeoutError.

Why

The standard library has threading.Event, threading.Barrier, threading.Condition and their asyncio cousins, and none of them is a count-down latch:

  • threading.Barrier is cyclic and symmetric: every participant must both arrive and wait, so one thread cannot wait while N unrelated threads each signal once. If you need a rendezvous, use it; latchkit does not duplicate it.
  • threading.Event and asyncio.Event are a single resettable boolean with no count, no notion of failure and no winner. A clear() can make a late waiter miss the signal, and a crashed producer leaves waiters blocked forever. asyncio.Event is also not thread-safe, so a worker thread cannot set it.
  • threading.Condition is the right building block, which is why people hand-roll a counter plus a condition in every codebase and get the edge cases (counting past zero, waking everyone, failure, timeouts) subtly wrong.

The "wait for N things, then proceed, once" primitive is CountDownLatch in Java and CountdownEvent in .NET. latchkit is that primitive done once, correctly, for both threads and asyncio, plus the two one-shot shapes that come up next to it, and an abort path modelled on threading.Barrier.abort() so failures reach waiters instead of hanging them.

There is no CLI: these are in-process primitives, and a command line could not share one with anything.

License

MIT. See LICENSE.

Metadata

Release files for latchkit 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for latchkit 0.2.0
File Size Uploaded
latchkit-0.2.0.tar.gz 25.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for latchkit 0.2.0
File Interpreter ABI Platform
latchkit-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 41.2 kB

Release files / latchkit-0.2.0.tar.gz

Download URL latchkit-0.2.0.tar.gz
Size 25.5 kB
Tags Source
SHA-256 checksum
How to use checksums
581e39e392ac911b0b0ecc6bf2cb90ad028a9e3b4d6b618bb4132246c9e7adc7
BLAKE2b-256 checksum
How to use checksums
756fd417e6e9cc6f37d654d690ee557729f248c2f11a3505aa30c54bfcefa473
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.2

Release files / latchkit-0.2.0-py3-none-any.whl

Download URL latchkit-0.2.0-py3-none-any.whl
Size 15.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d0b881dcd9ae093295bbcfcc36ef440342d235fb74f1ac94587b85598be93690
BLAKE2b-256 checksum
How to use checksums
938a04bd01fab3e3110ae76a432fe43acd9782953a67ac528155c8baf25fec44
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.2

Release history Release notifications | RSS feed

1.0.0

2 release files

This release

0.2.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page