posts - 54, comments - 139, trackbacks - 2, articles - 0
  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理

大数据量填充UI的技巧

Posted on 2007-07-30 17:50 我是程序员 阅读(3052) 评论(10)  编辑 收藏 所属分类: .NET开发技术
        这个内容本来已经在我过去的一篇随笔中略有提及,但是没有详细的说明这个问题。今天我就这个问题详细的讨论一下。这个问题的提出其实是一个源于一个设计缺陷或者错误。我所在的公司开发了一个财务系统,这个系统中有一张报表,这个报表的主要功能是显示数据检索的结果。由于在设计之初没有考虑分页处理,所以导致客户没有输入检索条件的时候,需要在界面上显示几万到几十万条记录。由于数据量非常大,导致UI的填充时间非常的漫长,为了提高UI填充的速度,采用了异步处理的方式,最初的处理方式如下:
 1 '数据传送事件
 2 Sub DataRowTransport(ByVal sender As ObjectByVal e As DataRowTransportEventArgs)
 3 
 4     '判断是否要进行线程调度
 5     If Me.InvokeRequired Then
 6 
 7         '调度线程
 8         Me.BeginInvoke(New DataRowTransportHandler(AddressOf DataRowTransport), New Object() {sender, e})
 9     Else
10 
11         '将数据填充到Grid
12         Me.FillGrid(e.Data)
13 
14         '响应事件处理
15         Application.DoEvents()
16 
17         '判断是否退出
18         If Me.Cancel Then
19 
20             e.Cancel = True
21         End If
22     End If
23 End Sub
        从代码的角度来看这段代码是没有什么问题的,基本上是微软提出的线程调度的最佳实践了,但是在实际中运用效果很不理想:界面不能刷新也不能响应取消操作(用户可以选择取消填充)。其实仔细想一想也不难明白,由于数据量很大,导致调用BeginInvoke的调用次数很大,而且两次之间的调用间隔很短,基本上把UI线程的消息循环给填满了,几乎没有机会来响应界面的操作了。
经过分析之后,发现了一个较好的解决方案,代码如下:
 1     '数据缓存
 2     Private _buffer As New ArrayList
 3 
 4     '数据填充队列
 5     Private _dataQueue As New Queue
 6 
 7     '数据传送事件,由数据传输线程触发
 8     Sub DataRowTransport(ByVal sender As ObjectByVal e As DataRowTransportEventArgs)
 9 
10         '添加到缓存
11         Me._buffer.Add(e.Data)
12 
13         '判断是否需要添加到填充队列中
14         If Me._buffer.Count > 100 Then
15 
16             '锁定
17             SyncLock Me._dataQueue
18 
19                 '添加到队列中
20                 Me._dataQueue.Enqueue(Me._buffer.ToArray())
21             End SyncLock
22 
23             '清除数据
24             Me._buffer.Clear()
25         End If
26 
27         '判断是否退出
28         If Me.cancel Then
29 
30             e.Cancel = True
31         End If
32     End Sub
33 
34     '填充数据,在UI线程调用
35     Sub FillData()
36 
37         '用于保存临时数据
38         Dim tempData As Object
39 
40         While (True)
41 
42             '设置数据为空
43             tempData = Nothing
44 
45             '判断是否取消
46             If Me.cancel Then
47 
48                 '退出循环
49                 Exit While
50             End If
51 
52             '判断是否有数据
53             If Me._dataQueue.Count > 0 Then
54 
55                 SyncLock Me._dataQueue
56 
57                     '判断是否有数据,检查两次尽可能的避免同步线程
58                     If Me._dataQueue.Count > 0 Then
59 
60                         '填充界面
61                         tempData = Me._dataQueue.Dequeue()
62                     End If
63 
64                 End SyncLock
65 
66             End If
67 
68             '判断是否有数据
69             If Not tempData Is Nothing Then
70 
71                 '填充界面
72                 Me.FillGrid(tempData)
73             End If
74 
75             '处理消息循环
76             Application.DoEvents()
77 
78         End While
79 
80     End Sub
经过实际测试,这种解决方案几乎不会延迟数据读取线程,总的时间几乎不到第一种方案的一半,而且正常的响应UI事件。通过这个事件说明,通过BeginInvoke的方式将后台线程操作调度到UI线程其实性能是比较差的(经过实际测试使用BeginInvoke调度操作UI的耗费的时间为直接跨线程不同步的操作UI耗费时间的两倍,而直接跨线程不同步调用UI耗费的时间是在UI线程中调用同一个方法的2被),在常规情况下应用还可以,但是一旦数据量很大,或者并发非常大的情况下,就需要考虑用其它的手段来处理了


0
0
(请您对文章做出评价)
« 上一篇:DotNetMock-单元测试的利器
» 下一篇:加密的故事(转载)

Feedback

#1楼   回复  引用  查看    

2007-07-30 20:15 by xiao_p      
显示几十万的数据给神仙看???
估计神仙看了也会头晕的。。。

#2楼   回复  引用  查看    

2007-07-30 20:44 by 冬冬      
我也遇到过这种问题,我觉得应该响应DataGridView的滚动事件,动态调用填充数据。

#3楼[楼主]   回复  引用  查看    

2007-07-30 20:47 by 我是程序员      
@冬冬
确实这样做比较好,但是由于以前的设计做的不灵活不支持分段取数据,另外如果读取的速度慢了还会引起超时,所以这样做也是没有办法的办法

#4楼   回复  引用    

2007-07-31 08:35 by 剑在上海^^[未注册用户]
LZ听过分页两个字没

#5楼   回复  引用  查看    

2007-07-31 08:52 by 针式个人知识库管理      
@xiao_p

显示几十万的数据给神仙看???
估计神仙看了也会头晕的。。。

一次在至多1000条就很多了,数量大的话肯定要使用分解结构或多条件查询

#6楼   回复  引用    

2007-07-31 09:25 by Bin[未注册用户]
需要那么劳神费事么?
用DataGridView或ListView的VirtualMode方法就可以了。

#7楼   回复  引用  查看    

2007-07-31 23:35 by 搜索人生      
数据库分页啊。。。。。。

#8楼   回复  引用    

2008-02-01 10:27 by mm11[未注册用户]
请问一下,你是怎么样通知界面需要填充界面的呢?

#9楼   回复  引用    

2008-02-01 11:56 by mm11[未注册用户]
我做了一下试验,由子线程大循环向主线程写入数据,按照您的第二种方法,如果使用Control.BeginInvoke把数据封送到主线程处理的话,还是出现主线程界面不能刷新也不能响应取消操作,只能使用Control.Invoke来封送,这样才能达到理想效果,不过这种方式可能会达不到“实时”状态,队列填充完指定数目以后,才会去填充界面,而且占用的内存是很大的
//由子线程调用,负责向主线程写入数据
private System.Collections.ArrayList _buffer=new System.Collections.ArrayList();
private System.Collections.Queue _dataQueue;

private System.Threading.Thread _th;

private delegate void FilesAddDelegate();
private FilesAddDelegate FilesAdd;

public Form2()
{
InitializeComponent();
FilesAdd = new FilesAddDelegate(AddFiles);
}

private void btSearch_Click(object sender, EventArgs e)
{
this._dataQueue = new System.Collections.Queue();
this._th = new System.Threading.Thread(new System.Threading.ThreadStart(ThreadProcedure));
this._th.Start();
}

private void ThreadProcedure()
{
for (int i = 0; i <= 1000000; i++)
{
string file = i.ToString() + ".txt";
this._buffer.Add(file);
if (this._buffer.Count > 100)
{
lock (this._dataQueue)
{
this._dataQueue.Enqueue(this._buffer.ToArray());
}
this._buffer.Clear();
}
//这里不能调用异步方法
//BeginInvoke(FilesAdd);
Invoke(FilesAdd);
}
}

private void AddFiles()
{
while (this._dataQueue.Count>0)
{
object temp = null;
lock (this._dataQueue)
{
if (this._dataQueue.Count > 0)
{
temp = this._dataQueue.Dequeue();
}
}
if (temp != null)
{
Array list = temp as Array;
foreach (string path in list)
{
this.FileListLB.Items.Add(path);
}
}
Application.DoEvents();
}
}
如果不使用队列,就像您的第一种方法,如果把您的第一种方法的BeginInvoke换成Invoke,在我的试验中也可以达到和第二种相同的效果。当然我填充界面的时间很短。希望能和您在这方面探讨一下
private delegate void FileListDelegate(int index);
private Thread searchThread;
private FileListDelegate fileListDelegate;

public MsdnForm()
{
InitializeComponent();
fileListDelegate = new FileListDelegate(AddFiles);
}

private void AddFiles( int index)
{
listBox.Items.Add(index.ToString());
Application.DoEvents();
}

private void ThreadProcedure()
{
for (int j = 0; j <= 1000;j++ )
{
for (int i = 0; i <= 10000; i++)
{
//这里也不能使用BeginInvoke
//BeginInvoke(fileListDelegate, new object[] { i });
Invoke(fileListDelegate, new object[] { i });
}
}
}

private void button3_Click(object sender, EventArgs e)
{
searchThread = new Thread(new ThreadStart(ThreadProcedure));
searchThread.Start();
}

#10楼[楼主]   回复  引用  查看    

2008-02-03 11:03 by 我是程序员      
@mm11
频繁的调用BeginInvoke会导致主线程的消息循环来不及处理OnPaint事件(推测),所以会出现界面变白的情况。在帖子中的处理方法是UI线程中作一个循环不停的轮询数据传送线程的数据,如果UI线程轮询到数据则填充数据,UI线程在做完这些操作后调用Application.DoEvents方法来响应事件。由于UI线程调用了Application.DoEvents方法,所以所有的UI线程消息都会被正常的处理(控件的消息都能够正常处理,所以可以正常的终止调用),所以界面可以正常的刷新。
为了验证这个你可以做一个例子:
在一个函数中做死循环调用Application.DoEvents,你会发现这个程序除了CPU占用率较高外,其他与正常的程序没有区别,如果想降低CPU的好费率可以调用Thread.Sleep来休眠个几十毫秒。
在实际的项目中使用这种方式填充界面会比调用BeginInvoke或者Invoke快好几倍。