Рабочий процесс завершается с ошибкой на запросе - PullRequest
5 голосов
/ 30 апреля 2019

В macOS High Sierra (версия 10.13.6) я запускаю программу на Python, которая выполняет следующие действия:

  • Запускает рабочий процесс, который использует данные (строки URL) из multiprocessing.Queue.
  • Рабочий процесс отправляет HTTP-запросы с пакетом requests, т. Е. Совершает 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
    

Non-Resolution

Теперь я не хочу, чтобы мои пользователи задавали имя переменной, подобной этой, чтобы иметь возможность использовать мой инструмент или 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, которую другой разработчик мог бы вызвать из своего кода, есть ли какой-нибудь умный способ решить эту проблему в нашем коде, чтобы другой разработчик не имел установить OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES в своей оболочке перед запуском программы, которая использует нашу функцию?

Конечно, возможное решение состоит в том, чтобы перепроектировать решение таким образом, чтобы нам не приходилось вводить данные в очередь до запуска рабочего процесса. Это определенно возможное решение. Однако цель этого вопроса - обсудить, почему эта проблема возникает, только когда мы передаем данные в очередь до запуска рабочего процесса.

Ответы [ 2 ]

2 голосов
/ 13 мая 2019

Отличное описание вопроса! У тебя есть мой голос.

Теперь за ответ:

  • До macOS 10.13 среда выполнения target-C не поддерживала использование между fork() и exec() в дочернем процессе многопоточного родительского процесса. Вы можете просто не вызывать какой-либо метод target-C в этом интервале. Это приводит к состоянию гонки. то есть большую часть времени это будет работать, а иногда и не получится. Например: если бы поток в родительском процессе удерживал одну из блокировок среды выполнения Object-C, когда произошло fork(), дочерний процесс 'заблокировался бы, когда он попытался бы взять эту блокировку.
  • Начиная с macOS 10.13, среда выполнения Objective C теперь поддерживает использование "между" fork() и exec(). Однако существуют ограничения, связанные с методами +initialize. (Вы проблема в этой зоне).

Теперь, прежде чем предлагать решение. Позвольте мне пролить свет на сложность, связанную с fork:

  • fork создает копию процесса.
  • Дочерний процесс заменяет себя другой программой, используя системный вызов execve()

Пока все вроде нормально, верно? Дочерний процесс (worker в вашем случае) имеет копию родительского процесса, и эта копия предоставляется дочернему процессу fork(). Но, fork() не копирует все! В частности, он не копирует потоки. Все потоки, запущенные в родительском процессе, не существуют в дочернем процессе

На этой заметке, сосредоточившись на вашей проблеме:

Хотя macOS 10.13+ поддерживает выполнение «чего угодно» между fork и exec. Тем не менее, очень неправильно делать что-либо между fork и exec. В вашем случае вызов q.put() до p.start(), как справедливо упомянуто @Darkonaut, запускает фидерный поток при первом вызове, и разветвление уже многопоточного приложения проблематично.

Это связано с тем, что методы +initialize по-прежнему имеют ограничения вокруг fork(). Проблема заключается в том, что гарантии безопасности потока +initialize неявно вводят блокировки вокруг состояния, которое не контролируется средой выполнения Objective-C.

Когда вы вызываете q.put() или используете библиотеку requests (вызов в библиотеку популярных запросов, это в конечном итоге вызовет модуль _scproxy для получения системных прокси, и это в конечном итоге вызовет метод + initialize) перед тем, как p.start(), любой из них приводит ваш родительский процесс к получению блокировки. Вы должны принять к сведению, что fork создает копию процесса. В вашем случае, когда q.put() вызывается до p.start(), fork происходит в неподходящее время, и вы workers получаете копию родительского процесса, lock в скопированном состоянии.

В вас worker, вы делаете q.get(). Это означает получение блокировки, но блокировка уже получена во время fork (от родителя).

Дочерний процесс (worker) ожидает освобождения lock, но lock никогда не будет выпущен. Потому что поток, который его освободит, не был скопирован fork().

Нет хорошего способа сделать +initialize поточно-ориентированным и вилко-безопасным. Вместо этого среда выполнения 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.

Надеюсь, что ответ на ваш вопрос 1.

Теперь по вопросу 2:

Несколько обходных путей от лучших к худшим:

  1. Ничего не делать между fork() и exec() (лучше не использовать запросы между fork() и exec*()).
  2. Используйте только безопасные асинхронные операции между fork () и exec (). Список доступных функций здесь
  3. Определите переменную среды OBJC_DISABLE_INITIALIZE_FORK_SAFETY = YES, или добавьте раздел __DATA, __ objc_fork_ok, или создайте с использованием SDK, более раннего, чем macOS 10.13. Затем скрестите пальцы.
1 голос
/ 11 мая 2019
  1. Я думаю, что это вызвано механизмом «прокси-поиска» или какой-то другой специфичной для mac реализацией urllib3 (используемой внутренне для python-запросов), которая вызывает форк.Проверьте github для получения дополнительной информации .

  2. Напишите свою функцию таким образом, чтобы в качестве одного из аргументов ей потребовались «объекты, которые могут вызвать разветвление при инициализации».Например, вашему работнику может потребоваться аргумент сеанса:


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()
        ...
...