Skip to content

Commit 82ba503

Browse files
committed
Dispose Python thread state when native thread is detached
1 parent d47ac51 commit 82ba503

File tree

5 files changed

+40
-52
lines changed

5 files changed

+40
-52
lines changed

graalpython/com.oracle.graal.python.cext/src/pystate.c

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -177,21 +177,18 @@ PyGILState_Release(PyGILState_STATE oldstate)
177177
{
178178
if (oldstate == PyGILState_UNLOCKED) {
179179
GraalPyTruffleGILState_Release();
180-
/* In the GraalPyTruffleGILState_Release up-call, we cleaned-up the pointer saved in
181-
* Java level Python thread state to avoid setting it to NULL in Truffle thread disposal
182-
* code, because it is not clear if the native thread is guaranteed to still be around
183-
* when the Truffle thread is being disposed.
184-
*/
185-
tstate_current = NULL;
186180
}
187181
if (TRUFFLE_CONTEXT) {
188182
graalpy_gilstate_counter--;
189183
if (graalpy_gilstate_counter == 0 && graalpy_attached_thread) {
184+
GraalPyTruffleBeforeThreadDetach();
190185
(*TRUFFLE_CONTEXT)->detachCurrentThread(TRUFFLE_CONTEXT);
191186
graalpy_attached_thread = 0;
192187
/*
193-
* The thread state on the Java-side can get garbage collected after the thread is detached,
194-
* so we need to make sure to fetch a fresh pointer the next time we attach.
188+
* The thread state on the Java-side is cleared in GraalPyTruffleBeforeThreadDetach.
189+
* As part of that the tstate_current pointer should have been set to NULL to make
190+
* sure to fetch a fresh pointer the next time we attach. Just to be sure, we clear
191+
* it here too:
195192
*/
196193
tstate_current = NULL;
197194
}

graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_thread.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,21 +87,20 @@ def test_register_new_thread(self):
8787
name="TestThread",
8888
includes="#include <pthread.h>",
8989
code=r'''
90-
void* thread_entrypoint(void* arg) {
90+
void test_attach_detach(PyObject *callable) {
9191
// This check is important not just to check that the function works without the thread attached,
9292
// but also because the thread attaching logic in it can break the following PyGILState_Ensure call
9393
if (PyGILState_Check()) {
9494
PyErr_SetString(PyExc_RuntimeError, "Thread shouldn't be holding the GIL at this point");
9595
PyErr_WriteUnraisable(NULL);
96-
return NULL;
96+
return;
9797
}
98-
PyObject* callable = (PyObject*)arg;
9998
PyGILState_STATE gstate;
10099
gstate = PyGILState_Ensure();
101100
if (!PyGILState_Check()) {
102101
PyErr_SetString(PyExc_RuntimeError, "GIL not acquired");
103102
PyErr_WriteUnraisable(NULL);
104-
return NULL;
103+
return;
105104
}
106105
if (!PyObject_CallNoArgs(callable)) {
107106
PyErr_WriteUnraisable(callable);
@@ -113,8 +112,12 @@ def test_register_new_thread(self):
113112
if (PyGILState_Check()) {
114113
PyErr_SetString(PyExc_RuntimeError, "GIL not released");
115114
PyErr_WriteUnraisable(NULL);
116-
return NULL;
117115
}
116+
}
117+
void* thread_entrypoint(void* arg) {
118+
test_attach_detach((PyObject*)arg);
119+
test_attach_detach((PyObject*)arg);
120+
test_attach_detach((PyObject*)arg);
118121
return NULL;
119122
}
120123
PyObject* run_in_thread(PyObject* self, PyObject* callable) {

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/PythonLanguage.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1028,7 +1028,7 @@ protected void initializeThread(PythonContext context, Thread thread) {
10281028

10291029
@Override
10301030
protected void disposeThread(PythonContext context, Thread thread) {
1031-
context.disposeThread(thread);
1031+
context.disposeThread(thread, false);
10321032
}
10331033

10341034
public Shape getEmptyShape() {

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextPyStateBuiltins.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@
5050
import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.Py_ssize_t;
5151
import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.Void;
5252

53-
import com.oracle.graal.python.PythonLanguage;
5453
import com.oracle.graal.python.builtins.modules.cext.PythonCextBuiltins.CApiBuiltin;
5554
import com.oracle.graal.python.builtins.modules.cext.PythonCextBuiltins.CApiNullaryBuiltinNode;
5655
import com.oracle.graal.python.builtins.modules.cext.PythonCextBuiltins.CApiUnaryBuiltinNode;
@@ -103,10 +102,6 @@ abstract static class PyTruffleGILState_Release extends CApiNullaryBuiltinNode {
103102
static Object restore(
104103
@Cached GilNode gil) {
105104
gil.release(true);
106-
// The C API that makes this up-call should reset the thread local variable to NULL
107-
// and here, on the Java side, we can clean up the pointers, because there is no need
108-
// to reset the native thread local on Truffle thread disposal
109-
getContext(gil).getThreadState(PythonLanguage.get(gil)).resetNativeThreadLocalVarPointer();
110105
return PNone.NO_VALUE;
111106
}
112107
}
@@ -119,12 +114,22 @@ Object get(Object tstateCurrentPtr,
119114
@CachedLibrary("tstateCurrentPtr") InteropLibrary lib) {
120115
PythonThreadState pythonThreadState = getContext().getThreadState(getLanguage());
121116
if (!lib.isNull(tstateCurrentPtr)) {
122-
pythonThreadState.setNativeThreadLocalVarPointer(lib, tstateCurrentPtr);
117+
pythonThreadState.setNativeThreadLocalVarPointer(tstateCurrentPtr);
123118
}
124119
return PThreadState.getOrCreateNativeThreadState(pythonThreadState);
125120
}
126121
}
127122

123+
@CApiBuiltin(ret = Void, args = {}, call = Ignored)
124+
abstract static class PyTruffleBeforeThreadDetach extends CApiNullaryBuiltinNode {
125+
@Specialization
126+
@TruffleBoundary
127+
Object doIt() {
128+
getContext().disposeThread(Thread.currentThread(), true);
129+
return PNone.NO_VALUE;
130+
}
131+
}
132+
128133
@CApiBuiltin(ret = PyObjectBorrowed, args = {}, call = Direct)
129134
abstract static class PyThreadState_GetDict extends CApiNullaryBuiltinNode {
130135

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PythonContext.java

Lines changed: 15 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@
6565
import static com.oracle.graal.python.nodes.StringLiterals.T_WARNINGS;
6666
import static com.oracle.graal.python.nodes.truffle.TruffleStringMigrationHelpers.isJavaString;
6767
import static com.oracle.graal.python.util.PythonUtils.TS_ENCODING;
68-
import static com.oracle.graal.python.util.PythonUtils.initUnsafe;
6968
import static com.oracle.graal.python.util.PythonUtils.toTruffleStringUncached;
7069
import static com.oracle.graal.python.util.PythonUtils.tsLiteral;
7170

@@ -352,12 +351,6 @@ public static final class PythonThreadState {
352351
*/
353352
Object nativeThreadLocalVarPointer;
354353

355-
/**
356-
* Raw pointer to {@link #nativeThreadLocalVarPointer} if available, so that we can clean up
357-
* during thread disposal.
358-
*/
359-
long nativeThreadLocalVarRawPointer;
360-
361354
/* The global tracing function, set by sys.settrace and returned by sys.gettrace. */
362355
Object traceFun;
363356

@@ -537,7 +530,7 @@ public void setContextVarsContext(PContextVarsContext contextVarsContext) {
537530
this.contextVarsContext = contextVarsContext;
538531
}
539532

540-
public void dispose(PythonContext context) {
533+
public void dispose(PythonContext context, boolean canRunGuestCode) {
541534
// This method may be called twice on the same object.
542535

543536
/*
@@ -558,14 +551,11 @@ public void dispose(PythonContext context) {
558551
}
559552
/*
560553
* Write 'NULL' to the native thread-local variable used to store the PyThreadState
561-
* struct such that it cannot accidentally be reused. We can invoke LLVM only if we are
562-
* not shutting down the thread.
554+
* struct such that it cannot accidentally be reused. Since this is done as a
555+
* precaution, we just skip this if we cannot run guest code, because it may invoke
556+
* LLVM.
563557
*/
564-
if (nativeThreadLocalVarRawPointer != 0 && context.env.isNativeAccessAllowed()) {
565-
context.getUnsafe().putAddress(nativeThreadLocalVarRawPointer, 0);
566-
nativeThreadLocalVarRawPointer = 0;
567-
nativeThreadLocalVarPointer = null;
568-
} else if (nativeThreadLocalVarPointer != null && !isShuttingDown()) {
558+
if (nativeThreadLocalVarPointer != null && canRunGuestCode) {
569559
CStructAccess.WritePointerNode.writeUncached(nativeThreadLocalVarPointer, 0, context.getNativeNull());
570560
nativeThreadLocalVarPointer = null;
571561
}
@@ -636,22 +626,10 @@ public void setAsyncgenFirstIter(Object asyncgenFirstIter) {
636626
this.asyncgenFirstIter = asyncgenFirstIter;
637627
}
638628

639-
public void resetNativeThreadLocalVarPointer() {
640-
nativeThreadLocalVarRawPointer = 0;
641-
nativeThreadLocalVarPointer = null;
642-
}
643-
644-
public void setNativeThreadLocalVarPointer(InteropLibrary interop, Object ptr) {
629+
public void setNativeThreadLocalVarPointer(Object ptr) {
645630
// either unset or same
646631
assert nativeThreadLocalVarPointer == null || nativeThreadLocalVarPointer == ptr ||
647632
InteropLibrary.getUncached().isIdentical(nativeThreadLocalVarPointer, ptr, InteropLibrary.getUncached());
648-
if (interop.isPointer(ptr)) {
649-
try {
650-
this.nativeThreadLocalVarRawPointer = interop.asPointer(ptr);
651-
} catch (UnsupportedMessageException e) {
652-
throw CompilerDirectives.shouldNotReachHere(e);
653-
}
654-
}
655633
this.nativeThreadLocalVarPointer = ptr;
656634
}
657635
}
@@ -2191,7 +2169,7 @@ public void runShutdownHooks() {
21912169
@TruffleBoundary
21922170
private void disposeThreadStates() {
21932171
for (PythonThreadState ts : threadStateMapping.values()) {
2194-
ts.dispose(this);
2172+
ts.dispose(this, true);
21952173
}
21962174
threadStateMapping.clear();
21972175
}
@@ -2270,7 +2248,7 @@ private void joinPythonThreads() {
22702248
// they are still running some GraalPython code, if they are embedder threads
22712249
// that are not running GraalPython code anymore, they will just never receive
22722250
// PythonThreadKillException and continue as if nothing happened.
2273-
disposeThread(thread);
2251+
disposeThread(thread, true);
22742252
boolean isOurThread = runViaLauncher || thread.getThreadGroup() == threadGroup;
22752253
// Do not try so hard when running in embedded mode and the thread may not be
22762254
// running any GraalPython code anymore
@@ -2364,6 +2342,11 @@ public void killSystemThread(Thread thread) {
23642342
env.submitThreadLocal(new Thread[]{thread}, new ThreadLocalAction(true, false) {
23652343
@Override
23662344
protected void perform(ThreadLocalAction.Access access) {
2345+
// just in case the thread holds GIL
2346+
PythonContext ctx = PythonContext.get(null);
2347+
if (ctx.ownsGil()) {
2348+
ctx.releaseGil();
2349+
}
23672350
throw new PythonThreadKillException();
23682351
}
23692352
});
@@ -2630,7 +2613,7 @@ public synchronized void attachThread(Thread thread, ContextThreadLocal<PythonTh
26302613
threadStateMapping.put(thread, threadState.get(thread));
26312614
}
26322615

2633-
public synchronized void disposeThread(Thread thread) {
2616+
public synchronized void disposeThread(Thread thread, boolean canRunGuestCode) {
26342617
CompilerAsserts.neverPartOfCompilation();
26352618
// check if there is a live sentinel lock
26362619
PythonThreadState ts = threadStateMapping.get(thread);
@@ -2640,7 +2623,7 @@ public synchronized void disposeThread(Thread thread) {
26402623
}
26412624
ts.shutdown();
26422625
threadStateMapping.remove(thread);
2643-
ts.dispose(this);
2626+
ts.dispose(this, canRunGuestCode);
26442627
releaseSentinelLock(ts.sentinelLock);
26452628
getSharedMultiprocessingData().removeChildContextThread(PThread.getThreadId(thread));
26462629
}

0 commit comments

Comments
 (0)