将大型单片单线程应用程序转换为多线程架构的建议?

Dav*_*vid 32 c++ architecture delphi multithreading c++builder

我公司的主要产品是大型单片C++应用程序,用于科学数据处理和可视化.它的代码库可以追溯到12年或13年,虽然我们已经将工作投入到升级和维护中(使用STL和Boost - 当我加入大多数容器时都是自定义的,例如 - 完全升级到Unicode和2010 VCL等)还有一个非常重要的问题:它是完全单线程的.鉴于它是一个数据处理和可视化程序,这越来越成为一个障碍.

我既是开发人员下一个版本的项目经理,我们希望解决这个问题,这对于这两个领域来说都是一项艰巨的任务.我正在寻求有关如何解决问题的具体,实用和建筑建议.

程序的数据流可能是这样的:

  • 窗口需要绘制数据
  • 在paint方法中,它将调用GetData方法,通常在一次绘制操作中为数百位数据调用数百次
  • 这将从文件或其他任何需要的计算或读取(通常非常复杂的数据流 - 将此视为流经复杂图形的数据,每个节点执行操作)

即,绘制消息处理程序将在处理完成时阻止,如果数据尚未计算和缓存,则可能需要很长时间.有时这是几分钟.执行冗长处理操作的程序的其他部分也会出现类似的路径 - 程序在整个时间(有时是几小时)内没有响应.

我正在寻求如何改变这一点的建议.实用的想法.也许这样的事情:

  • 设计用于异步请求数据的模式?
  • 存储大量对象,以便线程可以安全地读写?
  • 在某些东西试图读取时处理数据集的失效?
  • 有这种问题的模式和技术吗?
  • 我应该问什么,我没有想到?

自从几年前我的Uni时代以来,我没有做任何多线程编程,我认为我团队的其他成员处于类似的位置.我所知道的是学术上的,而不是实际的,并且远远不足以让人有信心接近这一点.

最终目标是拥有一个完全响应的程序,其中所有计算和数据生成都在其他线程中完成,并且UI始终响应.我们可能无法在一个开发周期中到达那里:)


编辑:我想我应该添加一些关于该应用程序的更多细节:

  • 它是适用于Windows的32位桌面应用程序.每个副本都是许可的.我们计划将其作为桌面本地运行的应用程序
  • 我们使用Embarcadero(以前的Borland)C++ Builder 2010进行开发.这会影响我们可以使用的并行库,因为大多数似乎(?)只为GCC或MSVC编写.幸运的是,他们正在积极开发它,它的C++标准支持比以前好多了.编译器支持这些Boost组件.
  • 它的架构并不像应该的那样干净,而且组件通常耦合得太紧.这是另一个问题:)

编辑#2:感谢您的回复!

  • 令我感到惊讶的是,很多人推荐了一个多进程架构(这是目前最受欢迎的答案),而不是多线程.我的印象是,这是一个非常Unix的程序结构,我对它的设计或工作方式一无所知.Windows上有关于它的可用资源吗?它在Windows上真的很常见吗?
  • 就一些多线程建议的具体方法而言,是否存在异步请求和消费数据,线程软件或异步MVP系统的设计模式,或者如何设计面向任务的系统,或者文章和书籍以及发布后的解构说明工作的东西和不起作用的东西?当然,我们可以自己开发所有这些架构,但是与其他人之前的工作一起工作并知道要避免哪些错误和陷阱是很好的.
  • 任何答案中没有涉及的一个方面是项目管理.我的印象是估计这需要多长时间,并在做一些不确定的事情时保持对项目的良好控制,因为这可能很难.这就是为什么我会在配方或实际编码建议之后,尽可能地引导和限制编码方向.

我还没有为这个问题找到答案 - 这不是因为答案的质量,这很好(而且比你还要好),但仅仅是因为我的范围,我希望得到更多的答案或讨论.谢谢那些已经回复的人!

Joh*_*ing 16

你面前有一个很大的挑战.我面临着类似的挑战 - 15年的单片单线程代码库,没有利用多核等等.我们花了很多精力去寻找一个可行且可行的设计和解决方案.

首先是坏消息.它将介于不切实际和不可能使您的单线程应用程序多线程之间.单线程应用程序依赖于它的单线程,这种方式既微妙又粗略.一个例子是计算部分是否需要来自GUI部分的输入.GUI必须在主线程中运行.如果您尝试直接从计算引擎获取此数据,您可能会遇到需要重新设计修复的死锁和竞争条件.在设计阶段,甚至在开发阶段,这些依赖性中的许多都不会出现,但只有在将版本构建置于恶劣环境中之​​后才会出现.

更多坏消息.编程多线程应用程序非常困难.只是锁定东西并做你必须做的事情似乎相当简单,但事实并非如此.首先,如果您锁定所有内容,最终会序列化您的应用程序,首先否定多线程的所有好处,同时仍然增加所有复杂性.即使你超越了这一点,编写一个无缺陷的MP应用程序也很难,但编写一个高性能的MP应用程序要困难得多.你可以通过火灾在一种洗礼中学习这项工作.但是,如果您使用生产代码(尤其是遗留生产代码)执行此操作,则会使您的业务面临风险.

现在好消息.您确实拥有不涉及重构整个应用程序的选项,并且会为您提供所需的大部分内容.一个选项特别容易实现(相对而言),并且比使您的应用程序完全MP更不容易出现缺陷.

您可以实例化应用程序的多个副本.使其中一个可见,而其他所有其他都不可见.使用可见应用程序作为表示层,但不要在那里进行计算工作.相反,将消息(可能通过套接字)发送到应用程序的不可见副本,这些副本执行工作并将结果发送回表示层.

这可能看起来像是一个黑客.也许是.但是,如果不将系统的稳定性和性能置于极大的风险之中,它将为您提供所需的功能.此外还有隐藏的好处.一个是应用程序的隐形引擎副本可以访问自己的虚拟内存空间,从而可以更轻松地利用系统的所有资源.它也可以很好地扩展.如果您在2核盒子上运行,则可以分离2个引擎副本.32芯?32份.你明白了.

  • 如果您的应用程序使用任何稀缺的系统资源,文件,数据库,声卡,EKG机器等,多个实例将争夺它.我曾经在一个可能是多实例_except_的应用程序上工作,以便撤消历史记录最终会在实例之间共享.几乎所有未设计为多实例的应用程序都存在这样的问题. (4认同)
  • +1消息交换也可以使用一些消息队列来完成,例如MSMQ (2认同)

And*_*gor 15

因此,您对算法的描述提示如何继续:

通常是非常复杂的数据流 - 将此视为流经复杂图形的数据,每个节点都执行操作

我会考虑使数据流图表实际上是完成工作的结构.图中的链接可以是线程安全的队列,每个节点的算法可以保持不变,除非包含在从队列中获取工作项并将结果存储在一个队列中的线程中.你可以更进一步,使用套接字和进程而不是队列和线程; 如果这样做有性能优势,这将让您分布在多台机器上.

然后你的绘画和其他GUI方法需要分成两部分:一半用于排队工作,另一半用于绘制或使用结果,因为它们从管道中出来.

如果应用假设数据是全局的,这可能不实用.但是如果它很好地包含在类中,正如您的描述所暗示的那样,那么这可能是使其并行化的最简单方法.

  • 我衷心赞同这种做法.它不仅提供了良好的结构,而且经过精心设计,最大限度地减少了数据争用和锁定需求(从而最大限度地发挥现代多核CPU和超线程的优势).关键技巧是只允许工作线程访问请求对象中的数据,并将结果放在那里.由于在给定时间只有一个线程拥有该请求,因此没有数据争用(除了队列上的锁定以使插入/移除原子). (2认同)

dth*_*rpe 8

  1. 不要尝试多线程旧应用程序中的所有内容.为了说它是多线程的多线程是浪费时间和金钱.你正在构建一个可以做某事的应用程序,而不是你自己的纪念碑.
  2. 剖析并研究您的执行流程,以确定应用程序花费大部分时间的位置.分析器是一个很好的工具,但是只需单步执行调试器中的代码即可.你可以在随机游走中找到最有趣的东西.
  3. 将UI与长时间运行的计算分离.使用跨线程通信技术从计算线程向UI发送更新.
  4. 作为#3的副作用:仔细考虑重入:现在计算在后台运行,用户可以在UI中浏览,UI中的哪些内容应该被禁用以防止与后台操作冲突?允许用户在对该数据运行计算时删除数据集可能是个坏主意.(缓解:计算会生成数据的本地快照)用户是否同时处理多个计算操作?如果处理得当,这可能是一项新功能,有助于合理化应用程序返工工作.如果被忽视,那将是一场灾难.
  5. 确定要被推入后台线程的候选特定操作.理想的候选者通常是单个函数或类,它可以完成大量工作(需要"大量时间"才能完成 - 超过几秒钟),具有良好定义的输入和输出,不使用全局资源,并且不要直接触摸UI.根据改造到这个理想所需的工作量来评估和优先考虑候选人.
  6. 在项目管理方面,一步一步.如果您有多个操作是可以移动到后台线程的强大候选者,并且它们彼此之间没有交互,那么这些操作可能由多个开发人员并行实现.但是,让每个人都先参与一次转换是一个很好的练习,这样每个人都能理解要查找的内容,并建立用户界面交互的模式等.举行扩展的白板会议,讨论提取一个转换的设计和过程.函数进入后台线程.去实施(一起或分发给个人),然后重新召集,将它们放在一起,讨论发现和痛点.
  7. 多线程是一个令人头痛的问题,需要比直接编码更仔细的思考,但将应用程序拆分为多个进程会产生更多麻烦,IMO.线程支持和可用的原语在Windows中很好,可能比其他一些平台更好.使用它们.
  8. 一般来说,不要做任何需要的事情.通过抛出更多模式和标准库,很容易严重过度实现并使问题复杂化.
  9. 如果你的团队中没有人以前做过多线程工作,那么预算时间可以让专家或资金聘请一名顾问.


Joh*_*ler 7

您要做的主要是将UI与数据集断开连接.我建议这样做的方法是在两者之间加一层.

您需要设计一个用于显示的数据数据结构.这很可能包含一些后端数据的副本,但"煮熟"以便于绘制.这里的关键想法是,这是快速和容易绘画.您甚至可能让此数据结构包含数据位的计算屏幕位置,以便快速绘制.

每当您收到WM_PAINT消息时,您应该获得此结构的最新完整版本并从中进行绘制.如果您正确执行此操作,您应该能够每秒处理多个WM_PAINT消息,因为绘制代码根本不会引用您的后端数据.它正在旋转煮熟的结构.这里的想法是,最好快速绘制陈旧数据而不是挂起UI.

与此同时...

你应该有2个这个煮熟的显示结构的完整副本.一个是WM_PAINT消息所看到的内容.(称之为cfd_A)另一个是你的CookDataForDisplay()函数.(称之为cfd_B).CookDataForDisplay()函数在单独的线程中运行,并在后台构建/更新cfd_B.此功能可以根据需要使用,因为它不以任何方式与显示器交互.一旦调用返回cfd_B将是结构的最新版本.

现在在应用程序窗口中交换cfd_Acfd_B以及InvalidateRect.

一个简单的方法就是让你的熟化显示结构成为一个位图,这可能是一个很好的方法来让球滚动,但我肯定有点想到你可以做多少更好的工作,更复杂的结构.

所以,回过头来举例说明.

  • 在paint方法中,它将调用GetData方法,通常在一次绘制操作中为数百位数据调用数百次

现在是2个线程,paint方法引用cfd_A并在UI线程上运行.同时cfd_B由后台线程使用GetData调用构建.

快速而肮脏的方法是

  1. 获取当前的WM_PAINT代码,将其粘贴到名为PaintIntoBitmap()的函数中.
  2. 创建一个位图和一个Memory DC,这是cfd_B.
  3. 创建一个线程并将其传递给cfd_B并让它调用PaintIntoBitmap()
  4. 该线程完成后,交换cfd_B和cfd_A

现在,您的新WM_PAINT方法只需在cfd_A中获取预渲染的位图并将其绘制到屏幕上.您的UI现在与后端GetData()函数断开连接.

现在真正的工作开始了,因为快速和肮脏的方式不能很好地处理窗口调整大小.你可以从那里逐步改进你的cfd_A和cfd_B结构,直到你达到对结果满意的程度.


Byr*_*ock 6

您可能只是将UI和工作任务分解为单独的线程.

在paint方法中,它不是直接调用getData(),而是将请求放在一个线程安全的队列中.getData()在另一个从队列中读取数据的线程中运行.完成getData线程后,它会通过线程同步传递数据来通知主线程重绘可视化区域及其结果数据.

虽然所有这一切都在发生,但你当然有一个进度条,说明网格样条,以便用户知道正在发生的事情.

这将使您的UI保持活泼,而不会出现多线程工作例程的痛苦(这可能类似于完全重写)

  • 换句话说,Paint消息永远不应该触发数据库查询_at all_.用户输入应该是触发查询的内容,而不是绘制. (7认同)