Source code for katharos.concurrency.base_threading_backend
from __future__ import annotations
from abc import ABC, abstractmethod
from types import TracebackType
from typing import Any, Callable, Protocol, runtime_checkable
[docs]
@runtime_checkable
class AbstractLock(Protocol):
"""A 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.Lock`` satisfies
this protocol structurally, as does any green-thread equivalent.
"""
[docs]
def __enter__(self) -> bool:
"""Acquire the lock on entering a ``with`` block."""
...
[docs]
def __exit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: TracebackType | None,
/,
) -> bool | None:
"""Release the lock on leaving a ``with`` block."""
...
[docs]
@runtime_checkable
class AbstractCondition(Protocol):
"""A condition variable usable as a context manager.
Captures the subset of :class:`threading.Condition` that Katharos uses:
acquiring the underlying lock via ``with``, waiting for a notification,
and notifying waiters. ``threading.Condition`` satisfies this protocol
structurally.
"""
[docs]
def __enter__(self) -> bool:
"""Acquire the underlying lock on entering a ``with`` block."""
...
[docs]
def __exit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: TracebackType | None,
/,
) -> bool | None:
"""Release the underlying lock on leaving a ``with`` block."""
...
[docs]
def wait(self, timeout: float | None = None) -> bool:
"""Release the lock and block until notified (or ``timeout`` elapses).
Args:
timeout: Maximum seconds to wait, or ``None`` to wait forever.
Returns:
``False`` if the call returned because ``timeout`` elapsed, else
``True``.
"""
...
[docs]
def notify(self, n: int = 1) -> None:
"""Wake at most ``n`` threads waiting on this condition.
Args:
n: The maximum number of waiters to wake.
"""
...
[docs]
def notify_all(self) -> None:
"""Wake all threads waiting on this condition."""
...
[docs]
class BaseThreadHandle(ABC):
"""A handle to a unit of work started by a :class:`BaseThreadingBackend`.
Returned by :meth:`BaseThreadingBackend.spawn`; lets callers wait for the
spawned work to finish or check whether it is still running, without
exposing the underlying thread implementation.
"""
[docs]
@abstractmethod
def join(self, timeout: float | None = None) -> None:
"""Block until the spawned work finishes (or ``timeout`` elapses).
Args:
timeout: Maximum seconds to wait, or ``None`` to wait forever.
"""
raise NotImplementedError()
[docs]
@abstractmethod
def is_alive(self) -> bool:
"""Return whether the spawned work is still running.
Returns:
``True`` while the work is in progress, ``False`` once it has
finished.
"""
raise NotImplementedError()
[docs]
class BaseThreadingBackend(ABC):
"""Abstraction over a concurrency implementation.
A backend decouples Katharos' concurrency types (such as
:class:`~katharos.types.Lazy` and
:class:`~katharos.concurrency.csp.Channel`) from a specific threading
library. The default backend is built on the standard library
:mod:`threading` module, but an alternative (for example one backed by
``greenlet`` or ``gevent``) 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 :meth:`spawn`.
- **Synchronization primitives** via :meth:`create_lock` and
:meth:`create_condition`, so consumers coordinate using primitives that
cooperate with the backend's scheduler.
- **Context-local storage** via :meth:`create_local`, isolated per unit of
concurrency the backend schedules.
"""
[docs]
@abstractmethod
def spawn(self, fn: Callable[[], Any]) -> BaseThreadHandle:
"""Run a zero-argument callable concurrently.
Args:
fn: The work to run. Its return value is ignored; callers that
need a result should arrange for ``fn`` to store it.
Returns:
A :class:`BaseThreadHandle` for the started work.
"""
raise NotImplementedError()
[docs]
@abstractmethod
def create_lock(self) -> AbstractLock:
"""Create a new mutual-exclusion lock.
Returns:
A lock that cooperates with this backend's scheduler.
"""
raise NotImplementedError()
[docs]
@abstractmethod
def create_condition(self) -> AbstractCondition:
"""Create a new condition variable.
Returns:
A condition variable that cooperates with this backend's
scheduler.
"""
raise NotImplementedError()
[docs]
@abstractmethod
def create_local(self) -> Any:
"""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
:class:`~katharos.concurrency.csp.Go` launcher.
Using the backend's own context-local (rather than hardcoding
:class:`threading.local`) is what keeps that per-context state correct
when the backend multiplexes many tasks onto a single OS thread.
Returns:
An object supporting arbitrary attribute get/set/delete that is
isolated per scheduled context (for example
:class:`threading.local`).
"""
raise NotImplementedError()