在多线程程序中' preexec_fn '是否安全?在什么情况下?



我理解使用subprocess.Popen(..., preexec_fn=func)使Popen线程不安全,并且如果在多线程程序中使用可能会死锁子进程:

警告:在应用程序中存在线程时,使用preexec_fn参数是不安全的。子进程可能在调用exec之前死锁。如果你一定要使用它,那就让它变得微不足道吧!尽量减少调用库的数量。

在多线程环境中使用它是否安全?例如,传递一个c编译的扩展函数,一个不获得任何解释器锁本身,是安全的吗?

我查看了相关的解释器代码,无法找到任何平常发生的死锁。可以传递一个简单的,纯python函数,如lambda: os.nice(20)曾经使子进程死锁?

注意:通过调用PyOS_AfterFork_Child()(Python早期版本中的PyOS_AfterFork())可以避免大多数明显的死锁。

注2:为了使问题易于回答,让我们假设我们运行的是最新版本的Glibc。

以下解释仅适用于POSIX。

在多线程进程中执行fork之后和exec之前的代码的问题不是Python特有的。

在子进程中,在调用fork()之后不要调用任何库函数。在调用exec()之前。其中一个库函数可能会使用fork()执行时父进程中持有的锁。

除了通常关心的问题,如锁定共享数据,一个库在什么时候应该很好地处理子进程的分叉只有调用fork()的线程正在运行。问题是子进程中的唯一线程可能会尝试获取由子线程中没有重复的线程。

例如,假设T1正在打印一些东西和当T2分叉一个新进程时,为printf()持有一个锁。在孩子身上进程中,如果唯一的线程(T2)调用printf(), T2及时死锁。

https://docs.oracle.com/cd/E19120-01/open.solaris/816-5137/gen-1/index.html

fork()系统调用创建了一个精确的地址副本被调用的空间,导致两个地址空间执行相同的代码

假设其他线程之一(除一个之外的任何线程执行fork()的任务是从你的支票中扣除钱帐户。

POSIX定义了存在线程时fork()的行为只传播分叉线程。

如果另一个线程有一个被锁定的互斥对象,这个互斥对象将被锁定子进程,但是锁的所有者不存在,无法解锁。因此,受锁保护的资源将是永久的不可用。

可能存在未处理的互斥体这一事实只会成为一个问题如果您的代码试图锁定一个互斥量,而该互斥量可能被另一个互斥量锁定线程在fork()时的状态。这意味着您不能调用在调用fork()和调用exec()。注意,对malloc()的调用是外部调用当前正在执行的应用程序,并且可能具有互斥锁突出。

如果你的代码调用了一些你自己的代码,而这些代码不进行任何调用在代码之外,不锁定任何互斥锁锁在另一个线程中,那么你的代码是安全的。

http://www.doublersolutions.com/docs/dce/osfdocs/htmls/develop/appdev/Appde193.htm

当复制父进程时,fork子例程也复制所有同步变量,包括它们的状态。因此,例如,互斥锁可能由不再存在的线程持有在子进程中与任何关联的资源可能不一致。

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

下面的代码是从https://blog.actorsfit.com/a?ID=00001-993928f7-96b0-42dd-8903-18ae712467f3

偷来的subprocess.Popen中的preexec_fnmultiprocessing.Process中的target相似,中断multiprocessing.Process在堆栈跟踪中清楚地显示了锁获取代码(self.lock.acquire())。emit中的time.sleep通常会导致死锁

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)

正常输出;

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)在Python 3.7中添加。这是CPython用来避免死锁的机制。https://github.com/google/python-atfork

为了演示,我在importlogging之前删除了它。

import os
del os.register_at_fork
# same code as above

僵局输出;

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

GH日志模块