Skip to main content

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 pointGo targetNotes
PyThread_init_threadpythread.InitNo-op. The Go runtime is already initialized.
PyThread_get_stacksizepythread.GetStacksizeReturns 0; per-interpreter stacksize lives in state once that exists (v0.7).
PyThread_set_stacksizepythread.SetStacksizeReturns -2 (not supported). Goroutines grow stacks dynamically.
PY_TIMEOUT_MAXpythread.TimeoutMaxConstant, microseconds. Matches the POSIX branch.
(new) goroutine handlepythread.Start, (*Handle).JoinReplaces PyThread_start_new_thread plus the platform-specific join.
(new) thread identpythread.Ident, (*Handle).IdentUnique per started handle.

Deferred to later phases:

  • PyThread_acquire_lock_timed_with_retries. Depends on state (for _PyThreadState_GET) and pytime (for deadlines). Lands when state lands in v0.3+.
  • PyThread_ParseTimeoutArg. Depends on object protocol and pytime.
  • 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 a sys.thread_info struct sequence; ports with sysmod in 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

  • Start is safe to call concurrently.
  • Join synchronizes-before any code that ran inside fn, in the Go memory model sense. We implement this with a chan 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:

  • Init and the stacksize stubs return the documented values.
  • Start then Join runs fn exactly once, in a separate goroutine.
  • Ident is unique across many Start calls.
  • A panicking fn surfaces the value through Join.
  • TimeoutMax is positive and divides cleanly to nanoseconds without overflow (the C invariant: TimeoutMax * 1000 fits in int64).