我试图从多个并行运行的进程中获取输出。每个进程完成后,我想将其输出打印到控制台。不幸的是,没有一种流行的方法可行。
TaskCompletionSource
使用TaskCompletionSource
输出可以在调用Exited
事件时异步到达,但它常常缺失,因为执行确实不要等待缓冲区刷新。将Task.Delay
添加几(100)毫秒看起来并不合适,也并非总是有效。
WaitForExit
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; }
}
我不认为这段代码有效,因为它没有做它应该做的事情。这样,一旦完成,就会显示每个流程的整个输出。
我很好奇可以做些什么来解决这个问题?我不想简单地并行运行流程,我已经可以做到,但我对它们的输出感兴趣。
答案 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
之前至少调用过一次ErrorDataReceived
或process.Exited
- 这将使两者都失效如果进程在回调完成之前退出,则output
和error
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();
};