Dav*_*vid 32 c++ architecture delphi multithreading c++builder
我公司的主要产品是大型单片C++应用程序,用于科学数据处理和可视化.它的代码库可以追溯到12年或13年,虽然我们已经将工作投入到升级和维护中(使用STL和Boost - 当我加入大多数容器时都是自定义的,例如 - 完全升级到Unicode和2010 VCL等)还有一个非常重要的问题:它是完全单线程的.鉴于它是一个数据处理和可视化程序,这越来越成为一个障碍.
我既是开发人员又是下一个版本的项目经理,我们希望解决这个问题,这对于这两个领域来说都是一项艰巨的任务.我正在寻求有关如何解决问题的具体,实用和建筑建议.
程序的数据流可能是这样的:
即,绘制消息处理程序将在处理完成时阻止,如果数据尚未计算和缓存,则可能需要很长时间.有时这是几分钟.执行冗长处理操作的程序的其他部分也会出现类似的路径 - 程序在整个时间(有时是几小时)内没有响应.
我正在寻求如何改变这一点的建议.实用的想法.也许这样的事情:
自从几年前我的Uni时代以来,我没有做任何多线程编程,我认为我团队的其他成员处于类似的位置.我所知道的是学术上的,而不是实际的,并且远远不足以让人有信心接近这一点.
最终目标是拥有一个完全响应的程序,其中所有计算和数据生成都在其他线程中完成,并且UI始终响应.我们可能无法在一个开发周期中到达那里:)
编辑:我想我应该添加一些关于该应用程序的更多细节:
编辑#2:感谢您的回复!
我还没有为这个问题找到答案 - 这不是因为答案的质量,这很好(而且比你还要好),但仅仅是因为我的范围,我希望得到更多的答案或讨论.谢谢那些已经回复的人!
Joh*_*ing 16
你面前有一个很大的挑战.我面临着类似的挑战 - 15年的单片单线程代码库,没有利用多核等等.我们花了很多精力去寻找一个可行且可行的设计和解决方案.
首先是坏消息.它将介于不切实际和不可能使您的单线程应用程序多线程之间.单线程应用程序依赖于它的单线程,这种方式既微妙又粗略.一个例子是计算部分是否需要来自GUI部分的输入.GUI必须在主线程中运行.如果您尝试直接从计算引擎获取此数据,您可能会遇到需要重新设计修复的死锁和竞争条件.在设计阶段,甚至在开发阶段,这些依赖性中的许多都不会出现,但只有在将版本构建置于恶劣环境中之后才会出现.
更多坏消息.编程多线程应用程序非常困难.只是锁定东西并做你必须做的事情似乎相当简单,但事实并非如此.首先,如果您锁定所有内容,最终会序列化您的应用程序,首先否定多线程的所有好处,同时仍然增加所有复杂性.即使你超越了这一点,编写一个无缺陷的MP应用程序也很难,但编写一个高性能的MP应用程序要困难得多.你可以通过火灾在一种洗礼中学习这项工作.但是,如果您使用生产代码(尤其是遗留生产代码)执行此操作,则会使您的业务面临风险.
现在好消息.您确实拥有不涉及重构整个应用程序的选项,并且会为您提供所需的大部分内容.一个选项特别容易实现(相对而言),并且比使您的应用程序完全MP更不容易出现缺陷.
您可以实例化应用程序的多个副本.使其中一个可见,而其他所有其他都不可见.使用可见应用程序作为表示层,但不要在那里进行计算工作.相反,将消息(可能通过套接字)发送到应用程序的不可见副本,这些副本执行工作并将结果发送回表示层.
这可能看起来像是一个黑客.也许是.但是,如果不将系统的稳定性和性能置于极大的风险之中,它将为您提供所需的功能.此外还有隐藏的好处.一个是应用程序的隐形引擎副本可以访问自己的虚拟内存空间,从而可以更轻松地利用系统的所有资源.它也可以很好地扩展.如果您在2核盒子上运行,则可以分离2个引擎副本.32芯?32份.你明白了.
And*_*gor 15
因此,您对算法的描述提示如何继续:
通常是非常复杂的数据流 - 将此视为流经复杂图形的数据,每个节点都执行操作
我会考虑使数据流图表实际上是完成工作的结构.图中的链接可以是线程安全的队列,每个节点的算法可以保持不变,除非包含在从队列中获取工作项并将结果存储在一个队列中的线程中.你可以更进一步,使用套接字和进程而不是队列和线程; 如果这样做有性能优势,这将让您分布在多台机器上.
然后你的绘画和其他GUI方法需要分成两部分:一半用于排队工作,另一半用于绘制或使用结果,因为它们从管道中出来.
如果应用假设数据是全局的,这可能不实用.但是如果它很好地包含在类中,正如您的描述所暗示的那样,那么这可能是使其并行化的最简单方法.
您要做的主要是将UI与数据集断开连接.我建议这样做的方法是在两者之间加一层.
您需要设计一个用于显示的数据数据结构.这很可能包含一些后端数据的副本,但"煮熟"以便于绘制.这里的关键想法是,这是快速和容易绘画.您甚至可能让此数据结构包含数据位的计算屏幕位置,以便快速绘制.
每当您收到WM_PAINT消息时,您应该获得此结构的最新完整版本并从中进行绘制.如果您正确执行此操作,您应该能够每秒处理多个WM_PAINT消息,因为绘制代码根本不会引用您的后端数据.它正在旋转煮熟的结构.这里的想法是,最好快速绘制陈旧数据而不是挂起UI.
与此同时...
你应该有2个这个煮熟的显示结构的完整副本.一个是WM_PAINT消息所看到的内容.(称之为cfd_A)另一个是你的CookDataForDisplay()函数.(称之为cfd_B).CookDataForDisplay()函数在单独的线程中运行,并在后台构建/更新cfd_B.此功能可以根据需要使用,因为它不以任何方式与显示器交互.一旦调用返回cfd_B将是结构的最新版本.
现在在应用程序窗口中交换cfd_A和cfd_B以及InvalidateRect.
一个简单的方法就是让你的熟化显示结构成为一个位图,这可能是一个很好的方法来让球滚动,但我肯定有点想到你可以做多少更好的工作,更复杂的结构.
所以,回过头来举例说明.
- 在paint方法中,它将调用GetData方法,通常在一次绘制操作中为数百位数据调用数百次
现在是2个线程,paint方法引用cfd_A并在UI线程上运行.同时cfd_B由后台线程使用GetData调用构建.
快速而肮脏的方法是
现在,您的新WM_PAINT方法只需在cfd_A中获取预渲染的位图并将其绘制到屏幕上.您的UI现在与后端GetData()函数断开连接.
现在真正的工作开始了,因为快速和肮脏的方式不能很好地处理窗口调整大小.你可以从那里逐步改进你的cfd_A和cfd_B结构,直到你达到对结果满意的程度.
您可能只是将UI和工作任务分解为单独的线程.
在paint方法中,它不是直接调用getData(),而是将请求放在一个线程安全的队列中.getData()在另一个从队列中读取数据的线程中运行.完成getData线程后,它会通过线程同步传递数据来通知主线程重绘可视化区域及其结果数据.
虽然所有这一切都在发生,但你当然有一个进度条,说明网格样条,以便用户知道正在发生的事情.
这将使您的UI保持活泼,而不会出现多线程工作例程的痛苦(这可能类似于完全重写)
归档时间: |
|
查看次数: |
3914 次 |
最近记录: |