繁体   English   中英

以编程方式启动批处理文件(带有输出重定向)时冻结,当批处理文件仅调用“start some.exe”时

[英]Freeze when programmatically launching a batch file (with output redirection), when the batch file simply calls "start some.exe"

编辑:发现一些没有答案的重复项:

  1. 批量输出重定向问题
  2. 如何在不继承子进程中的句柄的情况下使用“开始”命令?

我有一些 C# 代码试图在一个更大的长时间运行的程序中成为一个通用的进程启动器。 此代码需要捕获它启动的进程的输出,并等待它们完成。 我们通常使用此代码启动批处理文件,并且一切正常,除非我们想从批处理文件内部启动另一个子进程,这样它的寿命将超过批处理文件进程。 例如,假设我只想从批处理文件中执行“ start notepad.exe ”。

我遇到了与此问题相同的问题: 即使 Process.HasExited 为 true,Process.WaitForExit 也不会返回

基本上,即使批处理文件进程似乎退出得非常快(如预期),我的程序也会冻结,直到子进程(例如记事本)也退出。 但是,在我的情况下,记事本并不打算退出。

我尝试在链中的所有点注入“cmd.exe /C”,但没有成功。 我尝试使用“exit”或“exit /B”明确终止批处理文件。 我尝试同步和异步读取输出 - 有或没有工作线程。 我在这里尝试了模式: https : //github.com/alabax/CsharpRedirectStandardOutput/tree/master/RedirectStandardOutputLibrary (参见 FixedSimplePattern.cs 和 AdvancedPattern.cs),但还是没有运气。

编辑:我还尝试使用一些 C# P/Invoke 代码来执行通过 Windows API(CreatePipe/CreateProcess 等)启动的进程,所以我认为这个问题不是 C# Process API 特有的。

我发现的唯一解决方法是用一个工具替换 start 命令,该工具使用 DETACHED_PROCESS 标志调用 CreateProcess(CREATE_NO_WINDOW 也可以)。

在上述 SO 问题( https://stackoverflow.com/a/26722542/5932003 )中接受的答案是整个 Internet 中最接近似乎有效的东西,但事实证明它在您启动的每个批处理文件中都在泄漏线程. 我会在那里发表评论,但我还没有这样做的声誉:)。

演示线程泄漏的修改代码:

using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace TestSO26713374WaitForExit
{
    class Program
    {
    static void Main(string[] args)
    {
        while(true)
        {
            string foobat =
@"@echo off
START ping -t localhost
REM START ping -t google.com
REM ECHO Batch file is done!
EXIT /B 123
";

            File.WriteAllText("foo.bat", foobat);

            Process p = new Process
            {
                StartInfo =
                new ProcessStartInfo("foo.bat")
                {
                    UseShellExecute = false,
                    RedirectStandardOutput = true,
                    RedirectStandardError = true
                }
            };

            p.Start();

            var _ = ConsumeReader(p.StandardOutput);
            _ = ConsumeReader(p.StandardError);

            //Console.WriteLine("Calling WaitForExit()...");
            p.WaitForExit();
            //Console.WriteLine("Process has exited. Exit code: {0}", p.ExitCode);
            //Console.WriteLine("WaitForExit returned.");


            ThreadPool.GetMaxThreads(out int max, out int max2);
            ThreadPool.GetAvailableThreads(out int available, out int available2);

            Console.WriteLine(
                $"Active thread count: {max - available} (thread pool), {System.Diagnostics.Process.GetCurrentProcess().Threads.Count} (all).");

            Thread.Sleep(8000);
        }
    }

    async static Task ConsumeReader(TextReader reader)
    {
        string text;

        while ((text = await reader.ReadLineAsync()) != null)
        {
            Console.WriteLine(text);
        }
    }
}
}

上面的输出:

Active thread count: 2 (thread pool), 15 (all).
Active thread count: 4 (thread pool), 18 (all).
Active thread count: 6 (thread pool), 19 (all).
Active thread count: 8 (thread pool), 20 (all).
Active thread count: 9 (thread pool), 21 (all).
Active thread count: 11 (thread pool), 23 (all).
Active thread count: 13 (thread pool), 25 (all).
Active thread count: 15 (thread pool), 27 (all).
Active thread count: 17 (thread pool), 29 (all).
Active thread count: 19 (thread pool), 31 (all).
Active thread count: 21 (thread pool), 33 (all).
...

我的问题:

  1. 为什么 start 命令没有完全打破输出重定向链?
  2. 我是否坚持使用上述调用 CreateProcess(...DETACHED_PROCESS...) 的工具?

谢谢!

这是start命令的替代品:

start / b powershell.exe启动进程-FilePath“notepad.exe”

“start / b”仅用于启动powershell而不显示其窗口(否则会闪烁一秒钟 - 令人讨厌)。 Powershell在此之后接管并启动我们的流程,没有任何副作用。

如果powershell不是一个选项,事实证明这个超级简单的C#程序可以起到同样的作用:

using System.Diagnostics;

namespace Test
{
    class Program
    {
        static void Main(string[] args)
        {
            // You may want to expand this to set CreateNoWindow = true
            Process.Start(args[0]);
        }
    }
}

不需要CreateProcess(... DETACHED_PROCESS ...)。

我对第一个问题没有答案。 这更像是Windows的基本设计或实现细节,我不能说他们为什么决定这样做。

至于第二个问题......

不幸的是,由于它的实现方式,这似乎是Process类的限制。

使用stdout和stderr流的异步读取可以解决WaitForExit()问题,但是没有做任何事情来解决Windows将父进程绑定到子进程的基本方式。 在子进程退出之前,父进程的输出流(被重定向)将不会关闭,因此在父进程流上的C#程序中仍有未完成的读取,等待这些流被关闭。

Process类中,对于重定向输出,它将I / O流句柄包装在FileStream对象中,并且当它创建此对象时, 它不会在async标志设置为true创建它 ,这对于对象来说是必需的。使用IOCP进行异步操作。 它甚至不会创建具有IOCP支持的底层本机管道对象。

因此,当代码在流上发出异步读取时,这是通过“伪造”异步在.NET中实现的。 也就是说,它只是在常规线程池上排队同步读取。

因此,您为每个未完成的读取获得一个新的活动线程池线程,每个进程两个。 并且在子进程退出之前,输出流中的最后一次读取将不会返回,从而占用这些线程池线程。

我没有看到任何避免这种情况的好方法。

对于一个你知道在stdout和stderr流上输出量非常小的进程(或者根本没有...在例子中,没有任何内容写入stderr,例如),你可能只是没有阅读流。 但是每个流的缓冲区不是很大,因此无法读取它们通常会导致进程最终阻塞。

在某些特殊情况下,您可以安排一些事情,以便在您知道自己最终时不会从流中读取内容。 例如,在上面的代码中,您可以取消注释“批处理文件已完成!” 当您看到该文本时终止ConsumeReader()循环。 但这不是一个可以在很多情况下起作用的解决方案。 它将依赖于确切地知道将要写入的内容,至少对于stdout和stderr流的最后一行。

至于它的价值,至少我会说你并没有真正泄漏线程。 每个线程实际上都在做某事,一旦关联的进程退出,每个线程实际上最终都会被释放。 坦率地说,在Windows上,一个线程比一个进程花费的成本要少得多,所以如果你有足够的子进程同时运行你担心你的进程中的线程数量,你可能会有更大的鱼来炸(即所有那些儿童进程都在耗费资源)。

在研究了我能做什么后,我认为你当前的解决方案,一种用来代替start命令创建你的进程作为一个独立的孩子的工具,可能是最优雅的。 唯一可靠的替代方案是让您有效地重新实现Process类本身(或者至少是您在此处使用的部分),除了IOCP支持,以便标准流上的读取实际上是异步实现的。

我想你可以尝试向微软报告这个问题,但我想在这一点上,他们没有兴趣对Process类进行任何更改。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM