Luc*_*anc 5 c# linux multithreading tesseract .net-core
我有一个 .NET Core 2.1 服务,它在 Ubuntu 18.04 VM 上运行并通过 Process 实例调用 Tesseract OCR 4.00。我想使用 API 包装器,但我只能找到一个可用的,而且它只在最新版本的 Tesseract 中处于测试阶段——稳定的包装器使用版本 3 而不是 4。过去,这项服务运行良好,但我一直在更改它,以便减少从磁盘写入和读取文档/图像数据的频率,以提高速度。该服务曾经调用更多由于 API 的存在而不必要的外部进程(例如 ImageMagick),因此我一直用 API 调用替换它们。
最近,我一直在使用从真实数据中提取的示例文件对此进行测试。这是一个传真文档 PDF,有 133 页,但由于灰度和分辨率的原因,只有 5.8 MB。该服务获取一个文档,将其拆分为单独的页面,然后分配多个线程(每页一个线程)来调用 Tesseract 并使用Parallel.For
. 线程限制是可配置的。我知道 Tesseract 有自己的多线程环境变量 (OMP_THREAD_LIMIT)。我在之前的测试中发现,将其设置为“1”是目前我们设置的理想选择,但在我最近针对此问题的测试中,我尝试将其设置为未设置(动态值),但没有任何改进。
问题是,不可预测的是,当调用 Tesseract 时,该服务将挂起大约一分钟然后崩溃,而 journalctl 中显示的唯一错误是:
dotnet[32328]: Error while reaping child. errno = 10
dotnet[32328]: at System.Environment.FailFast(System.String, System.Exception)
dotnet[32328]: at System.Environment.FailFast(System.String)
dotnet[32328]: at System.Diagnostics.ProcessWaitState.TryReapChild()
dotnet[32328]: at System.Diagnostics.ProcessWaitState.CheckChildren(Boolean)
dotnet[32328]: at System.Diagnostics.Process.OnSigChild(Boolean)
Run Code Online (Sandbox Code Playgroud)
对于这个特定的错误,我在网上根本找不到任何东西。根据我在Process
课堂上所做的相关研究,在我看来,当进程退出并且 dotnet 试图清理它正在使用的资源时,就会发生这种情况。我真的不知道如何解决这个问题,尽管我已经尝试了许多“猜测”,例如更改线程限制值。线程之间没有交叉。每个线程都有自己的页面分区(基于如何Parallel.For
对集合进行分区),并且一次一个处理这些页面。
这是进程调用,从多个线程中调用(8 是我们通常设置的限制):
private bool ProcessOcrPage(IMagickImage page, int pageNumber, object instanceId)
{
var inputPageImagePath = Path.Combine(_fileOps.GetThreadWorkingDirectory(instanceId), $"ocrIn_{pageNumber}.{page.Format.ToString().ToLower()}");
string outputPageFilePathWithoutExt = Path.Combine(_fileOps.GetThreadOutputDirectory(instanceId),
$"pg_{pageNumber.ToString().PadLeft(3, '0')}");
page.Write(inputPageImagePath);
var cmdArgs = $"-l eng \"{inputPageImagePath}\" \"{outputPageFilePathWithoutExt}\" pdf";
bool success;
_logger.LogStatement($"[Thread {instanceId}] Executing the following command:{Environment.NewLine}tesseract {cmdArgs}", LogLevel.Debug);
var psi = new ProcessStartInfo("tesseract", cmdArgs)
{
RedirectStandardError = true,
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};
// 0 is not the default value for this environment variable. It should remain unset if there
// is no config value, as it is determined dynamically by default within OpenMP.
if (_processorConfig.TesseractThreadLimit > 0)
psi.EnvironmentVariables.Add("OMP_THREAD_LIMIT", _processorConfig.TesseractThreadLimit.ToString());
using (var p = new Process() { StartInfo = psi })
{
string standardErr, standardOut;
int exitCode;
p.Start();
standardOut = p.StandardOutput.ReadToEnd();
standardErr = p.StandardError.ReadToEnd();
p.WaitForExit();
exitCode = p.ExitCode;
if (!string.IsNullOrEmpty(standardOut))
_logger.LogStatement($"Tesseract stdOut:\n{standardOut}", LogLevel.Debug, nameof(ProcessOcrPage));
if (!string.IsNullOrEmpty(standardErr))
_logger.LogStatement($"Tesseract stdErr:\n{standardErr}", LogLevel.Debug, nameof(ProcessOcrPage));
success = p.ExitCode == 0;
}
return success;
}
Run Code Online (Sandbox Code Playgroud)
编辑 4:在聊天中与 Clint 进行大量测试和讨论后,这是我们学到的。错误是从进程事件“OnSigChild”引发的,从堆栈跟踪中可以明显看出这一点,但无法挂钩到引发此错误的同一事件中。给定 10 秒的超时时间,该过程永远不会超时(Tesseract 通常只需要几秒钟来处理给定的页面)。奇怪的是,如果删除进程超时并且我等待标准输出和错误流关闭,它将挂起 20-30 秒,但ps auxf
在此挂起时间内该进程不会出现。据我所知,Linux 能够确定进程已完成执行,但 .NET 则不然。否则,错误似乎是在进程完成执行的那一刻引发的。
对我来说最令人困惑的是,与我们在生产中使用的此代码的工作版本相比,代码的流程处理部分确实没有太大变化。这表明这是我在某处犯的错误,但我根本无法找到它。我想我将不得不在 dotnet GitHub 跟踪器上打开一个问题。
“收割孩子时出错”
进程会占用内核中的一些资源,在 Unix 上,当父进程死亡时,init 进程负责清理 Zombine 和 Orphan 进程(也称为收割子进程)的内核资源。.NET Core 在子进程终止后立即获取它们。
“我发现删除 stdout 和 stderr 流 ReadToEnd 调用会导致进程立即结束而不是挂起,并出现相同的错误”
该错误是由于您p.ExitCode
甚至在流程完成之前就过早调用,并且ReadToEnd
您只是延迟了此活动
更新代码摘要
StartInfo.FileName
应该指向您要启动的文件名UseShellExecute
如果进程应该直接从可执行文件创建,则为 false;如果您打算在启动进程时使用 shell,则为 true;AutoResetEvents
输出时发出信号,操作完成时发出错误信号Process.Close()
来释放资源关于 Linux 上 NetProcess 的 Redhat 博客
修订模块
private bool ProcessOcrPage(IMagickImage page, int pageNumber, object instanceId)
{
StringBuilder output = new StringBuilder();
StringBuilder error = new StringBuilder();
int exitCode;
var inputPageImagePath = Path.Combine(_fileOps.GetThreadWorkingDirectory(instanceId), $"ocrIn_{pageNumber}.{page.Format.ToString().ToLower()}");
string outputPageFilePathWithoutExt = Path.Combine(_fileOps.GetThreadOutputDirectory(instanceId),
$"pg_{pageNumber.ToString().PadLeft(3, '0')}");
page.Write(inputPageImagePath);
var cmdArgs = $"-l eng \"{inputPageImagePath}\" \"{outputPageFilePathWithoutExt}\" pdf";
bool success;
_logger.LogStatement($"[Thread {instanceId}] Executing the following command:{Environment.NewLine}tesseract {cmdArgs}", LogLevel.Debug);
using (var outputWaitHandle = new AutoResetEvent(false))
using (var errorWaitHandle = new AutoResetEvent(false))
{
try
{
using (var process = new Process())
{
process.StartInfo = new ProcessStartInfo
{
WindowStyle = ProcessWindowStyle.Hidden,
FileName = "tesseract.exe", // Verify if this is indeed the process that you want to start ?
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
Arguments = cmdArgs,
WorkingDirectory = Path.GetDirectoryName(path)
};
if (_processorConfig.TesseractThreadLimit > 0)
process.StartInfo.EnvironmentVariables.Add("OMP_THREAD_LIMIT", _processorConfig.TesseractThreadLimit.ToString());
process.OutputDataReceived += (sender, e) =>
{
if (e.Data == null)
{
outputWaitHandle.Set();
}
else
{
output.AppendLine(e.Data);
}
};
process.ErrorDataReceived += (sender, e) =>
{
if (e.Data == null)
{
errorWaitHandle.Set();
}
else
{
error.AppendLine(e.Data);
}
};
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
if (!outputWaitHandle.WaitOne(ProcessTimeOutMiliseconds) && !errorWaitHandle.WaitOne(ProcessTimeOutMiliseconds) && !process.WaitForExit(ProcessTimeOutMiliseconds))
{
//To cancel the read operation if the process is stil reading after the timeout this will prevent ObjectDisposeException
process.CancelOutputRead();
process.CancelErrorRead();
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("Timed Out");
//To release allocated resource for the Process
process.Close();
//Timed out
return false;
}
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("Completed On Time");
exitCode = process.ExitCode;
if (!string.IsNullOrEmpty(standardOut))
_logger.LogStatement($"Tesseract stdOut:\n{standardOut}", LogLevel.Debug, nameof(ProcessOcrPage));
if (!string.IsNullOrEmpty(standardErr))
_logger.LogStatement($"Tesseract stdErr:\n{standardErr}", LogLevel.Debug, nameof(ProcessOcrPage));
process.Close();
return exitCode == 0 ? true : false;
}
}
Catch
{
//Handle Exception
}
}
}
Run Code Online (Sandbox Code Playgroud)
归档时间: |
|
查看次数: |
381 次 |
最近记录: |