【问题标题】:Multithreaded Perl script leads to broken pipe if called as a Python subprocess如果作为 Python 子进程调用,多线程 Perl 脚本会导致管道损坏
【发布时间】:2020-08-04 05:02:04
【问题描述】:

我正在从 Python 3.7.3 调用带有子进程的 Perl 脚本。调用的 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 代码?

如果需要,我很乐意提供更多信息。


编辑:回应一些cmets,

  • 只有当你也有这个文件时才能运行 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,它是用多线程编译的。如果我使用从 Python 调用的相同 Perl,我会得到:

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

这无疑会帮助我更好地调试它。

【问题讨论】:

  • Broken pipe 意味着 Perl 进程关闭了 Python 进程试图读取的输出流。需要查看 Perl 脚本才能进一步诊断。
  • @mob 感谢您的评论!我确实链接到了这个 Perl 脚本:github.com/moses-smt/mosesdecoder/blob/master/scripts/tokenizer/…
  • 您是否检查过load_prefixes() 子在输入循环开始之前没有死?好像引用了一个$prefixfile变量,哪个文件可能不存在?
  • @HåkonHægland 您好,感谢您的提示!这个文件肯定存在,因为 Perl 进程不会立即失败:它在失败之前确实处理了数千行。
  • @MathiasMüller 您是如何安装该文件的?如果我能得到它,我可以测试更多..

标签: python multithreading perl subprocess


【解决方案1】:

问题似乎是如果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()

它现在运行到最后没有错误,并生成了包含 100000 行的输出文件 outfile

【讨论】:

  • 我遇到了同样的问题,但使用了不同的 Perl 就可以了!感谢您的精彩分析。我错误地指出这个 Perl 程序在调用时运行良好,例如-threads 4 来自命令行:在那里,使用了不同的 Perl,它是通过多线程编译的。
【解决方案2】:

首先是什么导致了这个错误?

写入封闭管道会导致操作系统将SIGPIPE 发送到调用write 的进程。这允许程序作为生成器工作。例如,尽管包含无限循环,但以下内容不会永远运行,因为head 将在读取十行后退出并关闭其 STDIN,导致perl 收到 SIGPIPE。

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

如果SIGPIPE 信号被忽略,write 系统调用将返回EPIPE(断管)。以下内容也不会永远运行,因为一旦 head 退出,print 就会返回错误 EPIPE

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

根据您的 Python 程序收到 EPIPE 错误这一事实,我们推断出两个事实:

  • Python 程序会忽略 SIGPIPE 信号,并且
  • 管道读取器端的所有手柄均已关闭。

所以我们必须问自己:为什么 Perl 程序会关闭它的 STDIN?它的 STDIN 不太可能被明确关闭。到目前为止,最可能的解释是子进程被终止了。

“谁”应该为此负责:操作系统/环境、我的 Python 代码、Perl 代码?

这取决于导致 Perl 程序退出的原因。首先要做的是弄清楚子进程返回了什么退出状态。根据退出状态,我们会知道是否

  • 进程被信号杀死,
  • 进程因错误退出,或
  • 过程成功完成。

如果退出代码告诉我们进程被信号杀死,退出代码也会告诉我们是哪个信号。这可以给我们一些信息。 (这将是三个场景中最难调试的。)

如果退出代码告诉我们进程返回了错误,则错误代码本身可能不包含任何额外的有用信息,但肯定会向孩子的 STDERR 发送错误消息以提供更多信息。

如果退出代码告诉我们该过程已成功完成,那么您提供的参数或输入可能并不代表您认为的意思。

所以一定要调用tokenizer.wait()来收集退出状态并将其存储在tokenizer.returncode中。还要确保记录发送到 STDERR 的内容。

【讨论】:

  • 感谢您非常有见地的回答!问题是 Perl 版本不能用于多线程,但您的建议肯定会有助于调试。
猜你喜欢
  • 2016-01-06
  • 1970-01-01
  • 1970-01-01
  • 2021-04-16
  • 2021-11-10
  • 2011-05-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多