收稿日期:2021-01-04
基金项目:安徽省高等学校省级质量工程教学研究一般项目(2019jyxm0819);安徽省高等学校省级质量工程项目(2017sjjd079);
安徽省高等学校自然科学重点项目(KJ2018A0909);安徽财贸职业学院校级项目(2019xqgj13).
作者简介:1.周沭玲,女,江苏沭阳人,合肥财经职业学院人工智能学院讲师(安徽合肥230061).2.金楠,通化市委党校.3.侯
海平,安徽财贸职业学院信息工程学院副教授.
2021年第4期
第42卷总第313
期
学报
基于WPF 消息机制的
多UI 线程并发阻塞问题解决方案
周沭玲1,金
楠2,侯海平3
摘要:当前,软件产品的用户体验越来越受到人们的关注,影响用户体验的主要因素之一就是
UI 线程是否阻塞.在用户场景中多UI 控件的高并发访问会造成UI 线程阻塞,导致用户体验迅速下降.该文主要探讨WPF 框架下UI 线程并发阻塞问题,通过制作实验描述UI 线程阻塞的具体问题,在分析Windows 消息机制的基础上,提出利用发送同步消息和异步消息方法来解决UI 并发阻塞问题.最后提出一种多UI 线程并发阻塞问题的解决方案,并通过实验验证了这种方案的实际效果.
关键词:WPF ;UI 线程;消息机制;阻塞中图分类号:TP311
文献标志码:A
文章编号:1008-7974(2021)04-0090-07
DOI :10.13877/jki22-1284.2021.04.015
随着互联网技术的快速发展,各种应用软件层出不穷,例如即时通讯软件、办公软件、信息资讯、购物娱乐软件等,用户花在软件上的时间越来越多,时间不断被各种软件割裂,用户时间的碎片化越趋明显.通过应用软件占领用户的时间、增加用户粘度是企业追求的目标,实现这一目标的关键就是提升应用软件操作的用户体验.用户的操作响应速度则是提升应用软件用户体验的关键因素之一,通常一个用户无法忍受3~5秒以上的响
应等待.例如Android 操作系统中服务的响应时间要求为10秒以内,广播消息的响应时间为10~60秒,UI 操作的响应时间为5秒以内.当程序响应超出这个时间,就会出现卡顿假死状态,用户被迫等待,无法进行软件的下一步操作[1].这就会造成用户放弃使用或者卸载该软件,软件用户的流失对于互联网时代的企业是无法接受的,因此减少UI 线程阻塞成为优化软件性能的主要研究对象[2].
大多数基于不同平台的开发框架都支持
周沭玲,等:基于WPF消息机制的多UI线程并发阻塞问题解决方案
线程技术,开发者可以将耗时费力的工作任务迁移到子线程中去运行,从而减少主线程或者UI线程压力[3],做到快速响应用户操作,然而这种解决传统单UI线程阻塞问题的方法并不适合多UI控件高并发访问场景.
本文提出一套多UI线程高并发的解决方案,涉及多UI线程、操作系统消息机制、子线程通信等知识.整体实验过程如下:首先,还原传统单UI线程阻塞问题的解决方法;再次,模拟单个UI线程高并发访问的问题场景,发现使用传统方法无法解决阻塞问题,从而引出操作系统消息机制;第三,使用操作系统消息机制解决单个UI控件高并发访问的阻塞问题;最后,模拟多个UI控件高并发访问的阻塞问题,提出将每个UI控件放入独立UI线程中的解决方法,利用操作系统消息机制实现多个子线程与多个UI线程通信,最终实现多个UI控件高并发条件下也能够实时刷新.
以下实验全部在Windows操作系统环境中完成,采用Windows Presentation Foundation 开发框架技术实现实验功能,下文统一简称WPF.
1问题场景
1.1传统场景中问题及常规处理方法
传统业务场景中常见的问题如“下载数据时更新UI界面中的进度条”“用户使用网络时实时监控网络流量和
速度”“接收通知消息显示到UI界面中”等,通过建立子线程或服务程序都能得到很好地解决[4].在主线程或UI 线程中开启子线程或服务,将上述耗时且易产生线程阻塞的工作任务添加到子线程或服务中执行,等到子线程或服务中的任务执行完成后,系统再回传完成的消息给主线程,继而完成一次耗时任务处理[5].可以看出,开启子线程或服务这种方法能够较好处理此类问
题.
1.2UI控件高并发访问场景中问题的发现
view ui框架如果软件在相对较短的时间内加载较少
图片时(加载图片相当于线程中的工作任务),
并不会暴露出软件的性能问题.但如果切换
到新场景中,多人共同操作UI界面,需要将软
件加载图片的数量增加到10000张(相当于UI线程工作任务量较大,此处可以看成是一个耗时任务),甚至更多的图片,实验结果发
现此时UI控件则会出现假死状态,即使提高
计算机性能配置也很难改变这一现象,因为
无法知道用户是否需要加载更多的图片.
为了能够解决上述问题,尝试采用1.1中
常规处理方法.模拟实验过程为:主线程创建ListView控件用于呈现10000张图片,而呈现10000张图片是一件非常耗费时间的任务,按照1.1中处理方法需要将这个耗时任务放到子线程中去完成,结果发现这样并不能启动这个子线程,因为它违背了WPF线程亲缘性规则.WPF线程亲缘性要求控件的创建和使用必须在同一个线程中,而当前情形是在主线程中创建Listview,在子线程中访问Listview,两个线程同时拥有一个控件,这是不被WPF 开发框架允许的,因此实验失败.
在监控多个客户端数据的场景中,作为
服务器一端实时获取多个客户端数据,并同
时呈现到UI界面上多个控件中,服务器端程
序UI界面中每一个控件对应一个客户端,并
负责呈现对应客户端的数据,如果其中一个
控件处在高并发处理数据中,则整个UI界面
会出现假死状态,其他控件更是无法处理对
应的客户端数据.
同样尝试采用上述子线程方法处理多个
客户端数据.模拟实验过程为:主线程中创建
2021年
第4
期
学报
多个UI 控件,根据客户端创建对应的子线程,有多少客户端就建立多少个子线程,每个子线程用于接收客户端数据,根据前述实验结论可以发现不能在子线程中操作其他线程创建的控件.另外,如果在子线程中采用循环方式采集对应客户端的数据,也非常容易造成UI 线程阻塞,因为这些数据最终还是要通过循环方式加载到UI 控件中,循环加载是造成UI 线程阻塞的主要因素.因此,采用传统子线程处理耗时任务的方式并不能成功解决UI 控件高并发问题.
2
使用WPF 消息机制解决单UI 线程阻
塞问题
2.1
WPF 消息机制原理
WPF 中UI 控件造成卡顿假死现象是由于
UI 控件所在线程阻塞造成的.WPF 消息机制给解决此类问题提供了可能性.其原理如图1
所示.
步骤1:Windows 操作系统收到中断消息,使用PostMessage ()方法将消息发送给Mes⁃sage Queue (消息队列),这些中断消息可以是用户鼠标的点击、键盘的输入,也可以是封装的
Message 消息.在使用PostMessage ()发送消息时,将最新的Message 插入到Message Queue 尾部.
步骤2:调用Dispatcher.PushFrameImpl ()方法消费Message Queue 中的消息,这是一种循环机制,Dispatch 内部通过GetMessage ()方法不断地从Message Queue 中获取消息.
步骤3:在WPF 中,Dispatcher 将获取的消息分发到指定的窗口,对于一个WPF 程序来说将会有一个隐藏的窗口来接收分发的消息.
步骤4:这个隐藏窗口使用类似Win32系统中WndProc ()方法处理收到的消息,从而更新UI 界面.
步骤5:如果当前窗口又产生新的消息,将再交由Windows 操作系统来处理消息,进入下一轮循环.2.2
拆分任务解决线程阻塞问题
通常UI 线程阻塞是因为在当前窗口处理的任务过大,耗时过多,任务不能及时处理,造成UI 线程不能接收Windows 操作系统传来的消息造成的.例如UI 线程在处理一个大任务(加载10000张图片)时,Windows
操作系统
图1WPF 框架中Windows 消息机制时序图
周沭玲,等:基于WPF消息机制的多UI线程并发阻塞问题解决方案
的消息就无法及时传递到当前窗口,导致UI 界面假死状态.窗口标题栏会出现“没有响应”字样.
对以上过程中步骤4进行分析,如果UI 线程接收到的操作系统消息指令是处理一个工作量较大的任务时,可以将工作量过大的任务切分成一个个小的任务,每一个小的任务完成后,向Windows操作系统传递一个消息,从而保证UI线程可以正常接收到Win⁃dows操作系统的消息,让当前UI线程有响应.
例如:在处理加载10000张图片时,如果等10000张图片加载完成再去更新UI线程,就会造成长时间阻塞UI线程.因此不必一次加载全部图片,可以一次只加载10张图片,然后通过发送消息给操作系统更新一次UI线程,总的任务就可以分解成1000次去更新UI线程,从而在界面响应上保证用户的体验,这种方法称为“拆分任务”.通过“拆分任务”在上一个任务处理和下一个任务处理的空档中,利用WPF的消息机制将消息传递到当前窗口进行处理,保证UI线程及时响应.
2.3使用WPF中Dispatcher实现任务拆分实时更新UI线程
WPF对应用程序中产生的消息使用Dis⁃patcherOperation进行了封装,这种封装暴露了消息的优先级Priority,定义了DispatcherOpera⁃tion消息的结束事件和取消事件.通过Dis⁃patcher对象创建消息、处理窗口消息形成消息产生到消费的闭环,这就为解决UI线程阻塞提供了可能性.具体做法如下:
步骤1:开发者调用Dispatcher的Invoke或BeginInvoke,发送DispatcherOperation消息,确定消息的Priority级别.
步骤2:该DispatcherOperation消息加入到DispatcherOperation消息队列中,也就是之前所说的Message Queue中.
步骤3:对应的隐藏窗口收到Dispatcher⁃Operation消息,按优先级执行该消息中包含的任务.
步骤4:UI线程更新.
根据这一过程分析得到,在拆分任务时使用Dispatcher向系统消息队列发送任务消息,能够保证UI线程及时更新,运行过程不阻塞.
2.4使用Dispatcher对象的Invoke和Begin‐Invoke解决UI线程更新问题Dispatcher对象给开发者解决UI线程阻塞带来了可能性,Dispatcher提供了Invoke和Be⁃ginInvoke方法,使用这两个方法向Dispatcher⁃Operation的消息队列发送消息,一方面可以保证在UI线程中进行任务拆分并及时更新UI线程,另一方面也可以保证子线程完成耗时任务后发送消息回到UI线程,更新UI线程.这两个方法的具体描述如下:
(1)Invoke的方法签名及使用场景object Invoke(Delegate method,object[]args);
参数1method是一个委托类型,可以理解为是一个方法的地址,表示发送到Dispatcher⁃Operation消息队列中的一个任务方法;参数2args是这个方法调用时传入的参数值,这样就将一个要执行的任务传递给W
indows消息机制处理.
Invoke方法用于同步处理场景,当用户需要等待方法执行返回结果才能继续往下执行时采用Invoke方法,它可以保证消息传递过程中消息保持一定顺序被执行.但是如果该消息中含有较大执行任务,也就是该委托对应的方法中执行的程序耗时比较长时,会造成线程阻塞.
(2)BeginInvoke的方法签名及使用场景IAsyncResultBeginInvoke(Delegate method,object[]args);
2021年
第4
期
学报
参数的表达意思同Invoke 方法.参数1表示委托,参数2表示方法执行时传递的参数值.不同的是该方法用于异步处理场景,当用户不需要等待method 参数方法执行完毕就继续往下执行其他程序时,可以采用该方法.它虽然不能保证消息按顺序地执行完成,但是可以保证程序很好的性能,从而提供给用户较好的体验.
对于使用BeginInvoke 方法产生消息的乱序,可以通过在进行参数传递时提供时间戳来标记消息的先后顺序.然后通过定时器定时获取一组已经有序的消息并执行它们,为了保证性能问题,需要通过多轮测试最终选取定时器的间隔时间和一组消息的组大小.
3
多UI 线程并发阻塞问题的解决方案
3.1
多UI 线程并发阻塞问题的实验设计为了方便展示实验过程,建立一个数据
采集系统,设置两个终端持续不断地将数据发送到程序主界面,接收方软件主界面通过两个区域的UI 可视化控件来展示这些由终端1和终端2发出的数据.为了方便观察效果,这里将数据以点的形式绘制在界面上.具体场景结构关系如图2所示
.
图2多终端数据展示过程
终端1持续不断地将数据发送到区域1控件,区域1持续不断地将这些数据以点的形式绘制在区域1的位置;终端2持续不断地将数据发送到区域2控件,区域2持续不断地将这些数据以点的形式绘制在区域2的位置.问题场景中,终端与程序之间的通信是建立在网络环境中,终端需要知道程序所在服务器的IP 地址,程序需要知道终端的唯一标识,让服务器程序能清楚知道是谁发送过来的.实验过程中发现存在两个问题.
(1)两个终端的数据展示工作使用单UI 线程将无法完成,必须为每一区域内的数据展示过程建立独立的UI 线程去处理数据绘制工作,两个终端需要建立两个UI 线程.
(2)终端数据是通过循环不断向外发出的,如果将这些点直接绘制在UI 控件上,UI 线程会立即造成阻塞.实验时看到的画面将是所有消息发送完毕,这些点一次展示到UI 界面上,这与实时展示数据点是不相符的.3.2
解决创建多UI 线程的问题
WPF 开发框架提供了VisualTarget 类给程
序创建多UI 线程带来了可能性,创建多UI 线
程的好处就是为每一个UI 线程建立自己的消息循环队列,每个UI 控件可以在自己的消息循环队列中使用GetMessage ()获取消息,相互不干扰,根据前面问题场景的模拟,可以建立两个UI 线程,以下是使用VisualTarget 类创建多UI 线程的步骤.
步骤1:创建一个自定义类继承FrameworkEle⁃ment ,其目的是建立新UI 线程中控件的宿主,将新的UI 线程中的UI 控件加入到当前UI 控件的可视化树中.
步骤2:实例化刚刚创建的可视化宿主类,将WPF 框架提供的HostVisual 实例化后加
入其中,并将可视化宿主类实例加入到当前UI 控件可视化树中.
步骤3:建立子线程,在子线程中创建每
个区域绘制数据点的UI 控件,这里选择WPF
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论