Author: Armin Rigo <ar...@tunes.org> Branch: Changeset: r3184:73a16cc62771 Date: 2019-01-28 23:33 +0100 http://bitbucket.org/cffi/cffi/changeset/73a16cc62771/
Log: Issue #362 Add "thread canary" objects which are deallocated if the PyThreadState is explicitly deallocated by CPython. If the thread shuts down first, then instead the canary is inserted in a zombie list. In that case, we clear and delete properly the PyThreadState at the next occasion. diff --git a/c/_cffi_backend.c b/c/_cffi_backend.c --- a/c/_cffi_backend.c +++ b/c/_cffi_backend.c @@ -7671,7 +7671,7 @@ init_cffi_tls(); if (PyErr_Occurred()) INITERROR; - init_cffi_tls_delete(); + init_cffi_tls_zombie(); if (PyErr_Occurred()) INITERROR; diff --git a/c/misc_thread_common.h b/c/misc_thread_common.h --- a/c/misc_thread_common.h +++ b/c/misc_thread_common.h @@ -5,10 +5,10 @@ struct cffi_tls_s { - /* The locally-made thread state. This is only non-null in case - we build the thread state here. It remains null if this thread - had already a thread state provided by CPython. */ - PyThreadState *local_thread_state; + /* The current thread's ThreadCanaryObj. This is only non-null in + case cffi builds the thread state here. It remains null if this + thread had already a thread state provided by CPython. */ + struct thread_canary_s *local_thread_canary; #ifndef USE__THREAD /* The saved errno. If the C compiler supports '__thread', then @@ -26,81 +26,245 @@ or misc_win32.h */ -/* issue #362: Py_Finalize() will free any threadstate around, so in - * that case we must not call PyThreadState_Delete() any more on them - * from cffi_thread_shutdown(). The following mess is to give a - * thread-safe way to know that Py_Finalize() started. +/* We try to keep the PyThreadState around in a thread not started by + * Python but where cffi callbacks occur. If we didn't do that, then + * the standard logic in PyGILState_Ensure() and PyGILState_Release() + * would create a new PyThreadState and completely free it for every + * single call. For some applications, this is a huge slow-down. + * + * As shown by issue #362, it is quite messy to do. The current + * solution is to keep the PyThreadState alive by incrementing its + * 'gilstate_counter'. We detect thread shut-down, and we put the + * PyThreadState inside a list of zombies (we can't free it + * immediately because we don't have the GIL at that point in time). + * We also detect other pieces of code (notably Py_Finalize()) which + * clear and free PyThreadStates under our feet, using ThreadCanaryObj. */ -#define TLS_DEL_LOCK() PyThread_acquire_lock(cffi_tls_delete_lock, WAIT_LOCK) -#define TLS_DEL_UNLOCK() PyThread_release_lock(cffi_tls_delete_lock) -static PyThread_type_lock cffi_tls_delete_lock = NULL; -static int cffi_tls_delete; -static PyObject *old_exitfunc; -static PyObject *cffi_tls_shutdown(PyObject *self, PyObject *args) +#define TLS_ZOM_LOCK() PyThread_acquire_lock(cffi_zombie_lock, WAIT_LOCK) +#define TLS_ZOM_UNLOCK() PyThread_release_lock(cffi_zombie_lock) +static PyThread_type_lock cffi_zombie_lock = NULL; + + +/* A 'canary' object is created in a thread when there is a callback + invoked, and that thread has no PyThreadState so far. It is an + object of reference count equal to 1, which is stored in the + PyThreadState->dict. Two things can occur then: + + 1. The PyThreadState can be forcefully cleared by Py_Finalize(). + Then thread_canary_dealloc() is called, and we have to cancel + the hacks we did to keep the PyThreadState alive. + + 2. The thread finishes. In that case, we put the canary in a list + of zombies, and at some convenient time later when we have the + GIL, we free all PyThreadStates in the zombie list. + + Some more fun comes from the fact that thread_canary_dealloc() can + be called at a point where the canary is in the zombie list already. + Also, the various pieces are freed at specific points in time, and + we must make sure not to access already-freed structures: + + - the struct cffi_tls_s is valid until the thread shuts down, and + then it is freed by cffi_thread_shutdown(). + + - the canary is a normal Python object, but we have a borrowed + reference to it from cffi_tls_s.local_thread_canary. + */ + +typedef struct thread_canary_s { + PyObject_HEAD + struct thread_canary_s *zombie_prev, *zombie_next; + PyThreadState *tstate; + struct cffi_tls_s *tls; +} ThreadCanaryObj; + +static PyTypeObject ThreadCanary_Type; /* forward */ +static ThreadCanaryObj cffi_zombie_head; + +static void +_thread_canary_detach_with_lock(ThreadCanaryObj *ob) { - /* the lock here will wait until any parallel cffi_thread_shutdown() - is done. Future cffi_thread_shutdown() won't touch their - PyThreadState any more, which are all supposed to be freed anyway - very soon after the present cffi_tls_shutdown() function is called. + /* must be called with both the GIL and TLS_ZOM_LOCK. */ + ThreadCanaryObj *p, *n; + p = ob->zombie_prev; + n = ob->zombie_next; + p->zombie_next = n; + n->zombie_prev = p; + ob->zombie_prev = NULL; + ob->zombie_next = NULL; +} + +static void +thread_canary_dealloc(ThreadCanaryObj *ob) +{ + /* this ThreadCanaryObj is being freed: if it is in the zombie + chained list, remove it. Thread-safety: 'zombie_next' amd + 'local_thread_canary' accesses need to be protected with + the TLS_ZOM_LOCK. */ - PyObject *ofn; - - TLS_DEL_LOCK(); - cffi_tls_delete = 0; /* Py_Finalize() called */ - TLS_DEL_UNLOCK(); - - ofn = old_exitfunc; - if (ofn == NULL) - { - Py_INCREF(Py_None); - return Py_None; + TLS_ZOM_LOCK(); + if (ob->zombie_next != NULL) { + //fprintf(stderr, "thread_canary_dealloc(%p): ZOMBIE\n", ob); + _thread_canary_detach_with_lock(ob); } else - { - old_exitfunc = NULL; - return PyObject_CallFunction(ofn, ""); + //fprintf(stderr, "thread_canary_dealloc(%p): not a zombie\n", ob); + + if (ob->tls != NULL) { + //fprintf(stderr, "thread_canary_dealloc(%p): was local_thread_canary\n", ob); + assert(ob->tls->local_thread_canary == ob); + ob->tls->local_thread_canary = NULL; } + TLS_ZOM_UNLOCK(); + + PyObject_Del((PyObject *)ob); } -static void init_cffi_tls_delete(void) +static void +thread_canary_make_zombie(ThreadCanaryObj *ob) { - static PyMethodDef mdef = { - "cffi_tls_shutdown", cffi_tls_shutdown, METH_NOARGS, - }; - PyObject *shutdown_fn; + /* This must be called without the GIL, but with the TLS_ZOM_LOCK. + It must be called at most once for a given ThreadCanaryObj. */ + ThreadCanaryObj *last; - cffi_tls_delete_lock = PyThread_allocate_lock(); - if (cffi_tls_delete_lock == NULL) - { - PyErr_SetString(PyExc_SystemError, - "can't allocate cffi_tls_delete_lock"); - return; + //fprintf(stderr, "thread_canary_make_zombie(%p)\n", ob); + if (ob->zombie_next) + Py_FatalError("cffi: ThreadCanaryObj is already a zombie"); + last = cffi_zombie_head.zombie_prev; + ob->zombie_next = &cffi_zombie_head; + ob->zombie_prev = last; + last->zombie_next = ob; + cffi_zombie_head.zombie_prev = ob; +} + +static void +thread_canary_free_zombies(void) +{ + /* This must be called with the GIL. */ + if (cffi_zombie_head.zombie_next == &cffi_zombie_head) + return; /* fast path */ + + while (1) { + ThreadCanaryObj *ob; + PyThreadState *tstate = NULL; + + TLS_ZOM_LOCK(); + ob = cffi_zombie_head.zombie_next; + if (ob != &cffi_zombie_head) { + tstate = ob->tstate; + //fprintf(stderr, "thread_canary_free_zombie(%p) tstate=%p\n", ob, tstate); + _thread_canary_detach_with_lock(ob); + if (tstate == NULL) + Py_FatalError("cffi: invalid ThreadCanaryObj->tstate"); + } + TLS_ZOM_UNLOCK(); + + if (tstate == NULL) + break; + PyThreadState_Clear(tstate); /* calls thread_canary_dealloc on 'ob', + but now ob->zombie_next == NULL. */ + PyThreadState_Delete(tstate); + //fprintf(stderr, "thread_canary_free_zombie: cleared and deleted tstate=%p\n", tstate); } + //fprintf(stderr, "thread_canary_free_zombie: end\n"); +} - shutdown_fn = PyCFunction_New(&mdef, NULL); - if (shutdown_fn == NULL) - return; +static void +thread_canary_register(PyThreadState *tstate) +{ + /* called with the GIL; 'tstate' is the current PyThreadState. */ + ThreadCanaryObj *canary; + PyObject *tdict; + struct cffi_tls_s *tls; + int err; - old_exitfunc = PySys_GetObject("exitfunc"); - if (PySys_SetObject("exitfunc", shutdown_fn) == 0) - cffi_tls_delete = 1; /* all ready */ - Py_DECREF(shutdown_fn); + /* first free the zombies, if any */ + thread_canary_free_zombies(); + + tls = get_cffi_tls(); + if (tls == NULL) + goto ignore_error; + + tdict = PyThreadState_GetDict(); + if (tdict == NULL) + goto ignore_error; + + canary = PyObject_New(ThreadCanaryObj, &ThreadCanary_Type); + //fprintf(stderr, "thread_canary_register(%p): tstate=%p tls=%p\n", canary, tstate, tls); + if (canary == NULL) + goto ignore_error; + canary->zombie_prev = NULL; + canary->zombie_next = NULL; + canary->tstate = tstate; + canary->tls = tls; + + err = PyDict_SetItemString(tdict, "cffi.thread.canary", (PyObject *)canary); + Py_DECREF(canary); + if (err < 0) + goto ignore_error; + + /* thread-safety: we have the GIL here, and 'tstate' is the one that + corresponds to our own thread. We are allocating a new 'canary' + and setting it up for our own thread, both in 'tdict' (which owns + the reference) and in 'tls->local_thread_canary' (which doesn't). */ + assert(Py_REFCNT(canary) == 1); + tls->local_thread_canary = canary; + tstate->gilstate_counter++; + /* ^^^ this means 'tstate' will never be automatically freed by + PyGILState_Release() */ + return; + + ignore_error: + PyErr_Clear(); +} + +static PyTypeObject ThreadCanary_Type = { + PyVarObject_HEAD_INIT(NULL, 0) + "_cffi_backend.thread_canary", + sizeof(ThreadCanaryObj), + 0, + (destructor)thread_canary_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_compare */ + 0, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + 0, /* tp_call */ + 0, /* tp_str */ + 0, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT, /* tp_flags */ +}; + +static void init_cffi_tls_zombie(void) +{ + cffi_zombie_head.zombie_next = &cffi_zombie_head; + cffi_zombie_head.zombie_prev = &cffi_zombie_head; + cffi_zombie_lock = PyThread_allocate_lock(); + if (cffi_zombie_lock == NULL) + PyErr_SetString(PyExc_SystemError, "can't allocate cffi_zombie_lock"); } static void cffi_thread_shutdown(void *p) { + /* this function is called from misc_thread_posix or misc_win32 + when a thread is about to end. */ struct cffi_tls_s *tls = (struct cffi_tls_s *)p; - if (tls->local_thread_state != NULL) { - /* - * issue #362: see comments above - */ - TLS_DEL_LOCK(); - if (cffi_tls_delete) - PyThreadState_Delete(tls->local_thread_state); - TLS_DEL_UNLOCK(); + /* thread-safety: this field 'local_thread_canary' can be reset + to NULL in parallel, protected by TLS_ZOM_LOCK. */ + TLS_ZOM_LOCK(); + if (tls->local_thread_canary != NULL) { + tls->local_thread_canary->tls = NULL; + thread_canary_make_zombie(tls->local_thread_canary); } + TLS_ZOM_UNLOCK(); + //fprintf(stderr, "thread_shutdown(%p)\n", tls); free(tls); } @@ -168,7 +332,6 @@ PyGILState_Ensure(). */ PyGILState_STATE result; - struct cffi_tls_s *tls; PyThreadState *ts = PyGILState_GetThisThreadState(); if (ts != NULL) { @@ -193,13 +356,9 @@ assert(ts == get_current_ts()); assert(ts->gilstate_counter >= 1); - /* Save the now-current thread state inside our 'local_thread_state' - field, to be removed at thread shutdown */ - tls = get_cffi_tls(); - if (tls != NULL) { - tls->local_thread_state = ts; - ts->gilstate_counter++; - } + /* Use the ThreadCanary mechanism to keep 'ts' alive until the + thread really shuts down */ + thread_canary_register(ts); return result; } _______________________________________________ pypy-commit mailing list pypy-commit@python.org https://mail.python.org/mailman/listinfo/pypy-commit