Skip to content

Commit 68567af

Browse files
author
benjamin.peterson
committed
Merged revisions 68460 via svnmerge from
svn+ssh://pythondev@svn.python.org/python/trunk ........ r68460 | kristjan.jonsson | 2009-01-09 14:31:26 -0600 (Fri, 09 Jan 2009) | 1 line Issue 4293: Make Py_AddPendingCall() thread safe ........ git-svn-id: http://svn.python.org/projects/python/branches/py3k@68690 6015fed2-1504-0410-9fe1-9d1591cc4771
1 parent 663a351 commit 68567af

1 file changed

Lines changed: 146 additions & 22 deletions

File tree

Python/ceval.c

Lines changed: 146 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ PyEval_GetCallStats(PyObject *self)
205205
#include "pythread.h"
206206

207207
static PyThread_type_lock interpreter_lock = 0; /* This is the GIL */
208+
static PyThread_type_lock pending_lock = 0; /* for pending calls */
208209
static long main_thread = 0;
209210

210211
int
@@ -276,6 +277,7 @@ PyEval_ReInitThreads(void)
276277
adding a new function to each thread_*.h. Instead, just
277278
create a new lock and waste a little bit of memory */
278279
interpreter_lock = PyThread_allocate_lock();
280+
pending_lock = PyThread_allocate_lock();
279281
PyThread_acquire_lock(interpreter_lock, 1);
280282
main_thread = PyThread_get_thread_ident();
281283

@@ -348,19 +350,145 @@ PyEval_RestoreThread(PyThreadState *tstate)
348350
#ifdef WITH_THREAD
349351
Any thread can schedule pending calls, but only the main thread
350352
will execute them.
353+
There is no facility to schedule calls to a particular thread, but
354+
that should be easy to change, should that ever be required. In
355+
that case, the static variables here should go into the python
356+
threadstate.
351357
#endif
358+
*/
359+
360+
#ifdef WITH_THREAD
361+
362+
/* The WITH_THREAD implementation is thread-safe. It allows
363+
scheduling to be made from any thread, and even from an executing
364+
callback.
365+
*/
366+
367+
#define NPENDINGCALLS 32
368+
static struct {
369+
int (*func)(void *);
370+
void *arg;
371+
} pendingcalls[NPENDINGCALLS];
372+
static int pendingfirst = 0;
373+
static int pendinglast = 0;
374+
static volatile int pendingcalls_to_do = 1; /* trigger initialization of lock */
375+
static char pendingbusy = 0;
376+
377+
int
378+
Py_AddPendingCall(int (*func)(void *), void *arg)
379+
{
380+
int i, j, result=0;
381+
PyThread_type_lock lock = pending_lock;
382+
383+
/* try a few times for the lock. Since this mechanism is used
384+
* for signal handling (on the main thread), there is a (slim)
385+
* chance that a signal is delivered on the same thread while we
386+
* hold the lock during the Py_MakePendingCalls() function.
387+
* This avoids a deadlock in that case.
388+
* Note that signals can be delivered on any thread. In particular,
389+
* on Windows, a SIGINT is delivered on a system-created worker
390+
* thread.
391+
* We also check for lock being NULL, in the unlikely case that
392+
* this function is called before any bytecode evaluation takes place.
393+
*/
394+
if (lock != NULL) {
395+
for (i = 0; i<100; i++) {
396+
if (PyThread_acquire_lock(lock, NOWAIT_LOCK))
397+
break;
398+
}
399+
if (i == 100)
400+
return -1;
401+
}
402+
403+
i = pendinglast;
404+
j = (i + 1) % NPENDINGCALLS;
405+
if (j == pendingfirst) {
406+
result = -1; /* Queue full */
407+
} else {
408+
pendingcalls[i].func = func;
409+
pendingcalls[i].arg = arg;
410+
pendinglast = j;
411+
}
412+
/* signal main loop */
413+
_Py_Ticker = 0;
414+
pendingcalls_to_do = 1;
415+
if (lock != NULL)
416+
PyThread_release_lock(lock);
417+
return result;
418+
}
419+
420+
int
421+
Py_MakePendingCalls(void)
422+
{
423+
int i;
424+
int r = 0;
352425

353-
XXX WARNING! ASYNCHRONOUSLY EXECUTING CODE!
426+
if (!pending_lock) {
427+
/* initial allocation of the lock */
428+
pending_lock = PyThread_allocate_lock();
429+
if (pending_lock == NULL)
430+
return -1;
431+
}
432+
433+
/* only service pending calls on main thread */
434+
if (main_thread && PyThread_get_thread_ident() != main_thread)
435+
return 0;
436+
/* don't perform recursive pending calls */
437+
if (pendingbusy)
438+
return 0;
439+
pendingbusy = 1;
440+
/* perform a bounded number of calls, in case of recursion */
441+
for (i=0; i<NPENDINGCALLS; i++) {
442+
int j;
443+
int (*func)(void *);
444+
void *arg;
445+
446+
/* pop one item off the queue while holding the lock */
447+
PyThread_acquire_lock(pending_lock, WAIT_LOCK);
448+
j = pendingfirst;
449+
if (j == pendinglast) {
450+
func = NULL; /* Queue empty */
451+
} else {
452+
func = pendingcalls[j].func;
453+
arg = pendingcalls[j].arg;
454+
pendingfirst = (j + 1) % NPENDINGCALLS;
455+
}
456+
pendingcalls_to_do = pendingfirst != pendinglast;
457+
PyThread_release_lock(pending_lock);
458+
/* having released the lock, perform the callback */
459+
if (func == NULL)
460+
break;
461+
r = func(arg);
462+
if (r)
463+
break;
464+
}
465+
pendingbusy = 0;
466+
return r;
467+
}
468+
469+
#else /* if ! defined WITH_THREAD */
470+
471+
/*
472+
WARNING! ASYNCHRONOUSLY EXECUTING CODE!
473+
This code is used for signal handling in python that isn't built
474+
with WITH_THREAD.
475+
Don't use this implementation when Py_AddPendingCalls() can happen
476+
on a different thread!
477+
354478
There are two possible race conditions:
355-
(1) nested asynchronous registry calls;
356-
(2) registry calls made while pending calls are being processed.
357-
While (1) is very unlikely, (2) is a real possibility.
479+
(1) nested asynchronous calls to Py_AddPendingCall()
480+
(2) AddPendingCall() calls made while pending calls are being processed.
481+
482+
(1) is very unlikely because typically signal delivery
483+
is blocked during signal handling. So it should be impossible.
484+
(2) is a real possibility.
358485
The current code is safe against (2), but not against (1).
359486
The safety against (2) is derived from the fact that only one
360-
thread (the main thread) ever takes things out of the queue.
361-
362-
XXX Darn! With the advent of thread state, we should have an array
363-
of pending calls per thread in the thread state! Later...
487+
thread is present, interrupted by signals, and that the critical
488+
section is protected with the "busy" variable. On Windows, which
489+
delivers SIGINT on a system thread, this does not hold and therefore
490+
Windows really shouldn't use this version.
491+
The two threads could theoretically wiggle around the "busy" variable.
364492
*/
365493

366494
#define NPENDINGCALLS 32
@@ -370,16 +498,14 @@ static struct {
370498
} pendingcalls[NPENDINGCALLS];
371499
static volatile int pendingfirst = 0;
372500
static volatile int pendinglast = 0;
373-
static volatile int things_to_do = 0;
501+
static volatile int pendingcalls_to_do = 0;
374502

375503
int
376504
Py_AddPendingCall(int (*func)(void *), void *arg)
377505
{
378506
static volatile int busy = 0;
379507
int i, j;
380508
/* XXX Begin critical section */
381-
/* XXX If you want this to be safe against nested
382-
XXX asynchronous calls, you'll have to work harder! */
383509
if (busy)
384510
return -1;
385511
busy = 1;
@@ -394,7 +520,7 @@ Py_AddPendingCall(int (*func)(void *), void *arg)
394520
pendinglast = j;
395521

396522
_Py_Ticker = 0;
397-
things_to_do = 1; /* Signal main loop */
523+
pendingcalls_to_do = 1; /* Signal main loop */
398524
busy = 0;
399525
/* XXX End critical section */
400526
return 0;
@@ -404,14 +530,10 @@ int
404530
Py_MakePendingCalls(void)
405531
{
406532
static int busy = 0;
407-
#ifdef WITH_THREAD
408-
if (main_thread && PyThread_get_thread_ident() != main_thread)
409-
return 0;
410-
#endif
411533
if (busy)
412534
return 0;
413535
busy = 1;
414-
things_to_do = 0;
536+
pendingcalls_to_do = 0;
415537
for (;;) {
416538
int i;
417539
int (*func)(void *);
@@ -424,14 +546,16 @@ Py_MakePendingCalls(void)
424546
pendingfirst = (i + 1) % NPENDINGCALLS;
425547
if (func(arg) < 0) {
426548
busy = 0;
427-
things_to_do = 1; /* We're not done yet */
549+
pendingcalls_to_do = 1; /* We're not done yet */
428550
return -1;
429551
}
430552
}
431553
busy = 0;
432554
return 0;
433555
}
434556

557+
#endif /* WITH_THREAD */
558+
435559

436560
/* The interpreter's recursion limit */
437561

@@ -518,7 +642,7 @@ static int _Py_TracingPossible = 0;
518642
/* for manipulating the thread switch and periodic "stuff" - used to be
519643
per thread, now just a pair o' globals */
520644
int _Py_CheckInterval = 100;
521-
volatile int _Py_Ticker = 100;
645+
volatile int _Py_Ticker = 0; /* so that we hit a "tick" first thing */
522646

523647
PyObject *
524648
PyEval_EvalCode(PyCodeObject *co, PyObject *globals, PyObject *locals)
@@ -903,7 +1027,7 @@ PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
9031027
/* Do periodic things. Doing this every time through
9041028
the loop would add too much overhead, so we do it
9051029
only every Nth instruction. We also do it if
906-
``things_to_do'' is set, i.e. when an asynchronous
1030+
``pendingcalls_to_do'' is set, i.e. when an asynchronous
9071031
event needs attention (e.g. a signal handler or
9081032
async I/O handler); see Py_AddPendingCall() and
9091033
Py_MakePendingCalls() above. */
@@ -919,12 +1043,12 @@ PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
9191043
#ifdef WITH_TSC
9201044
ticked = 1;
9211045
#endif
922-
if (things_to_do) {
1046+
if (pendingcalls_to_do) {
9231047
if (Py_MakePendingCalls() < 0) {
9241048
why = WHY_EXCEPTION;
9251049
goto on_error;
9261050
}
927-
if (things_to_do)
1051+
if (pendingcalls_to_do)
9281052
/* MakePendingCalls() didn't succeed.
9291053
Force early re-execution of this
9301054
"periodic" code, possibly after

0 commit comments

Comments
 (0)