Sus*_*Pal 5 python fork python-multithreading python-requests macos-high-sierra
在macOS High Sierra(版本10.13.6)中,我运行一个Python程序,该程序执行以下操作:
multiprocessing.Queue。requests包发送HTTP请求,即,它进行了requests.get()调用。满足上述条件的程序将导致工作进程崩溃,并显示以下错误:
objc[24250]: +[__NSPlaceholderDate initialize] may have been in progress in another thread when fork() was called.
objc[24250]: +[__NSPlaceholderDate initialize] may have been in progress in another thread when fork() was called. We cannot safely call it or ignore it in the fork() child process. Crashing instead. Set a breakpoint on objc_initializeAfterForkError to debug.
Run Code Online (Sandbox Code Playgroud)
我已阅读以下主题:
这些线程专注于用户的解决方法。解决方法是定义此环境变量:
OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES
Run Code Online (Sandbox Code Playgroud)
在这个问题中,我想理解为什么只有某些条件会产生错误,而其他条件却没有,并且如何解决此问题而不给OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES用户增加定义环境变量的负担。
import multiprocessing as mp
import requests
def worker(q):
print('worker: starting ...')
while True:
url = q.get()
if url is None:
print('worker: exiting ...')
break
print('worker: fetching', url)
response = requests.get(url)
print('worker: response:', response.status_code)
def master():
q = mp.Queue()
p = mp.Process(target=worker, args=(q,))
q.put('https://www.example.com/')
p.start()
print('master: started worker')
q.put('https://www.example.org/')
q.put('https://www.example.net/')
q.put(None)
print('master: sent data')
print('master: waiting for worker to exit')
p.join()
print('master: exiting ...')
master()
Run Code Online (Sandbox Code Playgroud)
这是带有错误的输出:
$ python3 foo.py
master: started worker
master: sent data
master: waiting for worker to exit
worker: starting ...
worker: fetching https://www.example.com/
objc[24250]: +[__NSPlaceholderDate initialize] may have been in progress in another thread when fork() was called.
objc[24250]: +[__NSPlaceholderDate initialize] may have been in progress in another thread when fork() was called. We cannot safely call it or ignore it in the fork() child process. Crashing instead. Set a breakpoint on objc_initializeAfterForkError to debug.
master: exiting ...
Run Code Online (Sandbox Code Playgroud)
我看到了一些独立的问题可以解决问题,即,仅执行其中一项即可解决问题:
该问题似乎仅在使用requests软件包时发生。如果我们在中注释掉这两行worker(),则可以解决此问题。
# response = requests.get(url)
# print('worker: response:', response.status_code)
Run Code Online (Sandbox Code Playgroud)仅当在q.put('https://www.example.com/')语句之前出现语句时,才会出现此问题p.start()。如果我们将语句ater移到该位置p.start(),则可以解决此问题。
p.start()
print('master: started worker')
q.put('https://www.example.com/')
Run Code Online (Sandbox Code Playgroud)设置环境变量OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES可以解决此问题。
OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES python3 foo.py
Run Code Online (Sandbox Code Playgroud)现在,我不想让用户设置这样的变量名以能够使用我的工具或API,因此我试图确定在程序中设置此环境变量是否可以解决问题。我发现将其添加到我的代码中不能解决问题:
import os
os.environ['OBJC_DISABLE_INITIALIZE_FORK_SAFETY'] = 'YES'
# Does not resolve the issue!
Run Code Online (Sandbox Code Playgroud)
为什么恰好在给定条件下(即之前requests.get()和q.put()之后)才发生此问题p.start()?换句话说,如果不满足这些条件之一,为什么问题消失了?
如果我们将诸如最小示例之类的东西作为API函数公开给其他开发人员,可以从他们的代码中调用该方法,那么有什么聪明的方法可以解决我们代码中的此问题,从而使其他开发人员不必OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES在其外壳中进行设置运行他们的程序使用我们的功能?
当然,可能的解决方案是重新设计该解决方案,以使我们不必在工作进程启动之前就将数据馈入队列。那绝对是一个可能的解决方案。但是,此问题的范围是讨论为什么仅当我们在工作进程启动之前将数据馈入队列时才发生此问题。
很好的问题描述!你得到了我的赞成。
现在回答:
fork()和exec()子进程中使用。您不能在该时间间隔内调用任何 Objective-C 方法。这导致竞争条件。即大部分时间它会工作,有时它会失败。例如:如果父进程中的线程在发生这种情况时碰巧持有 Object-C 运行时的锁之一fork(),则子进程在尝试获取该锁时将死锁。fork()和exec(). 但是,这些+initialize方法存在一些限制。(你的问题是在这个区域)。现在,在提出解决方案之前。让我谈谈与fork以下相关的复杂性:
fork 创建过程的副本。execve()系统调用将自身替换为不同的程序到目前为止一切似乎都还不错吧?子进程(worker在您的情况下)具有父进程的副本,并且该副本由fork(). 但是,fork()不会复制所有内容!特别是,它不复制线程。子进程中不存在父进程中运行的任何线程
在这一点上,关注你的问题:
虽然,macOS 10.13+ 支持在fork和之间做“任何事情” exec。但是,在fork和之间做任何事情都是非常不正确的exec。在您的情况下,@Darkonaut 正确提到的调用q.put()beforep.start()在第一次调用时会启动一个馈线线程,并且分叉已经是多线程的应用程序是有问题的。
这是因为+initialize方法仍然有围绕fork(). 的限制。问题是+initialize隐式引入围绕 Objective-C 运行时无法控制的状态的锁的线程安全保证。
当您调用q.put()或使用requests库(调用流行的请求库,这将最终调用 _scproxy 模块以获取系统代理,这将最终调用 +initialize 方法)之前p.start(),它们中的任何一个都会导致您的父进程获取一把锁。您必须注意fork创建过程的副本。在您的情况下, whenq.put()被调用 before p.start(),fork发生在错误的时间,而您是workers获得父进程副本的人,进入lock复制状态。
在你是worker,你正在做一个q.get(). 这意味着获取锁,但是在fork(从父级)期间已经获取了锁。
子进程 ( worker) 等待lock被释放但lock永远不会被释放。因为,释放它的线程没有被fork().
没有好的方法可以使+initialize线程安全和 fork 安全。相反,Objective-C 运行时只是停止进程而不是+initialize在子进程中运行任何覆盖:
+[SomeClass initialize] may have been in progress in another thread when fork() was called. We cannot safely call it or ignore it in the fork() child process. Crashing instead.
Run Code Online (Sandbox Code Playgroud)
希望能回答您的问题 1。
现在,对于问题 2:
一些从最好到最坏的解决方法:
fork()和exec()(最好不要用之间的请求fork()和exec*())。