You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Audit docs, drop dead code, add snippet linter (#65)
Fix two stale identifier references (Py_GIL_OWN, multi_executor fallback),
delete priv/_erlang_impl/_ssl.py and three uncalled py_util/0,1,2,3
exports, and repair a broken SharedDict example in docs/shared-dict.md.
Add tests for py:cast/4, py:async_gather/2 and py:dup_fd/1, plus
test/coverage_audit.md mapping every public API to its suite.
New scripts/lint_doc_snippets.escript validates py:Fn/N calls and
Python syntax in fenced blocks; wired into CI and a Makefile target.
- CPU-bound Python work that benefits from parallelism
437
-
- Long-running computations
438
-
- Need true concurrent Python execution
439
-
440
-
Use worker mode when:
441
-
- I/O-bound or short operations
442
-
- High call frequency
443
-
- Resource constraints
431
+
| Throughput (single context) |~400K/s |~100K/s |
432
+
| Parallelism (N contexts) | GIL-bound | Linear up to N cores |
433
+
| Resource usage | One pthread per context | One pthread + one subinterpreter per context |
434
+
435
+
## Pros and Cons
436
+
437
+
### Pros
438
+
439
+
-**True CPU parallelism.** Each context owns its GIL, so N contexts run on N cores at once. Worker mode serialises on the main GIL unless Python is built free-threaded (3.13t+).
440
+
-**Crash isolation.** A C-level fault in one subinterpreter leaves the others alive. Worker mode shares the main interpreter, so a corrupt module state can take everything down.
441
+
-**Clean namespace per context.** Each subinterpreter has its own `sys.modules`, so module-level state cannot bleed between contexts. Useful when running adversarial or untrusted code paths side by side.
442
+
-**Predictable scheduling.** Requests are dispatched via mutex/condvar IPC, not dirty schedulers, so OWN_GIL contexts will not be starved by other dirty NIF traffic.
443
+
444
+
### Cons
445
+
446
+
-**Python 3.14+ only.** Earlier versions have C-extension global-state bugs (`_decimal`, `numpy`, etc.) that crash inside subinterpreters. See [cpython#106078](https://github.com/python/cpython/issues/106078).
447
+
-**Higher per-call latency.**~4x the round-trip cost of worker mode (~10μs vs ~2.5μs) because every call crosses a mutex/condvar handoff to the dedicated thread.
448
+
-**Higher memory.** Each subinterpreter imports its own copy of every module. A 50 MB module set across 8 contexts is ~400 MB resident, not 50 MB.
449
+
-**C-extension compatibility is not universal.** Extensions must opt in via the multi-phase init protocol (PEP 489) and `Py_mod_multiple_interpreters`. Pure-Python and well-behaved C extensions work; older ones fail at import inside the subinterpreter.
450
+
-**No shared Python state.** Module globals, class definitions, and cached objects are per-interpreter. Use `py:state_store/2` (ETS-backed) or `erlang.send` for cross-context data.
451
+
-**Callback re-entry is restricted.** When Python in an OWN_GIL context calls `erlang.call`, the callback runs on a thread worker, not back on the OWN_GIL thread (which cannot suspend). Re-entrant Python -> Erlang -> *same* OWN_GIL context calls will not work; use a different context for the nested call, or use `erlang.async_call` from asyncio code.
452
+
-**Process-local envs do not span interpreters.** A `py_env_resource_t` is bound to the interpreter that created it. Reusing one across contexts returns `{error, env_wrong_interpreter}`.
453
+
454
+
### When to Use Each
455
+
456
+
Use **OWN_GIL** when:
457
+
458
+
- The workload is CPU-bound Python (ML inference, numpy/torch compute, parsing, codecs) and you want N-way parallelism per BEAM scheduler.
459
+
- You can pin the per-context memory budget and the modules in use are subinterpreter-safe.
460
+
- You are on Python 3.14+.
461
+
462
+
Use **worker** (default) when:
463
+
464
+
- You are on Python 3.12 or 3.13.
465
+
- Calls are short and frequent (every microsecond of overhead matters).
466
+
- You are running modules that are not subinterpreter-safe (some scientific stacks, older C extensions).
467
+
- You are already running free-threaded Python (3.13t+); worker mode gets parallelism for free without the per-interpreter memory cost.
468
+
469
+
### Common Pitfalls
470
+
471
+
-**Importing once is not enough.** Imports happen per subinterpreter. Pre-warming a worker context will not pre-warm the OWN_GIL contexts; do it inside each `py_context`.
472
+
-**Sharing Python objects across contexts.** Passing a `PyObject*` reference (via `py_state` or otherwise) between OWN_GIL contexts is undefined behaviour. Round-trip through Erlang terms or ETS-backed state.
473
+
-**Long-running tasks block the dispatcher.** A single OWN_GIL context processes one request at a time. If you have a 30-second compute job, parallelise across contexts; do not queue everything onto context 1.
474
+
-**Callback storms.** Heavy `erlang.call` use inside an OWN_GIL context routes to thread workers, which is fine, but the round-trip cost is then worker-style on top of OWN_GIL dispatch. For tight callback loops, prefer worker mode end-to-end.
- Callback re-entry to the same context is restricted (`erlang.call` from inside an OWN_GIL context routes to a thread worker, not back to that context)
113
+
114
+
For a fuller breakdown of OWN_GIL tradeoffs, common pitfalls, and a usage decision guide, see [OWN_GIL Internals: Pros and Cons](owngil_internals.md#pros-and-cons).
**py_context_process**: Gen_server that owns a Python context reference and handles call/eval/exec operations.
157
161
158
-
**Subinterpreter Thread Pool (C)**: Manages N threads, each with its own Python subinterpreter created with `Py_NewInterpreterFromConfig()` and `Py_GIL_OWN`.
162
+
**Subinterpreter Thread Pool (C)**: Manages N threads, each with its own Python subinterpreter created with `Py_NewInterpreterFromConfig()` and `PyInterpreterConfig_OWN_GIL`.
0 commit comments