[Python-Dev] Eureka! (Re: test_fork fails --with-thread)
Charles G Waldman
cgw@fnal.gov
Fri, 18 Aug 2000 23:31:06 -0500 (CDT)
Last month there was a flurry of discussion, around
http://www.python.org/pipermail/python-dev/2000-July/014208.html
about problems arising when combining threading and forking. I've
been reading through the python-dev archives and as far as I can tell
this problem has not yet been resolved.
Well, I think I understand what's going on and I have a patch that
fixes the problem.
Contrary to some folklore, you *can* use fork() in threaded code; you
just have to be a bit careful about locks...
Rather than write up a long-winded explanation myself, allow me to
quote:
-----------------------------------------------------------------
from "man pthread_atfork":
... recall that fork(2) duplicates the whole memory space,
including mutexes in their current locking state, but only the
calling thread: other threads are not running in the child
process. Thus, if a mutex is locked by a thread other than
the thread calling fork, that mutex will remain locked
forever in the child process, possibly blocking the execu-
tion of the child process.
and from http://www.lambdacs.com/newsgroup/FAQ.html#Q120
Q120: Calling fork() from a thread
> Can I fork from within a thread ?
Absolutely.
> If that is not explicitly forbidden, then what happens to the
> other threads in the child process ?
There ARE no other threads in the child process. Just the one that
forked. If your application/library has background threads that need
to exist in a forked child, then you should set up an "atfork" child
handler (by calling pthread_atfork) to recreate them. And if you use
mutexes, and want your application/library to be "fork safe" at all,
you also need to supply an atfork handler set to pre-lock all your
mutexes in the parent, then release them in the parent and child
handlers. Otherwise, ANOTHER thread might have a mutex locked when
one thread forks -- and because the owning thread doesn't exist in
the child, the mutex could never be released. (And, worse, whatever
data is protected by the mutex is in an unknown and inconsistent
state.)
-------------------------------------------------------------------
Below is a patch (I will also post this to SourceForge)
Notes on the patch:
1) I didn't make use of pthread_atfork, because I don't know how
portable it is. So, if somebody uses "fork" in a C extension there
will still be trouble.
2) I'm deliberately not cleaning up the old lock before creating
the new one, because the lock destructors also do error-checking.
It might be better to add a PyThread_reset_lock function to all the
thread_*.h files, but I'm hesitant to do this because of the amount
of testing required.
Patch:
Index: Modules/signalmodule.c
===================================================================
RCS file: /cvsroot/python/python/dist/src/Modules/signalmodule.c,v
retrieving revision 2.53
diff -c -r2.53 signalmodule.c
*** Modules/signalmodule.c 2000/08/03 02:34:44 2.53
--- Modules/signalmodule.c 2000/08/19 03:37:52
***************
*** 667,672 ****
--- 667,673 ----
PyOS_AfterFork(void)
{
#ifdef WITH_THREAD
+ PyEval_ReInitThreads();
main_thread = PyThread_get_thread_ident();
main_pid = getpid();
#endif
Index: Parser/intrcheck.c
===================================================================
RCS file: /cvsroot/python/python/dist/src/Parser/intrcheck.c,v
retrieving revision 2.39
diff -c -r2.39 intrcheck.c
*** Parser/intrcheck.c 2000/07/31 15:28:04 2.39
--- Parser/intrcheck.c 2000/08/19 03:37:54
***************
*** 206,209 ****
--- 206,212 ----
void
PyOS_AfterFork(void)
{
+ #ifdef WITH_THREAD
+ PyEval_ReInitThreads();
+ #endif
}
Index: Python/ceval.c
===================================================================
RCS file: /cvsroot/python/python/dist/src/Python/ceval.c,v
retrieving revision 2.191
diff -c -r2.191 ceval.c
*** Python/ceval.c 2000/08/18 19:53:25 2.191
--- Python/ceval.c 2000/08/19 03:38:06
***************
*** 142,147 ****
--- 142,165 ----
Py_FatalError("PyEval_ReleaseThread: wrong thread state");
PyThread_release_lock(interpreter_lock);
}
+
+ /* This function is called from PyOS_AfterFork to ensure that newly
+ created child processes don't hold locks referring to threads which
+ are not running in the child process. (This could also be done using
+ pthread_atfork mechanism, at least for the pthreads implementation) */
+ void
+ PyEval_ReInitThreads(void)
+ {
+ if (!interpreter_lock)
+ return;
+ /*XXX Can't use PyThread_free_lock here because it does too
+ much error-checking. Doing this cleanly would require
+ 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();
+ PyThread_acquire_lock(interpreter_lock, 1);
+ main_thread = PyThread_get_thread_ident();
+ }
#endif
/* Functions save_thread and restore_thread are always defined so