1605. gopy pythread spec
What we port and what we drop
cpython/Python/thread.c is the cross-platform shim. It includes one
of thread_pthread.h, thread_pthread_stubs.h, thread_nt.h to get
the platform primitives. Go's runtime already provides goroutines,
preemption, and a scheduler, so the platform .h files are dropped
outright (they are listed as -- in 1602). We port only the
cross-platform layer.
In v0.1 the surface is intentionally narrow:
| C entry point | Go target | Notes |
|---|---|---|
PyThread_init_thread | pythread.Init | No-op. The Go runtime is already initialized. |
PyThread_get_stacksize | pythread.GetStacksize | Returns 0; per-interpreter stacksize lives in state once that exists (v0.7). |
PyThread_set_stacksize | pythread.SetStacksize | Returns -2 (not supported). Goroutines grow stacks dynamically. |
PY_TIMEOUT_MAX | pythread.TimeoutMax | Constant, microseconds. Matches the POSIX branch. |
| (new) goroutine handle | pythread.Start, (*Handle).Join | Replaces PyThread_start_new_thread plus the platform-specific join. |
| (new) thread ident | pythread.Ident, (*Handle).Ident | Unique per started handle. |
Deferred to later phases:
PyThread_acquire_lock_timed_with_retries. Depends onstate(for_PyThreadState_GET) andpytime(for deadlines). Lands whenstatelands in v0.3+.PyThread_ParseTimeoutArg. Depends on object protocol andpytime.PyThread_tss_*. Depends on the object model. v0.1 has no Python callers needing TSS; the parking-lot work uses goroutine-scoped state, not TSS.PyThread_GetInfo. Builds asys.thread_infostruct sequence; ports withsysmodin v0.7.
API
package pythread
const TimeoutMax int64 = ... // microseconds, mirrors PY_TIMEOUT_MAX
type Ident uint64
func Init() // no-op, kept for source-shape parity
func GetStacksize() int // always 0 for now
func SetStacksize(int) int // always -2 ("not supported")
type Handle struct { /* unexported */ }
// Start runs fn on a new goroutine and returns a Handle. fn must not
// be nil. The handle's Ident is unique for the lifetime of the
// process.
func Start(fn func()) *Handle
func (h *Handle) Ident() Ident
// Join blocks until fn returns. It returns the recovered panic value
// from fn, or nil if fn returned normally. Calling Join more than
// once is safe; subsequent calls return immediately with the same
// value.
func (h *Handle) Join() any
Why a Handle, not a raw ident
CPython surfaces threads through opaque unsigned long idents and
relies on the platform to provide join (or to leak). The Go runtime
does not expose a goroutine ident; it instead encourages capturing a
handle. We follow Go conventions here. The naming spec (1601) allows
this: identifiers translate to Go idioms while semantics stay aligned.
The Ident type is still useful for parking-lot bookkeeping
(the parking lot keys waiters by address, but uses the ident in
diagnostics), so we keep it as a typed uint64 minted from a global
atomic counter at Start time.
Concurrency notes
Startis safe to call concurrently.Joinsynchronizes-before any code that ran inside fn, in the Go memory model sense. We implement this with achan struct{}closed on completion.- Panics inside fn are recovered. The recovered value is delivered via
Join. This matches CPython behavior where a thread's exception is surfaced through threading.Thread.run, except we do it at the primitive layer because the threading module is high-level Python.
Tests
pythread/thread_test.go:
Initand the stacksize stubs return the documented values.StartthenJoinruns fn exactly once, in a separate goroutine.Identis unique across many Start calls.- A panicking fn surfaces the value through
Join. TimeoutMaxis positive and divides cleanly to nanoseconds without overflow (the C invariant:TimeoutMax * 1000fits in int64).