让我先说一下,我绝对不是一个专业的c#程序员,到目前为止,我的大多数小程序都是用蛮力完成的。
我正在研究一个小的WinForms应用程序SSH到几个设备,tail -f
每个日志文件,并在文本框中显示实时输出,同时也保存到日志文件。现在,它可以工作,但是在日志记录期间占用了近30%的CPU,我确信我做错了什么。
在创建SshClient
并连接之后,我像这样运行tail命令(这些变量是为每个连接存在的记录器类的一部分):
command = client.CreateCommand("tail -f /tmp/messages")
result = command.BeginExecute();
stream = command.OutputStream;
我有一个日志读取/写入功能:
public async Task logOutput(IAsyncResult result, Stream stream, TextBox textBox, string logPath)
{
// Clear textbox ( thread-safe :) )
textBox.Invoke((MethodInvoker)(() => textBox.Clear()));
// Create reader for stream and writer for text file
StreamReader reader = new StreamReader(stream, Encoding.UTF8, true, 1024, true);
StreamWriter sw = File.AppendText(logPath);
// Start reading from SSH stream
while (!result.IsCompleted || !reader.EndOfStream)
{
string line = await reader.ReadLineAsync();
if (line != null)
{
// append to textbox
textBox.Invoke((Action)(() => textBox.AppendText(line + Environment.NewLine)));
// append to file
sw.WriteLine(line);
}
}
}
对于每个设备连接,我以如下方式调用
Task.Run(() => logOutput(logger.result, logger.stream, textBox, fileName), logger.token);
一切都很好,这只是CPU使用的问题。我猜我在每个日志进程中创建了一个以上的线程,但我不知道为什么或如何解决这个问题。
上面的代码有什么明显的修复吗?或者更好-是否有一种方法来设置一个回调,只打印新数据时,result
对象获得新的文本?
非常感谢所有的帮助!
编辑3/4/2021
我尝试了一个使用CopyToAsync
的简单测试,通过将logOutput()
中的代码更改为以下内容:
public async Task logOutput(IAsyncResult result, Stream stream, string logPath)
{
using (Stream fileStream = File.Open(logPath, FileMode.OpenOrCreate))
{
// While the result is running, copy everything from the command stream to a file
while (!result.IsCompleted)
{
await stream.CopyToAsync(fileStream);
}
}
}
然而,这将导致文本文件永远没有数据写入,并且CPU使用实际上稍微更糟。
第二次编辑3/4/2021
进行更多的调试,似乎只有在没有新数据传入时才会出现高CPU使用率。据我所知,这是因为无论正在运行的SSH命令是否有实际的新数据,ReadLineAsync()
方法都在不断触发,并且它以尽可能快的速度运行,占用所有CPU周期。虽然我不完全确定为什么会这样,但我真的需要一些帮助。我本以为ReadLineAsync()
会简单地等待,直到从SSH命令中有新的行可用,然后继续。
解决方案最终比我想象的要简单得多。
SSH有一个已知的bug。NET中,当没有实际接收到新数据时,命令的OutputStream
将不断吐出空数据。这导致我代码中的while循环尽可能快地运行,在这个过程中消耗了大量的CPU。
解决方案就是在循环中添加一个短的异步延迟。只有当接收到的数据是null
时,我才包括延迟,这样当有实际有效的数据通过时,读取不会中断。
while (!result.IsCompleted && !token.IsCancellationRequested)
{
string line = await reader.ReadLineAsync();
// Append line if it's valid
if (string.IsNullOrEmpty(line))
{
await Task.Delay(10); // prevents high CPU usage
continue;
}
// Append line to textbox
textBox.Invoke((Action)(() => textBox.AppendText(line + Environment.NewLine)));
// Append line to file
writer.WriteLine(line);
}
在Ryzen 5 3600上,这使我的CPU使用率从程序运行时的~30-40%降至数据流动时的不到1%。好多了。