Index: Doc/c-api/init.rst =================================================================== --- Doc/c-api/init.rst (revision 68340) +++ Doc/c-api/init.rst (working copy) @@ -780,6 +780,43 @@ .. versionadded:: 2.3 + +Asynchronous Notifications +========================== + +A mechanism is provided to make asynchronous notifications to the the main +interpreter thread. These notifications take the form of a function +pointer and a void argument. + +.. index:: single: setcheckinterval() (in module sys) + +Every check interval, when the interpreter lock is released and reacquired, +python will also call any such provided functions. This can be used for +example by asynchronous IO handlers. The notification can be scheduled +from a worker thread and the actuall call than made at the earliest +convenience by the main thread where it has possession of the global +interpreter lock and can perform any Python API calls. + +.. cfunction:: void Py_AddPendingCall( int (*func)(void *), void *arg) ) + + .. index:: single: Py_AddPendingCall() + + Post a notification to the Python main thread. If successful, + \*:attr`func` will be called with the argument :attr:`arg` at the earliest + convenience. \*:attr:`func` must can use the Python API and can take any + action such as setting object attributes to signal IO completion. + It must return 0 on success, or -1 signalling an exception. + + This function returns 0 on success in which case the notification has been + scheduled. Otherwise, for example if the notification buffer is full, + it returns -1 without setting any exception. + + This function can be called on any thread. + + .. versionadded:: 2.7 + + + .. _profiling: Profiling and Tracing Index: Doc/whatsnew/2.7.rst =================================================================== --- Doc/whatsnew/2.7.rst (revision 68340) +++ Doc/whatsnew/2.7.rst (working copy) @@ -60,7 +60,12 @@ .. ======================================================================== +Kristján Valur Jónsson, issue 4293 +Py_AddPendingCall is now thread safe. This allows any worker thread +to submit notifications to the python main thread. This is particularly +useful for asynchronous IO operations. + Other Language Changes ====================== Index: Python/ceval.c =================================================================== --- Python/ceval.c (revision 68340) +++ Python/ceval.c (working copy) @@ -213,6 +213,7 @@ #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 @@ 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,127 @@ #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 +*/ - XXX WARNING! ASYNCHRONOUSLY EXECUTING CODE! +#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 */ + +int +Py_AddPendingCall(int (*func)(void *), void *arg) +{ + int i, j, result=0; + + /* 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. + */ + for (i = 0; i<100; i++) { + if (PyThread_acquire_lock(pending_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; + PyThread_release_lock(pending_lock); + return result; +} + +int +Py_MakePendingCalls(void) +{ + int i; + if (!pending_lock) + /* initial allocation of the lock */ + pending_lock = PyThread_allocate_lock(); + + /* only service pending calls on main thread */ + if (main_thread && PyThread_get_thread_ident() != main_thread) + return 0; + /* perform a bounded number of calls, in case of recursion */ + for (i=0; i