Передача sys.stdout в качестве аргумента процессу

Я передаю «sys.stdout» в качестве аргумента процессу, а процесс затем записывает в «sys.stdout», пока он делает свое дело.

import multiprocessing
import sys
def worker_with(stream):
    stream.write('In the process\n')

if __name__ == '__main__':
    sys.stdout.write('In the main\n')
    lock = multiprocessing.Lock()

    w = multiprocessing.Process(target=worker_with, args=(sys.stdout,))

    w.start()
    w.join()

Приведенный выше код не работает, он возвращает следующую ошибку: «ValueError: операция над закрытым файлом».

Я попытался запустить тот же код, но вызвал функцию напрямую, а не порождал процесс, и он работает, он выводит на консоль. Я также пытался запустить тот же код, но вызывая непосредственно sys.stdout внутри функции, запуская его как процесс, и он работает. Кажется, проблема заключается в передаче sys.stout в качестве параметра процесса.

Кто-нибудь знает, почему?

Примечание: этот код вдохновлен учебником PYMOTW — связь между процессами.

РЕДАКТИРОВАТЬ: я использую Python 2.7.10, 32 бита в Windows7.


person user3722440    schedule 16.12.2015    source источник
comment
Я не получаю никаких ошибок, я использую Python 2.7.11 в Ubuntu 14.04.   -  person agold    schedule 16.12.2015
comment
Спасибо аголд. Давайте посмотрим, сможет ли кто-нибудь запустить тот же код на компьютере с Windows.   -  person user3722440    schedule 16.12.2015
comment
Как вы передаете стандартный вывод сценарию в Windows?   -  person Bob Dylan    schedule 16.12.2015
comment
Привет Боб Дилан. Извините, я не уверен, что полностью понимаю вопрос. Я ничего не передаю, я просто запускаю приведенный выше код как скрипт (поэтому по умолчанию sys.stdout выводит на консоль). Оно отвечает на вопрос?   -  person user3722440    schedule 16.12.2015


Ответы (1)


Когда вы передаете аргументы Process, они сохраняются в родительском объекте, передаются дочернему объекту и там распаковываются. К сожалению, похоже, что круговое путешествие через pickle молча ведет себя неправильно для файловых объектов; с протоколом 0 он выдает ошибку, но с протоколом 2 (самый высокий протокол Python 2 и тот, который используется для multiprocessing) он молча создает объект нежелательного файла:

>>> import pickle, sys
>>> pickle.loads(pickle.dumps(sys.stdout, pickle.HIGHEST_PROTOCOL))
<closed file '<uninitialized file>', mode '<uninitialized file>' at 0xDEADBEEF>

Та же проблема возникает и для именованных файлов; это не уникально для стандартных ручек. По сути, pickle не может передавать файловый объект туда и обратно; даже когда он заявляет об успехе, результат - мусор.

Как правило, от multiprocessing не ожидается, что он справится с таким сценарием; обычно Processes являются рабочими задачами, а ввод-вывод выполняется через основной процесс (потому что, если бы все они записывались независимо в один и тот же дескриптор файла, у вас были бы проблемы с чередующейся записью).

По крайней мере, в Python 3.5 они исправили это, поэтому ошибка стала немедленной и очевидной (файлоподобные объекты, возвращаемые open, TextIOWrapper и Buffered*, будут выдавать ошибку при обработке любым протоколом).

Лучшее, что вы можете сделать в Windows, это отправить известный файловый дескриптор в качестве аргумента:

sys.stdout.flush()  # Precaution to minimize output interleaving
w = multiprocessing.Process(target=worker_with, args=(sys.stdout.fileno(),))

затем снова откройте его с другой стороны, используя os.fdopen. Для fds, не являющихся частью стандартных дескрипторов (0, 1 и 2), поскольку Windows использует метод «порождения» для создания новых Processes, вам необходимо убедиться, что все такие fd были открыты как следствие importing модуля __main__, когда __name__ != "__main__" (Windows имитирует fork, импортируя модуль __main__, устанавливая __name__ на что-то другое). Конечно, если это именованный файл, а не стандартный дескриптор, вы можете просто передать имя и снова открыть его. Например, чтобы это работало, вы должны изменить:

def worker_with(stream):
    stream.write('In the process\n')

to:

import os

def worker_with(toopen):
    opener = open if isinstance(toopen, basestring) else os.fdopen
    with opener(toopen, 'a') as stream:
        stream.write('In the process\n')

Примечание. Как написано, если fd соответствует одному из стандартных дескрипторов, os.fdopen закроет базовый файловый дескриптор при выходе из инструкции with, что может быть не совсем тем, что вам нужно. Если вам нужны файловые дескрипторы, чтобы пережить закрытие блока with, при передаче файлового дескриптора вы можете использовать os.dup для дублирования дескриптора перед вызовом os.fdopen, чтобы два дескриптора не зависели друг от друга.

Другие решения могут включать в себя запись результатов обратно в основной процесс через multiprocessing.Pipe (поэтому основной процесс отвечает за передачу данных в sys.stdout, возможно, запуск потока для асинхронного выполнения этой работы) или использование конструкций более высокого уровня (например, multiprocessing.Pool().*map*), которые возвращать данные с помощью оператора return вместо явного файлового ввода-вывода.

Если вы действительно отчаянно хотите, чтобы это работало вообще для всех дескрипторов файлов (и вас не волнует переносимость), а не только для стандартных дескрипторов и дескрипторов, созданных на import из __main__, вы можете использовать недокументированная служебная функция Windows multiprocessing.forking.duplicate, которая используется для явного дублирования файлового дескриптора. от одного процесса к другому; это было бы невероятно хакерским (вам нужно было бы посмотреть остальную часть определения multiprocessing.forking.Popen в Windows, чтобы увидеть, как оно будет использоваться), но, по крайней мере, оно позволило бы передавать произвольные файловые дескрипторы, а не только статически открытые.

person ShadowRanger    schedule 16.12.2015
comment
ShadowRanger, большое спасибо за очень подробное объяснение. Я думаю, мне нужно больше узнать об дескрипторах файлов, но предложенный вами обходной путь довольно хорош. - person user3722440; 16.12.2015
comment
Только одна вещь: ваша первая строка кода (обход в оба конца) дает другой результат на моей машине: объект ‹idlelib.PyShell.PseudoOutputFile по адресу 0x02B4AFB0›. Если я не указываю протокол, я получаю: TypeError: can't pickle объекты _TextIOBase. - person user3722440; 16.12.2015
comment
@ user3722440: IDLE заменяет стандартные дескрипторы специальными, которые можно использовать для вывода на терминал IDLE, поэтому можно ожидать другого поведения; обычные программы, не работающие под IDLE, этого не увидят. IDLE PseudoOutputFile наследуется (косвенно) от io.TextIOBase; PseudoOutputFile не запрещает само травление, а io.TextIOBase запрещает, отсюда и ошибка. IDLE странен, и поведение, характерное для него, на самом деле не помогает понять, как стандартные дескрипторы работают вне IDLE. Если вы запускаете простой интерпретатор Python, он должен более точно имитировать мои результаты. - person ShadowRanger; 16.12.2015
comment
понял, спасибо (у меня точно такие же результаты при работе с оболочкой) - person user3722440; 16.12.2015