Goroutine-style M:N concurrency for free-threaded Python 3.14t
Project description
pyroutine
Goroutine-style concurrency for free-threaded Python 3.14t (GIL disabled).
Pyroutines are greenlet-backed: cheap to spawn (millions), parked without holding an OS thread, and run in true parallel across cores — the same trio of properties that make Go's goroutines special, on a custom M:N runtime.
from pyroutine import spawn, Queue
def worker(q, n):
q.put(n * n) # ordinary blocking code — no async/await
q = Queue() # cooperative FIFO queue (mimics queue.Queue)
task = spawn(worker, q, 10) # launch as a pyroutine task
spawn(worker, q, 5)
print(q.get(), q.get()) # 100, 25
task.join() # wait for the task to finish
Requirements
Runs on a free-threaded build (python3.14t) with greenlet ≥ 3.5.1:
uv python pin 3.14t # -> .python-version: 3.14+freethreaded
uv venv --python 3.14t
uv sync
uv run --no-sync python your_script.py
Install
The PyPI distribution name is pyroutine-rt; the import name is pyroutine.
(pip install pyroutine / pyroutines grabs unrelated packages — use pyroutine-rt.)
pip install pyroutine-rt # then: from pyroutine import spawn, gather
# or with uv:
uv add pyroutine-rt
Install the latest unreleased code straight from git:
pip install "pyroutine-rt @ git+https://github.com/VigyatGoel/pyroutine.git"
Either way, do it inside a free-threaded python3.14t environment (see Requirements);
on a stock GIL build it imports but won't run pyroutines in parallel.
Benchmarks
Cross-library benchmark suite in benchmarks/ comparing pyroutine
against asyncio/threading/multiprocessing and FastAPI/Flask/aiohttp/httpx/requests
across four surfaces: concurrency model, HTTP server, HTTP client, spawn/memory scaling.
uv sync --group bench # competitor libs
brew install oha # external HTTP load tool (server suite)
uv run --no-sync python -m benchmarks.run_all
Results print and write to benchmarks/RESULTS.md + results.json. The server suite
skips with a hint if oha is absent. This dev box is 4 perf + 4 efficiency cores, so
CPU-parallel speedup caps ~4x.
API (v1)
| Thing | Use |
|---|---|
spawn(fn, *a, **kw) |
Launch a callable as a concurrent pyroutine task -> returns a Task handle |
gather(*tasks) |
Wait for all tasks to complete and return a list of their results |
TaskGroup |
Context manager for structured concurrency. Spawns tasks and gathers them on exit, raising ExceptionGroup if any fail |
Task |
Represents a concurrent task. Methods: .join(timeout=None), .result(timeout=None) (re-raises task exception), property .done |
Queue(maxsize=0) |
Cooperative FIFO queue. Methods: .put(x, timeout=None) (blocks if full), .get(timeout=None) (blocks if empty), .qsize(), .empty(), .full() |
sleep(s) |
Cooperative sleep — parks the pyroutine via a timer, freeing the worker thread |
yield_() |
Yield execution to other ready pyroutines on the current worker thread |
Socket(...) |
Cooperative non-blocking socket wrapping a raw socket. Parks on the netpoller |
poll_wait(fd, READ|WRITE) |
Low-level: park the pyroutine until a raw fd is ready |
run_blocking(fn, *a) |
Run an uninstrumented blocking call (file I/O, DNS, sync requests) on a helper thread pool |
set_max_procs(n) |
Set worker/hub thread count (default: os.cpu_count()) |
shutdown() |
Drain outstanding pyroutines and stop the runtime |
enable_preemption(time_slice=0.010, check_interval=1000) / disable_preemption() |
Opt-in preemptive time-slicing so a CPU-bound pyroutine that never yields can't starve others on the same worker |
http.App |
Decorator-based HTTP router: @app.get("/path"), @app.post("/path"), etc. Handles JSON serialization, status/headers tuples, chunked responses, and keep-alive |
See examples/: fanout.py, pipeline.py, echo_server.py, http_server_example.py.
How it works
Greenlets give cheap stackful parking — a pyroutine can suspend anywhere, mid-stack, holding no OS thread (~1 KB each). One hard greenlet constraint shapes everything: a greenlet can't be switched from a different OS thread than the one that created it (greenlets don't migrate). So the runtime is N independent per-core cooperative schedulers running in parallel:
- Worker (M) — one OS thread per processor (default
os.cpu_count()). Its main greenlet is a hub: a loop that runs ready pyroutines and, when one parks, switches to the next. - Pyroutine (G) — a greenlet, bound for life to the worker that started it.
- Pending queues — per-worker queues of not-yet-started pyroutines, assigned round-robin, with work-stealing (the only thing that can cross threads is a not-yet-started task).
- Park / wake — a parked pyroutine yields to its hub; waking it (possibly from another worker or the main thread) enqueues its greenlet on its owner worker's ready queue and signals that worker. Values and wakeups cross threads, greenlets don't.
- Netpoller — a single
selectors(kqueue/epoll) thread. Socket I/O that would block parks the pyroutine and registers the fd; when it's ready the poller wakes the pyroutine. Thousands–millions of connections wait here on one thread, holding no workers — pyroutine's analogue of Go's integrated network poller.
Cooperative caveat (the one trade-off)
Only cooperative operations park cheaply: Queue operations, sleep(), yield_(),
and socket I/O via Socket (which parks on the netpoller). Those scale to
millions of parked pyroutines.
A blocking call the runtime doesn't know about — time.sleep, file I/O, DNS, or a
library using raw blocking sockets like requests — would freeze its worker and
every pyroutine on it. Wrap those in run_blocking, which offloads to a helper
thread pool:
import requests
from pyroutine import spawn, run_blocking
def fetch(url):
return run_blocking(requests.get, url).status_code # one pool thread per call
task = spawn(fetch, "https://httpbin.org/get")
status_code = task.result()
So native socket concurrency (via Socket) is cheap and unbounded; unwrapped
third-party blocking I/O is bounded by OS threads (~thousands). A gevent-style
monkeypatch to make requests use the netpoller transparently is on the roadmap.
Compared to Go
| Go goroutine | pyroutine | |
|---|---|---|
| Spawn | go f() |
spawn(f) |
| Cheap stackful parking | ✅ ~2 KB | ✅ ~2 KB (greenlet) |
| Millions concurrently parked | ✅ | ✅ |
| True multicore parallelism | ✅ | ✅ (GIL off) |
| Netpoller for sockets (millions of conns) | ✅ | ✅ (via Socket) |
Transparent I/O for any lib (e.g. requests) |
✅ (all I/O netpolled) | ⚠️ Socket is cooperative; others need run_blocking (monkeypatch on roadmap) |
| Preemption of CPU loops | ✅ | ✅ (opt-in via enable_preemption(); or yield_() manually) |
Roadmap
gevent-style socket monkeypatching so plain requests/socket code uses the
netpoller without run_blocking; select (with non-blocking default);
Context (cancellation/deadline); timers/ticker.
Development
uv run --no-sync pytest # full suite (run on python3.14t)
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pyroutine_rt-0.1.0.tar.gz.
File metadata
- Download URL: pyroutine_rt-0.1.0.tar.gz
- Upload date:
- Size: 22.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
19f5b8bef64cfc57d1515aa66a21abc7d4b0ce714c3cc883edf1abf39602c2c5
|
|
| MD5 |
bdde800e6d70254b4a5d7ed03c99529a
|
|
| BLAKE2b-256 |
82ed74b6bb94d22a44e92f12a2e391c841762bc2b6561408d62314a3e72322de
|
File details
Details for the file pyroutine_rt-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pyroutine_rt-0.1.0-py3-none-any.whl
- Upload date:
- Size: 25.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e2eca9165961b68006829e5fb1720a06731761d262b0d9e4e2b88616d6c247b1
|
|
| MD5 |
98171ef3bb37420af08f8e553bc8ee95
|
|
| BLAKE2b-256 |
36fb562007cdb8bc70a96ac03aeb0506cf242d19ade205434648d28eae50d3ab
|