【问题标题】:Task return a StreamReader in c#任务在c#中返回一个StreamReader
【发布时间】:2019-01-02 01:37:25
【问题描述】:

我在 C# 中有这个任务应该返回 DISM 的标准输出,所以我可以在需要的地方使用它:

public async Task<StreamReader> DISM(string Args)
{

   StreamReader DISMstdout = null;

    await Task.Run(() =>
    {
        Process DISMcmd = new Process();

        if (Environment.Is64BitOperatingSystem)
        {
            DISMcmd.StartInfo.FileName = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "SysWOW64", "dism.exe");
        }
        else
        {
            DISMcmd.StartInfo.FileName = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "System32", "dism.exe");
        }

        DISMcmd.StartInfo.Verb = "runas";

        DISMcmd.StartInfo.Arguments = DISMArguments;

        DISMcmd.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
        DISMcmd.StartInfo.CreateNoWindow = true;
        DISMcmd.StartInfo.UseShellExecute = false;
        DISMcmd.StartInfo.RedirectStandardOutput = true;
        DISMcmd.EnableRaisingEvents = true;
        DISMcmd.Start();

        DISMstdout = DISMcmd.StandardOutput;

        DISMcmd.WaitForExit();
    });
    return DISMstdout;
}

但这并没有真正起作用。 如果我想读取另一个任务的标准输出,我不能(因为它是空的)所以我的任务一定有问题?

public async Task Test()
{
    await Task.Run(() =>
    {

    StreamReader DISM = await new DISM("/Get-ImageInfo /ImageFile:" + ImagePath + @" /Index:1");

    string data = string.Empty;
     MessageBox.Show(DISM.ReadToEnd()); // this should display a msgbox with the standardoutput of dism

     while ((data = DISM.ReadLine()) != null)
     {
         if (data.Contains("Version : "))
         {
               // do something
         }
     }
   }); 
}

这段代码有什么问题?

【问题讨论】:

  • 您在进程退出后返回进程的标准输出流,此时进程不再存在,我猜它的标准 IO 流也不存在 - .NET 必须保留它们直到为您朗读为止。
  • 在调用WaitForExit() 之前尝试从流中读取数据,我敢打赌你的数据会在那里。我会做一些事情,比如将回调传递给从流中读取的DISM(),并将回调的结果作为方法的返回值返回。然后,一旦您通过 await DISM(...),整个 shebang 就会执行
  • 最后但同样重要的是,我不希望 ReadLine() 在您调用 ReadToEnd() 后返回任何内容。 ReadToEnd() 并不意味着“读取所有当前可用的数据”,它意味着“阻塞直到流关闭并且不再有数据可用”
  • 在您读取程序的所有输出之前,程序无法退出。所以 WaitForExit() 很可能会死锁。目前尚不清楚为什么需要它,但如果需要,请考虑 Exited 事件。请改用 StringReader。
  • 尝试将调用方式改为:StreamReader DISMoutput = await this.DISM("(...)");。 (没有new)完全消除Task.Run() lambda。在另一种方法中,消除DISMcmd.EnableRaisingEvents = true;。应该去异步。并检查您传递的参数。

标签: c# async-await task


【解决方案1】:

在使用 Benchmark.NET 解决这个问题后,似乎启动一个进程(我尝试了 DISM 和 Atom 有一些重要的东西)——从设置到Start()——大约需要 50 毫秒。这对我来说似乎可以忽略不计。毕竟,对于玩英雄联盟来说,50 毫秒的延迟已经足够了,而且你不会在一个紧凑的循环中开始这些。

我想提供“不要打扰 Task.Run() 并直接使用异步 I/O”的替代答案,除非您绝对需要摆脱这种延迟并相信产生后台线程会有所帮助:

static string GetDismPath()
{
    var windowsDir = Environment.GetFolderPath(Environment.SpecialFolder.Windows);
    var systemDir = Environment.Is64BitOperatingSystem ? "SysWOW64" : "System32";
    var dismExePath = Path.Combine(windowsDir, systemDir, "dism.exe");

    return dismExePath;
}

static Process StartDism(string args)
{
    var proc = new Process
    {
        StartInfo =
        {
            FileName = GetDismPath(),
            Verb = "runas",
            Arguments = args,
            WindowStyle = ProcessWindowStyle.Hidden,
            CreateNoWindow = true,
            UseShellExecute = false,
            RedirectStandardOutput = true
        }
    };

    proc.Start();

    return proc;
}
static void Cleanup(Process proc)
{
    Task.Run(async () =>
    {
        proc.StandardInput.Close();
        var buf = new char[0x1000];
        while (await proc.StandardOutput.ReadBlockAsync(buf, 0, buf.Length).ConfigureAwait(false) != 0) { }
        while (await proc.StandardError.ReadBlockAsync(buf, 0, buf.Length).ConfigureAwait(false) != 0) { }

        if (!proc.WaitForExit(5000))
        {
            proc.Kill();
        }
        proc.Dispose();
    });
}
static async Task Main(string[] args)
{
    var dismProc = StartDism("/?");

    // do what you want with the output
    var dismOutput = await dismProc.StandardOutput.ReadToEndAsync().ConfigureAwait(false);

    await Console.Out.WriteAsync(dismOutput).ConfigureAwait(false);
    Cleanup(dismProc);
}

我只是使用Task.Run() 来保持主线程的清理工作,以防你需要做其他事情,而 DISM 继续产生你不感兴趣的输出,你不想直接杀死。

【讨论】:

    【解决方案2】:

    相对于传统的异步方法,我编写你的方法来利用 async..await 的方式是这样的:

    public async Task<TResult> WithDism<TResult>(string args, Func<StreamReader, Task<TResult>> func)
    {
        return await Task.Run(async () =>
        {
            var proc = new Process();
    
            var windowsDir = Environment.GetFolderPath(Environment.SpecialFolder.Windows);
            var systemDir = Environment.Is64BitOperatingSystem ? "SysWOW64" : "System32";
            proc.StartInfo.FileName = Path.Combine(windowsDir, systemDir, "dism.exe");
    
            proc.StartInfo.Verb = "runas";
    
            proc.StartInfo.Arguments = args;
    
            proc.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
            proc.StartInfo.CreateNoWindow = true;
            proc.StartInfo.UseShellExecute = false;
            proc.StartInfo.RedirectStandardOutput = true;
            proc.Start();
    
            Console.Error.WriteLine("dism started");
    
            var result = await func(proc.StandardOutput);
    
            Console.Error.WriteLine("func finished");
            // discard rest of stdout
            await proc.StandardOutput.ReadToEndAsync();
            proc.WaitForExit();
    
            return result;
        });
    }
    

    实际上,在生成进程时可能会发生严重阻塞的唯一部分是在您处理它产生的输出时。像这样使用:

    var task = WithDism("/?", async sr => await sr.ReadToEndAsync()); // or process line-by-line
    Console.WriteLine("dism task running");
    Console.WriteLine(await task);
    

    它产生以下输出

    dism 任务运行
    dism 开始
    函数完成

    错误:740

    需要提升权限才能运行 DISM。
    使用提升的命令提示符来完成这些任务。


    请注意,在使用子进程时,您的工作是确保它们正确退出或关闭以避免留下僵尸进程。这就是为什么我添加了可能多余的 ReadToEndAsync() - 以防 func 仍然留下一些未消耗的输出,这应该允许该过程达到其自然结束。

    但是,这意味着调用函数只有在发生这种情况时才会继续。如果您留下很多您不感兴趣的未使用的输出,这将导致不必要的延迟。您可以通过将此清理生成到不同的后台任务并立即使用以下内容返回结果来解决此问题:

    Task.Run(() => {
        // discard rest of stdout and clean up process:
        await proc.StandardOutput.ReadToEndAsync();
        proc.WaitForExit();
    });
    

    但我承认我在这方面有点走投无路,我不完全确定让任务像这样“疯狂”的鲁棒性。当然,清理进程的适当方法将取决于在您从func 获得想要返回的输出后它实际在做什么。


    我在那里使用对 Console 的同步调用,因为它们仅用于说明事件的时间安排,我想知道随着执行到达该点。通常,您会以“病毒式”方式使用异步,以确保控制权尽快传递回顶层。

    【讨论】:

    • @Jimi - 最后一部分是“取决于”的东西。如果您可以依靠传递给WithDismfunc 来消耗整个流,那么它几乎没有什么区别。但是,如果你只是在输出中间寻找一个特定的行,那么在返回结果之前等待所有后面的行都被读取是没有意义的。事实上,甚至可能没有理由让它完成运行,因此您可能想中断它而不是读取可能有很多不需要的输出。
    • 但这是 .NET/Windows,您不能只将 SIGINT 发送到进程,因此您需要自行决定如何完全干净地或不干净地关闭生成的进程。
    • 这是因为 Process 通常使用自己的事件处理(它被构建为事件驱动,与异步模式不同,但仍然有效),因此您订阅其 Exited 事件(带有 @ 987654333@) 并最终有一个超时/完成逻辑来中断进程(有点苛刻,但Proces.Kill 引发了Exited 事件)=> 关于sending a signal in Windows(不同的语言,我同意:)
    • @Jimi 所以从技术上讲,在这里使用 async/await 你甚至不需要事件,你只需阅读你需要的内容,设置一个计时器,如果它仍然运行,在后台任务中终止进程?
    • @Jimi 出于对此的好奇,我在 Code Review 上发布了这个答案的略微修改版本,您可能想在此处回答您的 cmets 以获得我的至少一个支持:codereview.stackexchange.com/questions/200287/…
    猜你喜欢
    • 2011-02-19
    • 2018-01-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多