Skip to content

_thread as virtual threads in a new stdlib/, static or shared - #15

Closed
soasme wants to merge 5 commits into
mainfrom
greenthread
Closed

soasme wants to merge 5 commits into
mainfrom
greenthread

Conversation

@soasme

@soasme soasme commented Nov 5, 2026 •

Copy link
Copy Markdown
Owner

_thread as virtual threads, in a new stdlib/ directory. The module can be linked statically or loaded as a shared library. It uses no system threads, and spy.c and spy.h are unchanged from main.

What

  • stdlib/ holds the C modules of the standard library, one source file each in the native-module form.
    • make builds stdlib/libNAME.a and stdlib/NAME.so.
    • stdlib/spy-stdlib.h declares each module's init function.
    • spy links every module; spyc accepts either the .c or the .a file.
  • _thread provides CPython 3.15's interface for what threading needs, with CPython's error messages:
    • start_new_thread and start_joinable_thread with _ThreadHandle;
    • allocate_lock, lock and RLock;
    • get_ident, _count, stack_size and _shutdown, among others.
  • Semantics, following Mark Shannon's virtual-threads proposal for CPython (no PEP yet):
    • Every thread runs on the host's own system thread.
    • A thread switches only when it would block, on a held lock or a join of a thread that has not ended. There is no preemption.
    • A thread that blocks runs the queued threads on top of itself until its condition holds.
    • When no thread is left to run, a timed wait times out and an untimed wait raises RuntimeError instead of hanging.
    • Without continuations in the core, a thread can resume only after the threads above it end. So two threads handing a lock back and forth fail, and docs/threads.md section 2 lists every case that differs from CPython.
  • Implementation:
    • The module is Python, in stdlib/_thread.py.
    • tools/build_intrinsics.py embeds it into the generated stdlib/_thread-source.h.
    • stdlib/_thread.c is a 24-line wrapper that runs that source.
  • spy-std.c: once a native module's link table is linked, it is only read. ThreadSanitizer reported a race when several contexts loaded the same module at once.
  • Docs:
    • new docs/threads.md;
    • a manual paragraph on stdlib/;
    • a ROADMAP item: threads that switch anywhere they block, once calls stop recursing in C;
    • a Changelog line;
    • a Threads section in docs/benchmark.md.

Evidence

  • make test passes, including spec 419/419.

  • tests/std.py:

    • tests/thread_probe.py prints the same 40 lines under spy and under the pinned CPython 3.15.
    • tests/thread_virtual.py passes 14 spy-only checks: start order, no preemption, idents, nested waits resuming last first, the RuntimeError cases, and a thread that cannot wait for one below it.
  • tests/threads loads _thread.so into four contexts on four system threads at once. Each context runs virtual threads and is freed while one is still queued.

  • TLC on tests/tla/SpyThreads.tla (3 workers, budget 3): 71,820 distinct states, no error. Checked invariants:

    • only the top thread runs, and _current names it;
    • a queued thread has not started;
    • a wait ends only when its condition holds or nothing is left to run;
    • termination: the host's call always returns.

    Three planted bugs were each caught: a wait that hangs, a wrong ident on resume, and a wait that ends with its condition unmet.

  • make docs, make check-intrinsics and tests/tla_coverage.py pass.

  • make coverage on the earlier revision covered every engine line. spy.c is now identical to main, and cli.c changes by one line.

  • Local CI (tools/run_workflow.py, from a fresh clone): the test job (gcc and clang, with make sanitize), standard and archive pass.

  • Windows and macOS x86_64 are covered only by this PR's CI.

Cost

Measured on an M2 with macOS 26.5 and -O2; details in docs/benchmark.md.

Measure spy CPython
Import _thread 0.53 ms 0.01 ms
Start and join a thread 3.80 µs 18.1 µs
Acquire and release a free lock 0.51 µs 0.06 µs
  • Start-up against main over 12 alternating rounds of 60 runs: −0.4% wall time, with main against a copy of itself at −0.0%.
  • Callgrind: +0.12% instructions, a constant 3.3k per program spent registering the module.
  • spy grows by 16.6 KB, mostly the embedded source.

Open

  • AGENTS.md still says "Do not add further standard library modules". This PR follows the request for stdlib/ and stdlib parity, but leaves that rule unchanged.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Bq79MgongGBn3NvB5dYBoj

soasme and others added 5 commits November 6, 2026 12:46
docs/greenthread.md proposes several Python threads in one context on one
operating-system thread: parked threads switch on the heap, pinned ones
keep their C frames and run only from the top of the C stack; host
functions answer later through tickets; RuntimeError instead of a hang
when nothing can run. It starts after calls stop recursing in C.

tests/tla/SpyGreenThreads.tla checks the scheduler: 59,003 states, no
error (tla_check.py --only, in a Linux container). TLC rejects resuming a
thread under the top, freeing a lock that has waiters, preempting a pinned
thread, and leaving out the RuntimeError. make docs and tla_coverage.py
pass. Nothing in spy.c changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bq79MgongGBn3NvB5dYBoj
stdlib/_thread.c runs each Python thread on a system thread of its own;
threads take turns at one context through FIFO tickets under one mutex.
spy links it (stdlib/lib_thread.a); hosts load stdlib/_thread.so. The
core gains spy_thread_new/swap/free, which move one thread's error,
frames, handled exception, stack measure and call depth, and
spy_set_switch, polled with the interrupt and run at the next tick so no
builtin operation interleaves. The green-thread proposal becomes
docs/threads.md; its model is replaced by SpyThreads.tla.

make test passes; tests/std.py: _thread probe matches CPython 3.15
(40 lines). SpyThreads.tla: 25,665 states, no error; TLC rejects 7
planted bugs. Sanitizers and measurements follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bq79MgongGBn3NvB5dYBoj
ThreadSanitizer saw link_table's compare-and-swap write the table's api
field while another thread of a context read it; an already linked
table is now only loaded. TSan and ASan+UBSan clean on tests/threads,
the _thread probe and tests/api.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bq79MgongGBn3NvB5dYBoj
lock.acquire() and RLock.acquire() with no arguments wait for ever
without going through _timeout: a free lock's acquire and release take
0.47 us, down from 2.15 us. docs/benchmark.md gains a Threads section:
import 0.51 ms, start and join 27.4 us, a turn hand-off 3.66 us, and
start-up +2.3% on macOS from linking _thread (dyld binding its pthread
imports), +0.7% on Linux, +0.57% instructions. make test passes; the
_thread probe matches CPython (40 lines).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bq79MgongGBn3NvB5dYBoj
Every thread runs on the host's system thread and switches only where it
would block: a blocked thread runs the queued threads on top of itself
until what it waits for holds; with none left a timed wait times out and
a wait for ever raises RuntimeError. No preemption. The module is Python
(stdlib/_thread.py) embedded by tools/build_intrinsics.py into a C
wrapper, still linked statically or loaded as _thread.so; spy.c and
spy.h are back to main, and nothing needs -pthread.

make test passes: the _thread probe matches CPython (40 lines),
tests/thread_virtual.py passes 14 checks, tests/threads 4 contexts.
SpyThreads.tla: 71,820 states, no error; three planted bugs caught.
Start-up -0.4% wall on macOS (control -0.0%), +0.12% instructions;
start and join 3.80 us (was 27.4), a free lock 0.51 us.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bq79MgongGBn3NvB5dYBoj
@soasme soasme changed the title Green threads: a proposal and its scheduler model _thread as virtual threads in a new stdlib/, static or shared Nov 6, 2026
@soasme soasme closed this Nov 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant