在解析非常大的日志文件时保持UI响应

时间:2010-05-25 16:46:56

标签: c# file-io user-interface treeview backgroundworker

我正在编写一个解析非常大的日志文件的应用程序,以便用户可以以树视图格式查看内容。我使用BackGroundWorker来读取文件,并且在解析每条消息时,我使用BeginInvoke来获取GUI线程以将节点添加到我的树视图中。不幸的是,有两个问题:

  • 在解析文件时,树视图无法响应点击或滚动。我希望用户能够在文件解析时检查(即展开)节点,这样他们就不必等待整个文件完成解析。
  • 每次添加新节点时,树视图都会闪烁。

以下是表单中的代码:

private void btnChangeDir_Click(object sender, EventArgs e)
{
    OpenFileDialog browser = new OpenFileDialog();

    if (browser.ShowDialog() == DialogResult.OK)
    {
        tbSearchDir.Text = browser.FileName;
        BackgroundWorker bgw = new BackgroundWorker();
        bgw.DoWork += (ob, evArgs) => ParseFile(tbSearchDir.Text);
        bgw.RunWorkerAsync();
    }
}

private void ParseFile(string inputfile)
{
    FileStream logFileStream = new FileStream(inputfile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
    StreamReader LogsFile = new StreamReader(logFileStream);

    while (!LogsFile.EndOfStream)
    {
        string Msgtxt = LogsFile.ReadLine();
        Message msg = new Message(Msgtxt.Substring(26)); //Reads the text into a class with appropriate members
        AddTreeViewNode(msg);
    }
}

private void AddTreeViewNode(Message msg)
{
    TreeNode newNode = new TreeNode(msg.SeqNum);

    BeginInvoke(new Action(() =>
                               {
                                   treeView1.BeginUpdate();
                                   treeView1.Nodes.Add(newNode);
                                   treeView1.EndUpdate();
                                   Refresh();
                               }
                    )); 
}

需要改变什么?

修改

New code, to replace the last function above:
        List<TreeNode> nodeQueue = new List<TreeNode>(1000);

        private void AddTreeViewNode(Message msg)
        {
            TreeNode newNode = new TreeNode(msg.SeqNum);

            nodeQueue.Add(newNode);

            if (nodeQueue.Count == 1000)
            {
                var buffer = nodeQueue.ToArray();
                nodeQueue.Clear();
                BeginInvoke(new Action(() =>
                                           {
                                               treeView1.BeginUpdate();
                                               treeView1.Nodes.AddRange(buffer);
                                               treeView1.EndUpdate();
                                               Refresh();
                                               Application.DoEvents();
                                           }
                                ));
            }
        }

不知道为什么我将Refresh和DoEvents留在那里。一点测试其他评论...

4 个答案:

答案 0 :(得分:1)

Invoke/BeginInvoke在内部使用PostMessage来封送从任意线程到UI线程的请求。 BeginInvoke将使用需要由UI线程处理的消息充斥您的Windows消息队列,同时伴随着您禁用树重绘的事实,您添加的每个节点都可能影响您与之交互的能力。树正在加载。

一种选择是将多个更新一起批处理,然后发送它们批量更新树。因此,解析文件并使用每100个或一些节点更新树,而不是每次更新1个。

更新:编辑批量添加节点后,我建议如下。

1-相反使用Invoke而不是BeginInvoke,否则队列在更新树时填满,然后一旦树被更新,下一千个节点就可以插入了,这使你正确回到你在哪里。

2-插入每个批次后几百毫秒睡眠,以便有一段时间UI可以响应。您可以使用此功能,这将平衡性能与用户体验。较长的睡眠会让用户感觉更敏感,但最终会花费更长的时间来加载所有数据。

3-请注意,如果总计数不是1000的倍数,那么您当前的批处理解决方案将错过最后几个节点

    private void AddTreeViewNode(Message msg) 
    { 
        TreeNode newNode = new TreeNode(msg.SeqNum); 

        nodeQueue.Add(newNode); 

        if (nodeQueue.Count == 1000) 
        { 
            var buffer = nodeQueue.ToArray(); 
            nodeQueue.Clear(); 
            Invoke(new Action(() => 
                { 
                    treeView1.BeginUpdate(); 
                    treeView1.Nodes.AddRange(buffer); 
                    treeView1.EndUpdate(); 
                }));
            System.Threading.Thread.Sleep(500); 
        } 
    }

答案 1 :(得分:1)

你要做的第一件事就是在TreeView上启用双缓冲,这样它就会停止闪烁。这是自Vista以来的支持,但遗憾的是不是Windows Forms。在项目中添加一个新类并粘贴下面显示的代码。编译。将控件从工具箱顶部拖放到表单上。

using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;

class BufferedTreeView : TreeView {
    protected override void OnHandleCreated(EventArgs e) {
        base.OnHandleCreated(e);
        IntPtr style = (IntPtr)TVS_EX_DOUBLEBUFFER;
        SendMessage(this.Handle, TVM_SETEXTENDEDSTYLE, (IntPtr)style, (IntPtr)style);
    }
    // P/Invoke:
    private const int TVS_EX_DOUBLEBUFFER = 0x004;
    private const int TVM_SETEXTENDEDSTYLE = 0x1100 + 44;
    [DllImport("user32.dll")]
    private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wp, IntPtr lp);
}

保持UI响应需要重新设计ParseFile()方法。如上所述,它过于频繁地调用BeginInvoke()。这会使用户无法跟上的请求充斥着UI线程。它不再能够解决它的正常职责,比如绘画和响应鼠标点击。

这是浪费精力,人眼无法感知以每秒25次以上的速度发生的更新。将数据存储在集合BeginInvoke中,并以较慢的速率传递该集合。

答案 2 :(得分:0)

你尝试了Application.Doevents()方法吗?

答案 3 :(得分:0)

我没有使用分析器运行您的代码,但如果您认为缓慢的I / O作为botleneck,您可以尝试 MemoryMappedFile from .NET 4.0

因此,不是从磁盘中逐行搜索和读取,而是可以访问内存中的相同数据而不是磁盘IO。