@@ -77,53 +77,6 @@ TaggedPyObject tagIfDeferred(PyObject* obj);
7777
7878namespace {
7979
80- // RAII wrapper for PyThreadState* similar to Ref<>.
81- // Owns a PyThreadState created via PyThreadState_New and frees it via
82- // Clear + Delete/DeleteCurrent. Move-only, like Ref.
83- class PyThreadStateHandle {
84- public:
85- explicit PyThreadStateHandle (PyThreadState* tstate) : ptr_{tstate} {}
86-
87- ~PyThreadStateHandle () {
88- reset (nullptr );
89- }
90-
91- PyThreadStateHandle (PyThreadStateHandle&& other) noexcept : ptr_{other.ptr_ } {
92- other.ptr_ = nullptr ;
93- }
94-
95- PyThreadStateHandle (const PyThreadStateHandle&) = delete ;
96- PyThreadStateHandle& operator =(const PyThreadStateHandle&) = delete ;
97- PyThreadStateHandle& operator =(PyThreadStateHandle&& other) noexcept {
98- reset (nullptr );
99- ptr_ = other.ptr_ ;
100- other.ptr_ = nullptr ;
101- return *this ;
102- }
103-
104- PyThreadState* release () {
105- PyThreadState* tmp = ptr_;
106- ptr_ = nullptr ;
107- return tmp;
108- }
109-
110- void reset (PyThreadState* tstate = nullptr ) {
111- if (ptr_ != nullptr ) {
112- // Caller must hold GIL when freeing a threadstate.
113- PyThreadState_Clear (ptr_);
114- if (ptr_ == PyThreadState_Get ()) {
115- PyThreadState_DeleteCurrent ();
116- } else {
117- PyThreadState_Delete (ptr_);
118- }
119- }
120- ptr_ = tstate;
121- }
122-
123- private:
124- PyThreadState* ptr_{nullptr };
125- };
126-
12780// RAII equivalent of the Py_BEGIN_ALLOW_THREADS / Py_END_ALLOW_THREADS macro
12881// pair. Saves and releases the current thread state on construction, and
12982// restores it (reacquiring the GIL) on destruction.
@@ -993,6 +946,16 @@ std::string unitFullname(BorrowedRef<> unit) {
993946 return codeFullname (iter->second ->func_module , code);
994947}
995948
949+ PyThreadState* acquireCompileWorkerThreadState (PyInterpreterState* interp) {
950+ // Create the state on its owning worker so thread-local runtime state is
951+ // initialized for the thread that uses it. Passing the interpreter avoids
952+ // the subinterpreter-unsafe PyGILState_Ensure().
953+ PyThreadState* tstate = PyThreadState_New (interp);
954+ JIT_THROW_IF (tstate == nullptr , " Failed to allocate worker thread state" );
955+ PyEval_AcquireThread (tstate);
956+ return tstate;
957+ }
958+
996959// Load the preloader for a given function or code object. If it doesn't exist
997960// yet, then preload the function and return the new preloader.
998961//
@@ -1101,18 +1064,18 @@ std::pair<Result, Ref<>> tryCompilePreloaded(Ref<>&& unit) {
11011064}
11021065
11031066void compile_worker_thread (
1104- PyThreadState* initial_tstate ,
1067+ PyInterpreterState* interp ,
11051068 std::shared_ptr<ThreadedCompileContext> context,
11061069 std::shared_ptr<hir::IsolatedPreloaders> isolated) {
11071070 JIT_DLOG (" Started compile worker in thread {}" , std::this_thread::get_id ());
11081071
1072+ PyThreadState* tstate = acquireCompileWorkerThreadState (interp);
1073+
11091074 // Publish the context and isolated preload manager via TLS so that
11101075 // compileRunning()/preloaderManager() work on this worker.
11111076 hir::setThreadLocalPreloaderManager (isolated->manager ());
11121077
1113- // Acquire GIL with the pre-created worker threadstate.
1114- PyEval_AcquireThread (initial_tstate);
1115- context->beginWorker (initial_tstate);
1078+ context->beginWorker (tstate);
11161079
11171080 {
11181081 // Release the GIL for the lifetime of this scope, saving our tstate so it
@@ -1158,15 +1121,16 @@ void compile_worker_thread(
11581121 attempts,
11591122 retries);
11601123 }
1161- // GIL reacquired here, restoring threaded_compile_tstate as current.
1124+ // Leaving the scope reattached the worker state and reacquired the GIL when
1125+ // it is enabled.
11621126 context->endWorker ();
11631127
11641128 // Clear TLS for preload manager (context TLS already cleared by endWorker).
11651129 hir::setThreadLocalPreloaderManager (nullptr );
11661130
1167- // Now we hold GIL again with initial_tstate as current.
1168- // Clean up the worker threadstate .
1169- PyThreadState_Clear (PyThreadState_Get () );
1131+ // The worker state is current and attached, so it can be cleared and
1132+ // deleted by its owning thread .
1133+ PyThreadState_Clear (tstate );
11701134 PyThreadState_DeleteCurrent ();
11711135}
11721136
@@ -1193,34 +1157,20 @@ bool multithread_compile_units_preloaded(
11931157 // kept alive by the workers
11941158 auto compilation = std::make_shared<ThreadedCompileContext>(std::move (units));
11951159
1196- // Pre-create a PyThreadState for each worker while holding the GIL.
1197- // This avoids needing the GIL to be held inside the worker just to call
1198- // PyThreadState_New, and lets workers block on GIL acquisition until we
1199- // release it. Use RAII handle similar to Ref<> for exception safety.
12001160 PyInterpreterState* interp = ThreadedCompileContext::interpreter ();
1201- std::vector<PyThreadStateHandle> worker_tstates;
1202- worker_tstates.reserve (worker_count);
1203- for (size_t i = 0 ; i < worker_count; i++) {
1204- auto * tstate = PyThreadState_New (interp);
1205- JIT_CHECK (tstate != nullptr , " Failed to allocate thread state" );
1206- worker_tstates.emplace_back (tstate);
1207- }
12081161
12091162 // Track the worker threads on the module state so the runtime can wait for
12101163 // them to finish (e.g. during finalization) if we don't get to join them
12111164 // here.
12121165 auto * mod_state = cinderx::getModuleState ();
12131166 std::vector<std::thread>& worker_threads = mod_state->compile_worker_threads ;
1214- for (auto & worker_tstate : worker_tstates) {
1215- // Transfer ownership to the worker thread via release().
1216- PyThreadState* raw = worker_tstate.release ();
1167+ for (size_t i = 0 ; i < worker_count; i++) {
12171168 worker_threads.emplace_back (
1218- compile_worker_thread, raw , compilation, isolated);
1169+ compile_worker_thread, interp , compilation, isolated);
12191170 }
1220- // Ensure that no worker threads start compiling until they are all created,
1221- // in case something else in the process has hooked thread creation to run
1222- // arbitrary code (the worker threads need the GIL to initialize their thread
1223- // state).
1171+ // Keep the coordinator attached until every worker is launched in case
1172+ // thread creation has been hooked to run arbitrary code. In GIL builds,
1173+ // workers wait in PyEval_AcquireThread() until this point.
12241174 compilation->releaseGil ();
12251175
12261176 mod_state->joinCompileWorkers ();
@@ -3777,7 +3727,7 @@ void processBackgroundCompile(
37773727// drained.
37783728void backgroundCompileWorkerLoop (
37793729 CompilerContext<Compiler>* jit_ctx,
3780- PyThreadState* initial_tstate ) {
3730+ PyInterpreterState* interp ) {
37813731 JIT_DLOG (
37823732 " Background compile worker thread started: {}" ,
37833733 std::this_thread::get_id ());
@@ -3788,7 +3738,7 @@ void backgroundCompileWorkerLoop(
37883738#endif
37893739 " cinderx_compile" );
37903740#endif
3791- PyEval_AcquireThread (initial_tstate );
3741+ PyThreadState* tstate = acquireCompileWorkerThreadState (interp );
37923742
37933743 ThreadedCompileContext bgContext;
37943744 BackgroundCompileRegistry& reg = jit_ctx->backgroundCompileRegistry ();
@@ -3819,9 +3769,9 @@ void backgroundCompileWorkerLoop(
38193769 finishBackgroundCompile (task->code );
38203770 }
38213771
3822- // Now we hold GIL again with initial_tstate as current.
3823- // Clean up the worker threadstate .
3824- PyThreadState_Clear (PyThreadState_Get () );
3772+ // The worker state is current and attached, so it can be cleared and
3773+ // deleted by its owning thread .
3774+ PyThreadState_Clear (tstate );
38253775 PyThreadState_DeleteCurrent ();
38263776
38273777 JIT_DLOG (
@@ -3837,27 +3787,22 @@ bool startBackgroundWorkerThread(
38373787 // _PyInterpreterState_GET()) while the GIL is still held.
38383788 jit_ctx->builtins ();
38393789
3840- // Create a dedicated PyThreadState for the background worker while
3841- // holding the GIL, so the worker can attach via PyEval_AcquireThread
3842- // instead of the sub-interpreter-unsafe PyGILState_Ensure.
3790+ // The worker creates its own thread state, because PyThreadState_New() binds
3791+ // mimalloc and biased-reference-counting state to the calling thread. The
3792+ // interpreter is guaranteed to still be alive when it does: reg.worker is
3793+ // published under reg.mutex here, and cancelBackgroundCompiles() joins it
3794+ // before the interpreter is torn down.
38433795 PyInterpreterState* interp = PyInterpreterState_Get ();
3844- PyThreadState* tstate = PyThreadState_New (interp);
3845- if (tstate == nullptr ) {
3846- throw CAPIError ();
3847- }
38483796
38493797 // Normally already registered by jit::initialize(), but re-check here since
38503798 // the worker is the widest source of locks held across a fork.
38513799 ensureForkHandlersRegistered ();
38523800
38533801 try {
3854- reg.worker = std::thread (backgroundCompileWorkerLoop, jit_ctx, tstate );
3802+ reg.worker = std::thread (backgroundCompileWorkerLoop, jit_ctx, interp );
38553803 reg.worker_started = true ;
38563804 } catch (const std::system_error& exn) {
38573805 JIT_LOG (" Failed to start background compile worker: {}" , exn.what ());
3858- // Clean up the thread state we created for the worker.
3859- PyThreadState_Clear (tstate);
3860- PyThreadState_Delete (tstate);
38613806 return false ;
38623807 }
38633808 return true ;
0 commit comments