Concurrency Module¶
Concurrency backends and primitives for Katharos.
A BaseThreadingBackend abstracts thread spawning and synchronization
primitives so concurrency types (such as Lazy and
Channel) are not tied to a specific
threading library. ThreadingBackend is the standard-library default.
Threading Backends¶
BaseThreadingBackend¶
- class katharos.concurrency.BaseThreadingBackend[source]¶
Bases:
ABCAbstraction over a concurrency implementation.
A backend decouples Katharos’ concurrency types (such as
LazyandChannel) from a specific threading library. The default backend is built on the standard librarythreadingmodule, but an alternative (for example one backed bygreenletorgevent) can be supplied instead, as long as it provides compatible thread spawning and synchronization primitives.A backend exposes three capabilities:
Spawning lightweight units of work via
spawn().Synchronization primitives via
create_lock()andcreate_condition(), so consumers coordinate using primitives that cooperate with the backend’s scheduler.Context-local storage via
create_local(), isolated per unit of concurrency the backend schedules.
- abstractmethod spawn(fn)[source]¶
Run a zero-argument callable concurrently.
- Parameters:
fn (
Callable[[],Any]) – The work to run. Its return value is ignored; callers that need a result should arrange forfnto store it.- Return type:
- Returns:
A
BaseThreadHandlefor the started work.
- abstractmethod create_lock()[source]¶
Create a new mutual-exclusion lock.
- Return type:
- Returns:
A lock that cooperates with this backend’s scheduler.
- abstractmethod create_condition()[source]¶
Create a new condition variable.
- Return type:
- Returns:
A condition variable that cooperates with this backend’s scheduler.
- abstractmethod create_local()[source]¶
Create a context-local storage object.
The returned object stores attributes independently for each unit of concurrency the backend schedules: per OS thread for a thread-based backend, per green thread for a green-thread backend. It is used to hold execution-context-specific state, such as the active scopes of a
Golauncher.Using the backend’s own context-local (rather than hardcoding
threading.local) is what keeps that per-context state correct when the backend multiplexes many tasks onto a single OS thread.- Return type:
- Returns:
An object supporting arbitrary attribute get/set/delete that is isolated per scheduled context (for example
threading.local).
ThreadingBackend¶
- class katharos.concurrency.ThreadingBackend[source]¶
Bases:
BaseThreadingBackendA threading backend built on the standard library
threading.This is the default backend used by Katharos’ concurrency types. Spawned work runs on daemon
threading.Threadinstances, and the synchronization primitives are plainthreading.Lockandthreading.Conditionobjects.- spawn(fn)[source]¶
Run
fnon a new daemon thread.- Parameters:
fn (
Callable[[],Any]) – The zero-argument callable to run. Its return value is ignored.- Return type:
- Returns:
A
ThreadingHandlefor the started thread.
- create_lock()[source]¶
Create a new
threading.Lock.- Return type:
- Returns:
A standard-library lock.
- create_condition()[source]¶
Create a new
threading.Condition.- Return type:
- Returns:
A standard-library condition variable.
- create_local()[source]¶
Create a new
threading.local.- Return type:
- Returns:
Standard-library thread-local storage, isolated per OS thread.
default_backend¶
- katharos.concurrency.default_backend()[source]¶
Return the shared default threading backend.
Consumers that are not given an explicit backend use this one. The instance is stateless and safe to share across threads.
- Return type:
- Returns:
The process-wide default
ThreadingBackend.
Thread Handles¶
BaseThreadHandle¶
- class katharos.concurrency.BaseThreadHandle[source]¶
Bases:
ABCA handle to a unit of work started by a
BaseThreadingBackend.Returned by
BaseThreadingBackend.spawn(); lets callers wait for the spawned work to finish or check whether it is still running, without exposing the underlying thread implementation.
ThreadingHandle¶
- class katharos.concurrency.ThreadingHandle(thread)[source]¶
Bases:
BaseThreadHandleA
BaseThreadHandlebacked by athreading.Thread.- __init__(thread)[source]¶
Wrap an already-started thread.
- Parameters:
thread (
Thread) – The running thread this handle refers to.
Synchronization Protocols¶
AbstractLock¶
- class katharos.concurrency.AbstractLock(*args, **kwargs)[source]¶
Bases:
ProtocolA mutual-exclusion lock usable as a context manager.
Only the context-manager surface is required, which is all the concurrency primitives in Katharos rely on.
threading.Locksatisfies this protocol structurally, as does any green-thread equivalent.- __init__(*args, **kwargs)¶
AbstractCondition¶
- class katharos.concurrency.AbstractCondition(*args, **kwargs)[source]¶
Bases:
ProtocolA condition variable usable as a context manager.
Captures the subset of
threading.Conditionthat Katharos uses: acquiring the underlying lock viawith, waiting for a notification, and notifying waiters.threading.Conditionsatisfies this protocol structurally.- __exit__(exc_type, exc_value, traceback, /)[source]¶
Release the underlying lock on leaving a
withblock.
- __init__(*args, **kwargs)¶
CSP Primitives¶
CSP (Communicating Sequential Processes) style concurrency for Katharos.
This module provides Go-style concurrency primitives for coordinating concurrent computations in the CSP tradition:
CSPRuntime: a runtime that binds channels and goroutines to a single threading backend; thecspinstance is the default runtime.
Go: launch a callable concurrently, mirroring Go’sgo func().
Channel: a Go-style, thread-safe channel for communicating values between threads (paired withGo).
ChannelClosedError: raised on sending to or closing a closed channel.
select()/recv(): wait on whichever of several channels becomes ready first, mirroring Go’sselect;SelectResultdescribes the outcome.
CSPRuntime¶
- class katharos.concurrency.csp.CSPRuntime(backend=None)[source]¶
Bases:
objectA CSP runtime that binds channels and goroutines to one backend.
CSPRuntimebundles Katharos’ Communicating Sequential Processes primitives –ChannelandGo– around a singleBaseThreadingBackend. Channels created viaChanneland work launched viagoall run on (and synchronize through) that one backend, so the whole concurrency model is swappable in a single place.The two members are:
Channel: a backend-bound channel class. Subscript it with the element type and call it to construct a channel:ch = csp.Channel[int](capacity=1). Thebackendis supplied automatically, so callers never pass it.go: a backend-boundGolauncher. Call it to run work concurrently (csp.go(fn, *args)) or use it as awithblock for structured concurrency.
Examples
>>> from katharos.concurrency.threading_backend import ThreadingBackend >>> csp = CSPRuntime(ThreadingBackend()) >>> ch = csp.Channel[int](capacity=1) >>> _ = csp.go(ch.send, 42) >>> ch.recv() Success(42)
Used as a structured-concurrency scope, the block waits for everything launched inside it:
>>> ch = csp.Channel[int]() >>> with csp.go: ... _ = csp.go(ch.send, 1) ... received = ch.recv() >>> received Success(1)
- __init__(backend=None)[source]¶
Initialize a CSP runtime bound to a backend.
- Parameters:
backend (
BaseThreadingBackend|None) – The threading backend that channels and goroutines from this runtime use. Defaults to the shareddefault_backend().
- property backend: BaseThreadingBackend¶
The threading backend shared by this runtime’s channels and goroutines.
- katharos.concurrency.csp.csp = <katharos.concurrency.csp.csp.CSPRuntime object>¶
A CSP runtime that binds channels and goroutines to one backend.
CSPRuntimebundles Katharos’ Communicating Sequential Processes primitives –ChannelandGo– around a singleBaseThreadingBackend. Channels created viaChanneland work launched viagoall run on (and synchronize through) that one backend, so the whole concurrency model is swappable in a single place.The two members are:
Channel: a backend-bound channel class. Subscript it with the element type and call it to construct a channel:ch = csp.Channel[int](capacity=1). Thebackendis supplied automatically, so callers never pass it.go: a backend-boundGolauncher. Call it to run work concurrently (csp.go(fn, *args)) or use it as awithblock for structured concurrency.
Examples
>>> from katharos.concurrency.threading_backend import ThreadingBackend >>> csp = CSPRuntime(ThreadingBackend()) >>> ch = csp.Channel[int](capacity=1) >>> _ = csp.go(ch.send, 42) >>> ch.recv() Success(42)
Used as a structured-concurrency scope, the block waits for everything launched inside it:
>>> ch = csp.Channel[int]() >>> with csp.go: ... _ = csp.go(ch.send, 1) ... received = ch.recv() >>> received Success(1)
Channel¶
- class katharos.concurrency.csp.Channel(capacity=0, *, backend)[source]¶
Bases:
GenericA Go-style channel for communicating values between threads.
A
Channelis a typed, thread-safe conduit. Producerssend()values and consumersrecv()them; both operations block to coordinate the two sides, mirroring Go’s channels.The
capacitycontrols buffering:Unbuffered (
capacity == 0, the default): everysend()blocks until another thread is ready torecv()the value, giving a synchronous hand-off (rendezvous).Buffered (
capacity > 0):send()only blocks once the buffer is full, andrecv()only blocks once it is empty.
Unlike Go,
recv()returns aResult:Success(value)while values are available,Failure(ChannelClosedError)once the channel is closed and drained, orFailure(ChannelTimeoutError)if a timeout is specified and expires. This makes “the channel is closed” a type-safe outcome rather than a second return value. Iterating over a channel yields its values until it is closed.Note
Every
send(),recv(), andclose()wakes all waiters (notify_all) to keep coordination correct under contention. This is O(number of waiters) per operation, so a single channel shared by a very large number of blocked senders/receivers trades throughput for simplicity.Examples
>>> from katharos.concurrency.threading_backend import ThreadingBackend >>> ch = Channel[int](capacity=1, backend=ThreadingBackend()) >>> ch.send(42) >>> ch.recv() Success(42) >>> ch.close() >>> ch.recv() Failure(ChannelClosedError('recv on closed channel'))
- __init__(capacity=0, *, backend)[source]¶
Initialize a channel.
- Parameters:
capacity (
int) – Buffer size.0(the default) makes the channel unbuffered, so each send blocks for a matching receive. A positive value buffers that many pending values.backend (
BaseThreadingBackend) – The threading backend whose condition variable coordinates senders and receivers.
- Raises:
ValueError – If
capacityis negative.
- property backend: BaseThreadingBackend¶
The threading backend coordinating this channel’s senders and receivers.
- send(value)[source]¶
Send a value into the channel, blocking as needed.
For a buffered channel this blocks while the buffer is full. For an unbuffered channel it blocks until another thread receives the value.
- Parameters:
value (
TypeVar(A)) – The value to send.- Raises:
ChannelClosedError – If the channel is (or becomes) closed before the value can be delivered.
- Return type:
- recv(timeout=None)[source]¶
Receive a value, blocking until one is available or the channel closes.
- Parameters:
timeout (
float|None) – Maximum seconds to wait for a value.None(default) blocks indefinitely. A positive float will return aChannelTimeoutErrorfailure if no value arrives in time.- Return type:
Result[ChannelClosedError|ChannelTimeoutError,TypeVar(A)]- Returns:
Success(value)when a value is received.Failure(ChannelClosedError)when the channel is closed and empty.Failure(ChannelTimeoutError)when the timeout expires.
Examples
>>> from katharos.concurrency.threading_backend import ThreadingBackend >>> ch = Channel[int](capacity=1, backend=ThreadingBackend()) >>> ch.send(42) >>> ch.recv() Success(42)
>>> ch = Channel[int](backend=ThreadingBackend()) >>> ch.close() >>> ch.recv() Failure(ChannelClosedError('recv on closed channel'))
Timeout on an empty channel:
>>> ch = Channel[int](backend=ThreadingBackend()) >>> ch.recv(timeout=0.01) Failure(ChannelTimeoutError('recv timed out after 0.01 seconds'))
- register_observer(callback)[source]¶
Register a callback notified when this channel changes state.
Used by
select()to learn when a value becomes available (or the channel closes) without polling. The callback is invoked while this channel’s internal lock is held, so it must do no more than set a flag and signal its own condition; it must never call back into this channel (doing so would deadlock the non-reentrant lock).
- unregister_observer(callback)[source]¶
Remove a callback previously registered with
register_observer().Removing a callback that is not registered is a no-op.
- close()[source]¶
Close the channel.
After closing,
send()raisesChannelClosedErrorandrecv()returns the remaining buffered values followed by aFailure(ChannelClosedError). Any blocked senders or receivers are woken.- Raises:
ChannelClosedError – If the channel is already closed.
- Return type:
Go¶
- class katharos.concurrency.csp.Go(backend)[source]¶
Bases:
objectLaunch callables concurrently, mimicking Go’s
go func()statement.A
Goinstance is callable:go(fn, *args, **kwargs)runsfn(*args, **kwargs)concurrently on a threading backend and returns immediately, just likego f(x, y)starts a goroutine in Go. The work runs on the instance’sBaseThreadingBackend, so the concurrency model is swappable (standard threads by default, or a green-thread backend if supplied).Used as a context manager,
Gobecomes a structured-concurrency scope: every call made inside thewithblock is tracked, and leaving the block blocks until all of that work has finished (similar to async.WaitGroup). Scopes are tracked per execution context (via the backend’s context-local storage) and nest correctly, so a singleGoinstance is safe to share across contexts. Calls made outside anywithblock are pure fire-and-forget and are not tracked.You normally reach a launcher through a
CSPRuntimeruntime (thecspdefault exposes one ascsp.go); construct your ownGoonly to pin a specific backend.Note
Like a goroutine, a callable launched here is fire-and-forget: its return value is discarded and an exception it raises does not propagate out of the scope. Use a
Lazyor aChannelto communicate results or failures back to the caller.Examples
>>> import threading >>> from katharos.concurrency.threading_backend import ThreadingBackend >>> go = Go(ThreadingBackend()) >>> done = threading.Event() >>> handle = go(done.set) >>> handle.join() >>> done.is_set() True
>>> results = [] >>> handle = go(results.append, 42) >>> handle.join() >>> results [42]
Used as a scope, the block waits for everything spawned inside it:
>>> lock = threading.Lock() >>> collected = [] >>> def collect(x): ... with lock: ... collected.append(x) >>> with go: ... _ = go(collect, 1) ... _ = go(collect, 2) >>> sorted(collected) [1, 2]
- __init__(backend)[source]¶
Initialize a launcher bound to a backend.
- Parameters:
backend (
BaseThreadingBackend) – The threading backend used to spawn work.
- __call__(fn, *args, **kwargs)[source]¶
Run
fn(*args, **kwargs)concurrently without blocking.If called inside a
withblock on this instance, the spawned work is tracked and joined when the block exits.- Parameters:
- Return type:
- Returns:
A
BaseThreadHandlefor the spawned work, so callers canjoinit. It may be ignored for fire-and-forget use.
- __enter__()[source]¶
Open a structured-concurrency scope on the calling context.
- Return type:
- Returns:
This launcher, so calls made on it inside the block are tracked.
- __exit__(exc_type, exc_value, traceback)[source]¶
Close the scope, blocking until all work spawned in it finishes.
The scope is joined even when the block exits because of an exception, so no spawned work outlives the block. The exception, if any, is allowed to propagate.
- Parameters:
exc_type (
type[BaseException] |None) – The exception type if the block raised, elseNone.exc_value (
BaseException|None) – The exception instance if the block raised, elseNone.traceback (
TracebackType|None) – The traceback if the block raised, elseNone.
- Return type:
select¶
- katharos.concurrency.csp.select(*cases, default=False, timeout=None)[source]¶
Wait for one of several channel operations to become ready.
Mirrors Go’s
select: given a number of cases (built withrecv()), block until one is ready and return it. When several are ready at once, the first in argument order wins. A closed channel counts as ready (its case yields aFailure(ChannelClosedError)).- Parameters:
*cases (
_RecvCase[TypeVar(A)]) – The receive cases to wait on, in priority order.default (
bool) – IfTrue, return immediately with a default result when no case is ready, instead of blocking. Mutually useful with, and takes precedence over,timeout.timeout (
float|None) – Maximum seconds to block waiting for a case.None(the default) blocks indefinitely.
- Returns:
the chosen case, the default branch, or a timeout.
- Return type:
SelectResult[TypeVar(A)]- Raises:
ValueError – If called with no cases and neither
defaultnortimeout, which would block forever.
Examples
>>> from katharos.concurrency.csp import csp, recv, select >>> a = csp.Channel[int](capacity=1) >>> b = csp.Channel[int](capacity=1) >>> b.send(99) >>> choice = select(recv(a), recv(b)) >>> choice.index, choice.value.unwrap() (1, 99)
With
default, an empty channel does not block:>>> select(recv(a), default=True) SelectResult(default)
- katharos.concurrency.csp.recv(channel)[source]¶
Build a receive case for
select().- Parameters:
channel (
Channel[TypeVar(A)]) – The channel to receive from when this case is chosen.- Return type:
_RecvCase[TypeVar(A)]- Returns:
A select case that, when ready, yields the result of receiving from
channel.
Examples
>>> from katharos.concurrency.csp import csp, recv, select >>> ch = csp.Channel[int](capacity=1) >>> ch.send(7) >>> choice = select(recv(ch)) >>> choice.index, choice.value.unwrap() (0, 7)
SelectResult¶
- class katharos.concurrency.csp.SelectResult(index=None, channel=None, value=None, is_default=False, is_timeout=False)[source]¶
Bases:
GenericThe outcome of a
select()call.Exactly one of three things happened: a case became ready (
index,channelandvalueare set), thedefaultbranch was taken (is_default), or the call timed out (is_timeout). In the latter two casesindex,channelandvalueare allNone.- index¶
Position (in argument order) of the chosen case, or
None.
- channel¶
The chosen case’s channel, or
None.
- is_default¶
Whether the
defaultbranch was taken.
- is_timeout¶
Whether the call timed out before any case was ready.