Commit 296dfef6 authored by Tim Peters's avatar Tim Peters

Followup to bug #1069160.

PyThreadState_SetAsyncExc():  internal correctness changes wrt
refcount safety and deadlock avoidance.  Also added a basic test
case (relying on ctypes) and repaired the docs.
parent 09846f00
...@@ -702,9 +702,9 @@ interpreter lock has been created. ...@@ -702,9 +702,9 @@ interpreter lock has been created.
This function does not steal any references to \var{exc}. This function does not steal any references to \var{exc}.
To prevent naive misuse, you must write your own C extension To prevent naive misuse, you must write your own C extension
to call this. Must be called with the GIL held. to call this. Must be called with the GIL held.
Returns the number of thread states modified; if it returns a number Returns the number of thread states modified; this is normally one, but
greater than one, you're in trouble, and you should call it again will be zero if the thread id isn't found. If \var{exc} is
with \var{exc} set to \constant{NULL} to revert the effect. \constant{NULL}, the pending exception (if any) for the thread is cleared.
This raises no exceptions. This raises no exceptions.
\versionadded{2.3} \versionadded{2.3}
\end{cfuncdesc} \end{cfuncdesc}
......
...@@ -131,6 +131,75 @@ class ThreadTests(unittest.TestCase): ...@@ -131,6 +131,75 @@ class ThreadTests(unittest.TestCase):
threading._DummyThread)) threading._DummyThread))
del threading._active[tid] del threading._active[tid]
# PyThreadState_SetAsyncExc() is a CPython-only gimmick, not (currently)
# exposed at the Python level. This test relies on ctypes to get at it.
def test_PyThreadState_SetAsyncExc(self):
try:
import ctypes
except ImportError:
if verbose:
print "test_PyThreadState_SetAsyncExc can't import ctypes"
return # can't do anything
set_async_exc = ctypes.pythonapi.PyThreadState_SetAsyncExc
class AsyncExc(Exception):
pass
exception = ctypes.py_object(AsyncExc)
# `worker_started` is set by the thread when it's inside a try/except
# block waiting to catch the asynchronously set AsyncExc exception.
# `worker_saw_exception` is set by the thread upon catching that
# exception.
worker_started = threading.Event()
worker_saw_exception = threading.Event()
class Worker(threading.Thread):
def run(self):
self.id = thread.get_ident()
self.finished = False
try:
while True:
worker_started.set()
time.sleep(0.1)
except AsyncExc:
self.finished = True
worker_saw_exception.set()
t = Worker()
if verbose:
print " started worker thread"
t.start()
# Try a thread id that doesn't make sense.
if verbose:
print " trying nonsensical thread id"
result = set_async_exc(-1, exception)
self.assertEqual(result, 0) # no thread states modified
# Now raise an exception in the worker thread.
if verbose:
print " waiting for worker thread to get started"
worker_started.wait()
if verbose:
print " verifying worker hasn't exited"
self.assert_(not t.finished)
if verbose:
print " attempting to raise asynch exception in worker"
result = set_async_exc(t.id, exception)
self.assertEqual(result, 1) # one thread state modified
if verbose:
print " waiting for worker to say it caught the exception"
worker_saw_exception.wait(timeout=10)
self.assert_(t.finished)
if verbose:
print " all OK -- joining worker"
if t.finished:
t.join()
# else the thread is still running, and we have no way to kill it
def test_main(): def test_main():
test.test_support.run_unittest(ThreadTests) test.test_support.run_unittest(ThreadTests)
......
...@@ -78,6 +78,15 @@ Build ...@@ -78,6 +78,15 @@ Build
- Bug #1530448, ctypes buld failure on Solaris 10 was fixed. - Bug #1530448, ctypes buld failure on Solaris 10 was fixed.
C API
-----
- Bug #1069160. Internal correctness changes were made to
``PyThreadState_SetAsyncExc()``. A test case was added, and
the documentation was changed to state that the return value
is always 1 (normal) or 0 (if the specified thread wasn't found).
Mac Mac
--- ---
......
...@@ -342,28 +342,43 @@ PyThreadState_GetDict(void) ...@@ -342,28 +342,43 @@ PyThreadState_GetDict(void)
/* Asynchronously raise an exception in a thread. /* Asynchronously raise an exception in a thread.
Requested by Just van Rossum and Alex Martelli. Requested by Just van Rossum and Alex Martelli.
To prevent naive misuse, you must write your own extension To prevent naive misuse, you must write your own extension
to call this. Must be called with the GIL held. to call this, or use ctypes. Must be called with the GIL held.
Returns the number of tstates modified; if it returns a number Returns the number of tstates modified (normally 1, but 0 if `id` didn't
greater than one, you're in trouble, and you should call it again match any known thread id). Can be called with exc=NULL to clear an
with exc=NULL to revert the effect. This raises no exceptions. */ existing async exception. This raises no exceptions. */
int int
PyThreadState_SetAsyncExc(long id, PyObject *exc) { PyThreadState_SetAsyncExc(long id, PyObject *exc) {
PyThreadState *tstate = PyThreadState_GET(); PyThreadState *tstate = PyThreadState_GET();
PyInterpreterState *interp = tstate->interp; PyInterpreterState *interp = tstate->interp;
PyThreadState *p; PyThreadState *p;
int count = 0;
/* Although the GIL is held, a few C API functions can be called
* without the GIL held, and in particular some that create and
* destroy thread and interpreter states. Those can mutate the
* list of thread states we're traversing, so to prevent that we lock
* head_mutex for the duration.
*/
HEAD_LOCK(); HEAD_LOCK();
for (p = interp->tstate_head; p != NULL; p = p->next) { for (p = interp->tstate_head; p != NULL; p = p->next) {
if (p->thread_id != id) if (p->thread_id == id) {
continue; /* Tricky: we need to decref the current value
Py_CLEAR(p->async_exc); * (if any) in p->async_exc, but that can in turn
* allow arbitrary Python code to run, including
* perhaps calls to this function. To prevent
* deadlock, we need to release head_mutex before
* the decref.
*/
PyObject *old_exc = p->async_exc;
Py_XINCREF(exc); Py_XINCREF(exc);
p->async_exc = exc; p->async_exc = exc;
count += 1; HEAD_UNLOCK();
Py_XDECREF(old_exc);
return 1;
}
} }
HEAD_UNLOCK(); HEAD_UNLOCK();
return count; return 0;
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment