在工作进程启动之前将数据放入输入队列时,工作进程在requests.get()上崩溃

时间:2019-04-30 15:58:54

标签: python python-requests fork python-multithreading macos-high-sierra

在macOS High Sierra(版本10.13.6)中,我运行一个执行以下操作的Python程序:

  • 启动工作进程,该工作进程使用multiprocessing.Queue中的数据(URL字符串)。
  • 工作进程使用requests包发送HTTP请求,即,它进行了requests.get()个调用。
  • 某些数据(URL字符串)甚至在启动工作进程之前就已馈入队列。

满足以上条件的程序会导致工作进程崩溃,并显示以下错误:

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.

我已阅读以下主题:

这些线程专注于用户的解决方法。解决方法是定义以下环境变量:

OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES

在这个问题中,我想理解为什么只有某些条件会重现错误,而其他条件却没有,并且如何解决此问题而又不会给用户增加定义环境变量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()

以下是带有错误的输出:

$ 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 ...

解决方案

我看到了一些独立的问题可以解决问题,即仅执行其中一项即可解决问题:

  1. 该问题似乎仅在使用requests软件包时发生。如果我们在worker()中注释掉这两行,就可以解决问题。

        # response = requests.get(url)
        # print('worker: response:', response.status_code)
    
  2. 仅当q.put('https://www.example.com/')语句之前发生p.start()语句时,才出现此问题。如果我们将声明移至p.start()的位置,就可以解决问题。

        p.start()
        print('master: started worker')
    
        q.put('https://www.example.com/')
    
  3. 设置环境变量OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES可解决此问题。

    OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES python3 foo.py
    

非解决方案

现在,我不希望我的用户设置这样的变量名以能够使用我的工具或API,因此我试图确定在程序中设置此环境变量是否可以解决问题。我发现将其添加到我的代码中不能解决问题:

import os
os.environ['OBJC_DISABLE_INITIALIZE_FORK_SAFETY'] = 'YES'
# Does not resolve the issue!

问题

  1. 为什么仅在给定条件下,即requests.get()之前的q.put()p.start()会发生此问题?换句话说,如果不满足这些条件之一,为什么问题会消失?

  2. 如果我们将诸如最小示例之类的内容公开为另一个开发人员可能会从其代码中调用的API函数,那么是否有任何聪明的方法来解决我们代码中的此问题,以使另一个开发人员没有在运行使用我们函数的程序之前在其shell中设置OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES

当然,一种可能的解决方案是重新设计该解决方案,以使我们不必在工作进程启动之前将数据馈入队列。那绝对是一个可能的解决方案。但是,此问题的范围是讨论为什么仅当我们在工作进程开始之前将数据馈入队列时才发生此问题。

3 个答案:

答案 0 :(得分:2)

很棒的问题描述!你有我的支持。

现在是答案:

  • 在macOS 10.13之前,objective-C运行时不支持在多线程父进程的子进程的fork()exec()之间使用。您不能在该间隔内调用任何Objective-C方法。这导致比赛条件。即大多数情况下它会工作,有时会失败。例如:如果fork()发生时,如果父进程中的线程恰巧持有Object-C运行时的锁之一,则子进程在尝试获取该锁时将死锁。
  • 从macOS 10.13开始,Objective-C运行时现在支持在fork()exec()之间使用。但是,存在涉及+initialize方法的限制。 (您的问题是在此区域)。

现在,在提出解决方案之前。让我来说明一下与fork相关的复杂性:

  • fork创建该过程的副本。
  • 子进程使用execve()系统调用将其替换为其他程序

到目前为止,一切似乎还好吧?子进程(在您的情况下为worker)具有父进程的副本,此副本由fork()提供给子进程。 但是,fork()不会复制所有内容!特别是,它不会复制线程。父进程中运行的任何线程在子进程中都不存在

请注意,着重于您的问题:

尽管如此,macOS 10.13+支持在forkexec之间执行“任何操作”。但是,在forkexec之间进行任何操作都是非常不正确的。在您的情况下,如@Darkonaut所正确提到的,在q.put()之前调用p.start()会在第一次调用时启动feeder线程,并分叉一个已经存在的多线程应用程序。

这是因为+initialize方法仍然对fork()有限制。问题是+initialize的线程安全保证隐含地引入了锁定状态,而Objective-C运行时没有控制。

当您调用q.put()或使用requests库(调用流行的请求库时,最终将调用_scproxy模块以获取系统代理,并且最终将调用+ p.start()之前的“ initialize method”(初始化方法),它们中的任何一个都会导致您的父进程获取锁。您必须注意,fork创建了一个流程副本。在您的情况下,如果在q.put()之前调用p.start(),则fork在错误的时间发生,并且您workers得到了父进程的副本,得到了{{ 1}}处于复制状态。

lock中,您正在做worker。这意味着要获取锁,但是该锁已在q.get()期间(从父级)获取。

子进程(fork)等待worker被释放,但是lock将永远不会被释放。因为,lock不会复制将释放该线程的线程。

没有使fork()既线程安全又叉安全的好方法。相反,Objective-C运行时只是停止进程,而不是在子进程中运行任何+initialize覆盖:

+initialize

希望能回答您的问题1。

现在,对于问题2:

从最佳到最坏的几种解决方法:

  1. +[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. fork()之间什么也不做(最好不要使用exec()fork()之间的请求)。
  2. 在fork()和exec()之间仅使用异步信号安全操作。可用的功能列表here
  3. 定义环境变量OBJC_DISABLE_INITIALIZE_FORK_SAFETY = YES,或添加__DATA,__ objc_fork_ok部分,或使用macOS 10.13之前的SDK进行构建。然后用手指交叉。

答案 1 :(得分:1)

  1. 我认为这是由“代理查找”机制引起的,或者是其他Mac特定的urllib3的实现(由python-requests内部使用)导致了分叉。选中github for more info

  2. 以这样一种方式编写函数,即它要求“可能在init上造成分叉的对象”作为参数之一。例如,您的工作人员可能需要一个会话参数:


def worker(q, session):
    ...

    while True:
        ...
        response = session.get(url)
        print('worker: response:', response.status_code)

def master():
    with requests.Session() as session:  # Or use `session.close()` at the end if you don't like context-manager
        q = mp.Queue()
        p = mp.Process(target=worker, args=(q, session))
        q.put('https://www.example.com/')

        p.start()
        ...

答案 2 :(得分:1)

我在macOS Catalina上遇到了同样的问题。我试图挖掘更深的请求库,而原因似乎是密码学库。升级可以解决所有问题。

pip install cryptography --upgrade  # Version 2.8 worked for me.

我有2.7版,正在产生这些objc错误。显然,该库中的某处导致了分叉负载,并且机制已在较新版本中更改。