diff options
author | Kristján Valur Jónsson <kristjan@ccpgames.com> | 2009-01-09 20:31:26 (GMT) |
---|---|---|
committer | Kristján Valur Jónsson <kristjan@ccpgames.com> | 2009-01-09 20:31:26 (GMT) |
commit | 0e91938e5851173821f917bf19157dde5f946c09 (patch) | |
tree | eeb964dca6ec2b117daa32022769b9124fed09e3 /Python | |
parent | 84040dbe8170864ba673321ec7568974bdabf5a4 (diff) | |
download | cpython-0e91938e5851173821f917bf19157dde5f946c09.zip cpython-0e91938e5851173821f917bf19157dde5f946c09.tar.gz cpython-0e91938e5851173821f917bf19157dde5f946c09.tar.bz2 |
Issue 4293: Make Py_AddPendingCall() thread safe
Diffstat (limited to 'Python')
-rw-r--r-- | Python/ceval.c | 168 |
1 files changed, 146 insertions, 22 deletions
diff --git a/Python/ceval.c b/Python/ceval.c index 09501af..88483a5 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -213,6 +213,7 @@ PyEval_GetCallStats(PyObject *self) #include "pythread.h" static PyThread_type_lock interpreter_lock = 0; /* This is the GIL */ +static PyThread_type_lock pending_lock = 0; /* for pending calls */ static long main_thread = 0; int @@ -284,6 +285,7 @@ PyEval_ReInitThreads(void) adding a new function to each thread_*.h. Instead, just create a new lock and waste a little bit of memory */ interpreter_lock = PyThread_allocate_lock(); + pending_lock = PyThread_allocate_lock(); PyThread_acquire_lock(interpreter_lock, 1); main_thread = PyThread_get_thread_ident(); @@ -356,19 +358,145 @@ PyEval_RestoreThread(PyThreadState *tstate) #ifdef WITH_THREAD Any thread can schedule pending calls, but only the main thread will execute them. + There is no facility to schedule calls to a particular thread, but + that should be easy to change, should that ever be required. In + that case, the static variables here should go into the python + threadstate. #endif +*/ + +#ifdef WITH_THREAD + +/* The WITH_THREAD implementation is thread-safe. It allows + scheduling to be made from any thread, and even from an executing + callback. + */ + +#define NPENDINGCALLS 32 +static struct { + int (*func)(void *); + void *arg; +} pendingcalls[NPENDINGCALLS]; +static int pendingfirst = 0; +static int pendinglast = 0; +static volatile int pendingcalls_to_do = 1; /* trigger initialization of lock */ +static char pendingbusy = 0; + +int +Py_AddPendingCall(int (*func)(void *), void *arg) +{ + int i, j, result=0; + PyThread_type_lock lock = pending_lock; + + /* try a few times for the lock. Since this mechanism is used + * for signal handling (on the main thread), there is a (slim) + * chance that a signal is delivered on the same thread while we + * hold the lock during the Py_MakePendingCalls() function. + * This avoids a deadlock in that case. + * Note that signals can be delivered on any thread. In particular, + * on Windows, a SIGINT is delivered on a system-created worker + * thread. + * We also check for lock being NULL, in the unlikely case that + * this function is called before any bytecode evaluation takes place. + */ + if (lock != NULL) { + for (i = 0; i<100; i++) { + if (PyThread_acquire_lock(lock, NOWAIT_LOCK)) + break; + } + if (i == 100) + return -1; + } + + i = pendinglast; + j = (i + 1) % NPENDINGCALLS; + if (j == pendingfirst) { + result = -1; /* Queue full */ + } else { + pendingcalls[i].func = func; + pendingcalls[i].arg = arg; + pendinglast = j; + } + /* signal main loop */ + _Py_Ticker = 0; + pendingcalls_to_do = 1; + if (lock != NULL) + PyThread_release_lock(lock); + return result; +} + +int +Py_MakePendingCalls(void) +{ + int i; + int r = 0; - XXX WARNING! ASYNCHRONOUSLY EXECUTING CODE! + if (!pending_lock) { + /* initial allocation of the lock */ + pending_lock = PyThread_allocate_lock(); + if (pending_lock == NULL) + return -1; + } + + /* only service pending calls on main thread */ + if (main_thread && PyThread_get_thread_ident() != main_thread) + return 0; + /* don't perform recursive pending calls */ + if (pendingbusy) + return 0; + pendingbusy = 1; + /* perform a bounded number of calls, in case of recursion */ + for (i=0; i<NPENDINGCALLS; i++) { + int j; + int (*func)(void *); + void *arg; + + /* pop one item off the queue while holding the lock */ + PyThread_acquire_lock(pending_lock, WAIT_LOCK); + j = pendingfirst; + if (j == pendinglast) { + func = NULL; /* Queue empty */ + } else { + func = pendingcalls[j].func; + arg = pendingcalls[j].arg; + pendingfirst = (j + 1) % NPENDINGCALLS; + } + pendingcalls_to_do = pendingfirst != pendinglast; + PyThread_release_lock(pending_lock); + /* having released the lock, perform the callback */ + if (func == NULL) + break; + r = func(arg); + if (r) + break; + } + pendingbusy = 0; + return r; +} + +#else /* if ! defined WITH_THREAD */ + +/* + WARNING! ASYNCHRONOUSLY EXECUTING CODE! + This code is used for signal handling in python that isn't built + with WITH_THREAD. + Don't use this implementation when Py_AddPendingCalls() can happen + on a different thread! + There are two possible race conditions: - (1) nested asynchronous registry calls; - (2) registry calls made while pending calls are being processed. - While (1) is very unlikely, (2) is a real possibility. + (1) nested asynchronous calls to Py_AddPendingCall() + (2) AddPendingCall() calls made while pending calls are being processed. + + (1) is very unlikely because typically signal delivery + is blocked during signal handling. So it should be impossible. + (2) is a real possibility. The current code is safe against (2), but not against (1). The safety against (2) is derived from the fact that only one - thread (the main thread) ever takes things out of the queue. - - XXX Darn! With the advent of thread state, we should have an array - of pending calls per thread in the thread state! Later... + thread is present, interrupted by signals, and that the critical + section is protected with the "busy" variable. On Windows, which + delivers SIGINT on a system thread, this does not hold and therefore + Windows really shouldn't use this version. + The two threads could theoretically wiggle around the "busy" variable. */ #define NPENDINGCALLS 32 @@ -378,7 +506,7 @@ static struct { } pendingcalls[NPENDINGCALLS]; static volatile int pendingfirst = 0; static volatile int pendinglast = 0; -static volatile int things_to_do = 0; +static volatile int pendingcalls_to_do = 0; int Py_AddPendingCall(int (*func)(void *), void *arg) @@ -386,8 +514,6 @@ Py_AddPendingCall(int (*func)(void *), void *arg) static volatile int busy = 0; int i, j; /* XXX Begin critical section */ - /* XXX If you want this to be safe against nested - XXX asynchronous calls, you'll have to work harder! */ if (busy) return -1; busy = 1; @@ -402,7 +528,7 @@ Py_AddPendingCall(int (*func)(void *), void *arg) pendinglast = j; _Py_Ticker = 0; - things_to_do = 1; /* Signal main loop */ + pendingcalls_to_do = 1; /* Signal main loop */ busy = 0; /* XXX End critical section */ return 0; @@ -412,14 +538,10 @@ int Py_MakePendingCalls(void) { static int busy = 0; -#ifdef WITH_THREAD - if (main_thread && PyThread_get_thread_ident() != main_thread) - return 0; -#endif if (busy) return 0; busy = 1; - things_to_do = 0; + pendingcalls_to_do = 0; for (;;) { int i; int (*func)(void *); @@ -432,7 +554,7 @@ Py_MakePendingCalls(void) pendingfirst = (i + 1) % NPENDINGCALLS; if (func(arg) < 0) { busy = 0; - things_to_do = 1; /* We're not done yet */ + pendingcalls_to_do = 1; /* We're not done yet */ return -1; } } @@ -440,6 +562,8 @@ Py_MakePendingCalls(void) return 0; } +#endif /* WITH_THREAD */ + /* The interpreter's recursion limit */ @@ -514,7 +638,7 @@ static int _Py_TracingPossible = 0; /* for manipulating the thread switch and periodic "stuff" - used to be per thread, now just a pair o' globals */ int _Py_CheckInterval = 100; -volatile int _Py_Ticker = 100; +volatile int _Py_Ticker = 0; /* so that we hit a "tick" first thing */ PyObject * PyEval_EvalCode(PyCodeObject *co, PyObject *globals, PyObject *locals) @@ -832,7 +956,7 @@ PyEval_EvalFrameEx(PyFrameObject *f, int throwflag) /* Do periodic things. Doing this every time through the loop would add too much overhead, so we do it only every Nth instruction. We also do it if - ``things_to_do'' is set, i.e. when an asynchronous + ``pendingcalls_to_do'' is set, i.e. when an asynchronous event needs attention (e.g. a signal handler or async I/O handler); see Py_AddPendingCall() and Py_MakePendingCalls() above. */ @@ -848,12 +972,12 @@ PyEval_EvalFrameEx(PyFrameObject *f, int throwflag) #ifdef WITH_TSC ticked = 1; #endif - if (things_to_do) { + if (pendingcalls_to_do) { if (Py_MakePendingCalls() < 0) { why = WHY_EXCEPTION; goto on_error; } - if (things_to_do) + if (pendingcalls_to_do) /* MakePendingCalls() didn't succeed. Force early re-execution of this "periodic" code, possibly after |