Многопоточный сценарий Perl приводит к разрыву канала, если вызывается как подпроцесс Python - PullRequest
2 голосов
/ 21 апреля 2020

Я вызываю скрипт Perl из Python 3.7.3 с подпроцессом. Вызывается сценарий Perl:

https://github.com/moses-smt/mosesdecoder/blob/master/scripts/tokenizer/tokenizer.perl

И код, который я использую для его вызова:

import sys
import os
import subprocess
import threading

def copy_out(source, dest):
    for line in source:
        dest.write(line)

num_threads=4

args = ["perl", "tokenizer.perl",
        "-l", "en",
        "-threads", str(num_threads)
       ]

with open(os.devnull, "wb") as devnull:
    tokenizer = subprocess.Popen(args,
        stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=devnull)

tokenizer_thread = threading.Thread(target=copy_out, args=(tokenizer.stdout, open("outfile", "wb")))
tokenizer_thread.start()

num_lines = 100000

for _ in range(num_lines):
    tokenizer.stdin.write(b'Random line.\n')

tokenizer.stdin.close()
tokenizer_thread.join()

tokenizer.wait()

В моей системе это приводит к следующей ошибке:

Traceback (most recent call last):
  File "t.py", line 27, in <module>
    tokenizer.stdin.write(b'Random line.\n')
BrokenPipeError: [Errno 32] Broken pipe

Я исследовал это, и оказалось, что если аргумент -threads для подпроцесса равен 1, ошибка не выдается. Поскольку я не хочу отказываться от многопоточности в дочернем процессе, мой вопрос:

В чем причина этой ошибки? «Кто» виноват в этом: ОС / окружение, мой код Python, код Perl?

Я рад предоставить дополнительную информацию, если это необходимо.


РЕДАКТИРОВАТЬ : Чтобы ответить на некоторые комментарии,

  • Запуск сценария Perl возможен только при наличии этого файла: https://github.com/moses-smt/mosesdecoder/blob/master/scripts/share/nonbreaking_prefixes/nonbreaking_prefix.en
  • Сценарий Perl фактически обрабатывает несколько тысяч строк, прежде чем процесс завершится сбоем. В моем скрипте Python, указанном выше, если я уменьшу num_lines, я больше не получу эту ошибку.
  • Если я вызову этот скрипт Perl просто в командной строке, без Python, он работает нормально: независимо от того, сколько (Perl) потоков или строк ввода.
  • Моя Python переменная num_threads контролирует только количество потоков Perl подпроцесс. Я никогда не запускаю несколько Python потоков, только одну.

РЕДАКТИРОВАТЬ 2 : В своем первом редактировании я неправильно указал, что эта Perl программа работает нормально, когда вызывается с помощью, например, -threads 4 из командной строки: там был использован другой Perl, скомпилированный с многопоточностью. Если я использую тот же Perl, который вызывается из Python, я получаю:

$ cat [file with 100000 lines] | [correct perl] tokenizer.perl -l en -threads 4
Can't locate object method "new" via package "Thread" at
tokenizer.perl line 130, <STDIN> line 8000.

, что, без сомнения, помогло бы мне лучше отладить это.

Ответы [ 2 ]

3 голосов
/ 22 апреля 2020

Кажется, проблема в том, что скрипт perl падает, если perl не поддерживает потоки. Вы можете проверить, поддерживает ли ваш perl потоки, выполнив:

perl -MConfig -E 'say "Threads supported" if $Config{useithreads}'

В моем случае вывод был пуст, поэтому я установил новый perl с поддержкой потоков:

perlbrew install perl-5.30.0 --as=5.30.0-threads -Dusethreads
perlbrew use 5.30.0-threads

Затем я снова запустил сценарий Python:

import sys
import os
import subprocess
import threading

def copy_out(source, dest):
    for line in iter(source.readline, b''):
        dest.write(line)

num_threads=4
args = ["perl", "tokenizer.perl",
        "-l", "en",
        "-threads", str(num_threads)
       ]
tokenizer = subprocess.Popen(
    args,
    bufsize=-1,  #use default bufsize = 8192 bytes
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE,
    stderr=subprocess.DEVNULL)

tokenizer_thread = threading.Thread(
    target=copy_out, args=(tokenizer.stdout, open("outfile", "wb")))
tokenizer_thread.start()

num_lines = 100000

for _ in range(num_lines):
    tokenizer.stdin.write(b'Random line.\n')

tokenizer.stdin.close()
tokenizer_thread.join()
tokenizer.wait()

, и теперь он завершился без ошибок и сгенерировал выходной файл outfile с 100000 строками.

2 голосов
/ 22 апреля 2020

В чем причина этой ошибки?

Запись в закрытый канал заставляет ОС отправлять SIGPIPE процессу, вызывающему write. Это позволяет программе работать как генераторы. Например, следующее не будет выполняться вечно, несмотря на то, что содержит бесконечный l oop, потому что head выйдет и закроет свой STDIN после чтения десяти строк, что приведет к perl получению SIGPIPE.

perl -le'1 while print ++$i;' | head

Если сигнал SIGPIPE игнорируется, системный вызов write возвратит EPIPE (Broken pipe). Следующее также не будет работать вечно, потому что print возвращает ошибку EPIPE после head выхода.

perl -le'$SIG{PIPE}="IGNORE"; 1 while print ++$i;' | head

Из-за того, что ваша Python программа получила ошибку EPIPE, мы выводим два факта:

  • Программа Python игнорирует сигналы SIGPIPE и
  • Все дескрипторы конца считывателя канала были закрыты.

Поэтому мы должны спросить себя: почему программа Perl закрывает свой STDIN? очень маловероятно, что его STDIN был закрыт явно. Безусловно, наиболее вероятное объяснение состоит в том, что дочерний процесс был прерван.

"Кто" виноват в этом: ОС / окружение, мой код Python, Perl код?

Это зависит от того, что вызвало выход из программы Perl. Первое, что нужно сделать, это выяснить, какой статус выхода был возвращен дочерним процессом. В зависимости от состояния выхода мы будем знать, был ли

  • процесс завершен сигналом,
  • процесс завершился с ошибкой или
  • процесс завершено успешно.

Если код выхода говорит нам, что процесс был прерван сигналом, код выхода также сообщит нам, каким сигналом. Это может дать нам некоторую информацию. (Это будет самый сложный из трех сценариев ios для отладки.)

Если код завершения сообщает нам, что процесс возвратил ошибку, сам код ошибки может не содержать никакой дополнительной полезной информации, но является ошибкой сообщение, несомненно, было отправлено в STDERR ребенка, чтобы предоставить больше информации.

Если код завершения сообщает нам о успешном завершении процесса, возможно, аргументы или вводимые вами данные не означают того, что вы думаете, что они означают.

Так что не забудьте позвонить tokenizer.wait(), чтобы получить статус выхода и сохранить его в tokenizer.returncode. Также обязательно запишите, что отправляется в STDERR.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...