偶尔不会从并行运行的进程获取输出

时间:2017-11-16 05:16:04

标签: c# multithreading process

我试图从多个并行运行的进程中获取输出。每个进程完成后,我想将其输出打印到控制台。不幸的是,没有一种流行的方法可行。

  1. 使用TaskCompletionSource
  2. 使用TaskCompletionSource输出可以在调用Exited事件时异步到达,但它常常缺失,因为执行确实不要等待缓冲区刷新。将Task.Delay添加几(100)毫秒看起来并不合适,也并非总是有效。

    1. 使用WaitForExit
    2. WaitForExit的问题在于它确实并行工作,但它等待所有进程完成。只有这样,您才能打印出他们的结果,这样您才能看到任何进展,直到他们全部退出。

      为了演示它,我创建了一个演示应用程序。它调用拼写错误的ipconfig。如果您在LINQPad中运行几次,那么您会在某个时刻看到此行会启动通知您没有输出。

      if (temp.OutputLength == 0 && temp.ErrorLength == 0) temp.Dump();
      

      这里是重现问题的测试应用程序。故意ipconfig被误导以引发Output问题。

      void Main()
      {
          var testCount = 30;
      
          var tasks = Enumerable.Range(0, testCount).Select(i => Task.Run(() => RunTestProcess()));
      
          Task.WaitAll(tasks.ToArray());
      
          Console.WriteLine("Done!");
      
      }
      
      private static object _consoleSyncLock = new object();
      private static volatile int counter = 0;
      
      public static async Task RunTestProcess()
      {
      
          var stopwatch = Stopwatch.StartNew();
          var result = await CmdExecutor.Execute("ipconfigf", $"", "/Q", "/C");
      
          lock (_consoleSyncLock)
          {
              var temp = new
              {
                  OutputLength = result.Output.Length,
                  ErrorLength = result.Error.Length,
                  Thread.CurrentThread.ManagedThreadId,
                  stopwatch.Elapsed,
                  Counter = counter++
              };
      
              if (temp.OutputLength == 0 && temp.ErrorLength == 0) temp.Dump();
          }
      }
      
      public class CmdExecutor
      {
          public static Task<CmdResult> Execute(string fileName, string arguments, params string[] cmdSwitches)
          {
              Console.WriteLine(nameof(Execute) + " - " + Thread.CurrentThread.ManagedThreadId);
      
              if (cmdSwitches == null) throw new ArgumentNullException(nameof(cmdSwitches));
              if (fileName == null) throw new ArgumentNullException(nameof(fileName));
              if (arguments == null) throw new ArgumentNullException(nameof(arguments));
      
              arguments = $"{string.Join(" ", cmdSwitches)} {fileName} {arguments}";
              var startInfo = new ProcessStartInfo("cmd", arguments)
              {
                  UseShellExecute = false,
                  RedirectStandardOutput = true,
                  RedirectStandardError = true,
                  CreateNoWindow = true,
              };
      
              var tcs = new TaskCompletionSource<CmdResult>();
      
              var process = new Process { StartInfo = startInfo, EnableRaisingEvents = true };
              {
                  var output = new StringBuilder();
                  var error = new StringBuilder();
      
                  process.OutputDataReceived += (sender, e) =>
                  {
                      output.AppendLine(e.Data);
                  };
      
                  process.ErrorDataReceived += (sender, e) =>
                  {
                      error.AppendLine(e.Data);
                  };
      
                  process.Exited += (sender, e) =>
                  {
                      tcs.SetResult(new CmdResult
                      {
                          Arguments = arguments,
                          Output = output.ToString(),
                          Error = error.ToString(),
                          ExitCode = process.ExitCode
                      });
                      process.Dispose();
                  };
      
                  process.Start();
                  process.BeginOutputReadLine();
                  process.BeginErrorReadLine();
      
                  return tcs.Task;
              }
          }
      }
      
      [Serializable]
      public class CmdResult
      {
          public string Arguments { get; set; }
          public string Output { get; set; }
          public string Error { get; set; }
          public int ExitCode { get; set; }
      }
      

      我不认为这段代码有效,因为它没有做它应该做的事情。这样,一旦完成,就会显示每个流程的整个输出。

      我很好奇可以做些什么来解决这个问题?我不想简单地并行运行流程,我已经可以做到,但我对它们的输出感兴趣。

2 个答案:

答案 0 :(得分:3)

您可以等待进程退出Exited事件本身:

process.Exited += (sender, e) =>
{
    // here
    ((Process)sender).WaitForExit();
    tcs.SetResult(new CmdResult
    {
        Arguments = arguments,
        Output = output.ToString(),
        Error = error.ToString(),
        ExitCode = process.ExitCode
    });
    process.Dispose();
};

通过这种添加,我总是使用您的示例代码获得整个输出。

答案 1 :(得分:2)

对我而言,错误似乎是假设在OutputDataReceived之前至少调用过一次ErrorDataReceivedprocess.Exited - 这将使两者都失效如果进程在回调完成之前退出,则outputerror StringBuilders为空。

通过添加第二个TaskCompletionSource来监视至少一个错误/数据回调的存在,我能够可靠地始终至少调用一个数据回调。

我还将StringBuilders更改为ConcurrentBags以确保安全 - 我不是100%肯定回调中使用的线程:

var tcsGotData = new TaskCompletionSource<bool>();
var output = new ConcurrentBag<string>();
var error = new ConcurrentBag<string>();

process.OutputDataReceived += (sender, e) =>
{
    output.Add(e.Data);
    tcsGotData.TrySetResult(true);
};

process.ErrorDataReceived += (sender, e) =>
{
    error.Add(e.Data);
    tcsGotData.TrySetResult(true);
};

process.Exited += (sender, e) =>
{
    tcsGotData.Task.Wait(); // You might want to put a timeout here, though ...
    tcs.SetResult(new CmdResult
    {
        Arguments = arguments,
        Output = string.Join("", output),
        Error = string.Join("", error),
        ExitCode = process.ExitCode
    });
    process.Dispose();
};