大数据量填充UI的技巧

        这个内容本来已经在我过去的一篇 随笔中略有提及,但是没有详细的说明这个问题。今天我就这个问题详细的讨论一下。这个问题的提出其实是一个源于一个设计缺陷或者错误。我所在的公司开发了一个财务系统,这个系统中有一张报表,这个报表的主要功能是显示数据检索的结果。由于在设计之初没有考虑分页处理,所以导致客户没有输入检索条件的时候,需要在界面上显示几万到几十万条记录。由于数据量非常大,导致UI的填充时间非常的漫长,为了提高UI填充的速度,采用了异步处理的方式,最初的处理方式如下:
 1  ' 数据传送事件
 2  Sub  DataRowTransport( ByVal  sender  As   Object ByVal  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   Object ByVal  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被),在常规情况下应用还可以,但是一旦数据量很大,或者并发非常大的情况下,就需要考虑用其它的手段来处理了


你可能感兴趣的:(大数据量)