The following explanation is for POSIX only.
This issue of executing code after forking and before execing in a multi-threaded process is not Python specific.
In the child, do not call any library functions after calling fork()
and before calling exec(). One of the library functions might use a
lock that was held in the parent at the time of the fork().
Besides the usual concerns such as locking shared data, a library
should be well behaved with respect to forking a child process when
only the thread that called fork() is running. The problem is that the
sole thread in the child process might try to grab a lock held by a
thread not duplicated in the child.
For example, assume that T1 is in the middle of printing something and
holds a lock for printf(), when T2 forks a new process. In the child
process, if the sole thread (T2) calls printf(), T2 promptly
deadlocks.
https://docs.oracle.com/cd/E19120-01/open.solaris/816-5137/gen-1/index.html
The fork( ) system call creates an exact duplicate of the address
space from which it is called, resulting in two address spaces
executing the same code.
Suppose that one of the other threads (any thread other than the one
doing the fork( )) has the job of deducting money from your checking
account.
POSIX defined the behavior of fork( ) in the presence of threads to
propagate only the forking thread.
If the other thread has a mutex locked, the mutex will be locked in
the child process, but the lock owner will not exist to unlock it.
Therefore, the resource protected by the lock will be permanently
unavailable.
The fact that there may be mutexes outstanding only becomes a problem
if your code attempts to lock a mutex that could be locked by another
thread at the time of the fork( ). This means that you cannot call
outside of your own code between the call to fork( ) and the call to
exec( ). Note that a call to malloc( ), for example, is a call outside
of the currently executing application program and may have a mutex
outstanding.
if your code calls some of your own code that does not make any calls
outside of your code and does not lock any mutexes that could possibly
be locked in another thread, then your code is safe.
http://www.doublersolutions.com/docs/dce/osfdocs/htmls/develop/appdev/Appde193.htm
When duplicating the parent process, the fork subroutine also
duplicates all the synchronization variables, including their state.
Thus, for example, mutexes may be held by threads that no longer exist
in the child process and any associated resource may be inconsistent.
https://www.ibm.com/docs/en/aix/7.2?topic=programming-process-duplication-termination
https://pubs.opengroup.org/onlinepubs/000095399/functions/fork.html
https://lwn.net/Articles/674660/
https://softwareengineering.stackexchange.com/questions/384505/why-would-cpython-logging-use-a-lock-for-each-handler-rather-than-one-lock-per-l
The code below is stolen from https://blog.actorsfit.com/a?ID=00001-993928f7-96b0-42dd-8903-18ae712467f3
The preexec_fn in subprocess.Popenis similar to target in multiprocessing.Process and interrupting multiprocessing.Process clearly shows in the stacktrace the lock acquiring code (self.lock.acquire()).
time.sleep in emit mostly results in deadlock.
import sys
import time
import logging
import threading
import multiprocessing
import subprocess
class MyHandler(logging.StreamHandler):
def emit(self, record):
time.sleep(0.1)
super().emit(record)
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
handler = MyHandler()
formatter = logging.Formatter('%(asctime)s %(process)d %(thread)d %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
def thread_fn():
logger.info('thread')
logger.info('thread')
def process_fn():
logger.info('child')
logger.info('child')
t1 = threading.Thread(target=thread_fn)
t1.start()
p1 = multiprocessing.Process(target=process_fn)
p1.start()
# subprocess.Popen(args=['echo from shell'], shell=True, preexec_fn=process_fn)
Normal output;
2022-06-30 03:28:28,162 30093 140582533375744 thread
2022-06-30 03:28:28,164 30100 140582559856448 child
2022-06-30 03:28:28,263 30093 140582533375744 thread
2022-06-30 03:28:28,266 30100 140582559856448 child
os.register_at_fork (pthread_atfork) was added in Python 3.7.
This is the mechanism CPython uses to avoid deadlocks.
https://github.com/google/python-atfork
For demo, I delete it before importing logging.
import os
del os.register_at_fork
# same code as above
Deadlock output;
2022-06-30 03:30:10,090 30374 140014242154240 thread
2022-06-30 03:30:10,191 30374 140014242154240 thread
^CError in atexit._run_exitfuncs:
Traceback (most recent call last):
File "/usr/lib/python3.8/multiprocessing/popen_fork.py", line 27, in poll
Process Process-1:
pid, sts = os.waitpid(self.pid, flag)
KeyboardInterrupt
Traceback (most recent call last):
File "/usr/lib/python3.8/multiprocessing/process.py", line 313, in _bootstrap
self.run()
File "/usr/lib/python3.8/multiprocessing/process.py", line 108, in run
self._target(*self._args, **self._kwargs)
File "/home/niz/src/python/tor-sec/tor_sec/new_fork_trhead.py", line 29, in process_fn
logger.info('child')
File "/usr/lib/python3.8/logging/__init__.py", line 1434, in info
self._log(INFO, msg, args, **kwargs)
File "/usr/lib/python3.8/logging/__init__.py", line 1577, in _log
self.handle(record)
File "/usr/lib/python3.8/logging/__init__.py", line 1587, in handle
self.callHandlers(record)
File "/usr/lib/python3.8/logging/__init__.py", line 1649, in callHandlers
hdlr.handle(record)
File "/usr/lib/python3.8/logging/__init__.py", line 948, in handle
self.acquire()
File "/usr/lib/python3.8/logging/__init__.py", line 899, in acquire
self.lock.acquire()
KeyboardInterrupt
logging module at GH