我公司的主要产品是大型单片C ++应用程序,用于科学数据处理和可视化。它的代码库可以追溯到12年或13年,虽然我们已经将工作投入到升级和维护中(使用STL和Boost - 当我加入大多数容器时都是自定义的,例如 - 完全升级到Unicode和2010 VCL等)还有一个非常重要的问题:它是完全单线程的。鉴于它是一个数据处理和可视化程序,这已成为越来越多的障碍。
我是开发人员和项目经理,我们要解决这个问题的下一个版本,这对两个领域来说都是一项艰巨的任务。我正在寻求关于如何解决问题的具体,实用和建筑建议。
程序的数据流可能是这样的:
即,绘制消息处理程序将在处理完成时阻塞,如果数据尚未计算和缓存,则可能需要很长时间。有时这是几分钟。执行冗长处理操作的程序的其他部分也会出现类似的路径 - 程序在整个时间内都没有响应,有时是几个小时。
我正在寻求如何改变这一点的建议。实用的想法。也许是这样的事情:
自从我几年前的Uni时代以来,我没有做任何多线程编程,我认为我团队的其他成员也处于类似的位置。我所知道的是学术性的,而不是实际的,并且远远不足以让人有信心接近这一点。
最终目标是拥有一个完全响应的程序,其中所有计算和数据生成都在其他线程中完成,并且UI始终响应。我们可能无法在一个开发周期中实现:)
编辑:我想我应该添加一些关于该应用的详细信息:
编辑#2:感谢您的回复!
我还没有为这个问题找到答案 - 这不是因为答案的质量,这很好(而且比得上),但仅仅是因为我的范围,我希望得到更多的答案或讨论。谢谢那些已经回复过的人!
答案 0 :(得分:16)
你面前有一个很大的挑战。我面临着类似的挑战 - 15年前的单片单线程代码库,没有利用多核等等。我们花了很多精力去寻找一个可行且可行的设计和解决方案。 / p>
首先是坏消息。它将介于不切实际和不可能使您的单线程应用程序多线程之间。单线程应用程序依赖于它的单线程,这种方式既微妙又粗略。一个例子是计算部分是否需要来自GUI部分的输入。 GUI必须在主线程中运行。如果您尝试直接从计算引擎获取此数据,您可能会遇到需要重新设计修复的死锁和竞争条件。在设计阶段,甚至在开发阶段,这些依赖性中的许多都不会出现,但只有在将版本构建置于恶劣环境中之后才会出现。
更多坏消息。编程多线程应用程序非常困难。只是锁定东西并做你必须做的事情似乎相当简单,但事实并非如此。首先,如果您锁定所有内容,最终会序列化您的应用程序,首先否定多线程的所有好处,同时仍然增加所有复杂性。即使你超越了这一点,编写一个无缺陷的MP应用程序也很难,但编写一个高性能的MP应用程序要困难得多。你可以通过火灾在一种洗礼中学习这项工作。但是,如果您使用生产代码执行此操作,尤其是遗留生产代码,则会使您的商务风险处于危险之中。
现在好消息。您确实拥有不涉及重构整个应用程序的选项,并且会为您提供所需的大部分内容。一个选项特别容易实现(相对而言),并且比使您的应用程序完全MP更不容易出现缺陷。您可以实例化应用程序的多个副本。使其中一个可见,而其他所有其他都不可见。使用可见应用程序作为表示层,但不要在那里进行计算工作。相反,将消息(可能通过套接字)发送到应用程序的不可见副本,这些副本执行工作并将结果发送回表示层。
这看起来像是一个黑客。也许是。但是,如果不将系统的稳定性和性能置于极大的风险之中,它将为您提供所需的功能。此外还有隐藏的好处。一个是应用程序的隐形引擎副本可以访问自己的虚拟内存空间,从而更容易利用所有系统资源。它也可以很好地扩展。如果您在2核盒子上运行,则可以分离2个引擎副本。 32芯? 32份。你明白了。
答案 1 :(得分:15)
因此,您对算法的描述提示如何继续:
通常是非常复杂的数据流 - 将此视为流经复杂图表的数据,每个节点都执行操作
我会考虑使数据流图表实际上是完成工作的结构。图中的链接可以是线程安全的队列,每个节点的算法可以保持不变,除非包含在从队列中获取工作项并将结果存储在一个队列中的线程中。你可以更进一步,使用套接字和进程而不是队列和线程;如果在执行此操作方面有性能优势,这将让您分布在多台计算机上。
然后你的绘画和其他GUI方法需要分成两部分:一半用于排队工作,另一半用于绘制或使用结果,因为它们从管道中出来。
如果应用假定数据是全局的,这可能不实用。但如果它很好地包含在类中,正如您的描述所暗示的那样,那么这可能是使其并行化的最简单方法。
答案 2 :(得分:8)
答案 3 :(得分:7)
您要做的主要是将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结构,直到你达到对结果满意的程度。
答案 4 :(得分:6)
您可能只是将UI和工作任务分解为单独的线程。
在paint方法中,不是直接调用getData(),而是将请求放在线程安全的队列中。 getData()在另一个从队列中读取数据的线程中运行。完成getData线程后,它会通过线程同步来重新通知主线程重绘可视化区域及其结果数据。
虽然所有这一切都在发生,但你当然有一个进度条说明网格样条,以便用户知道正在发生的事情。
这样可以让您的UI保持活泼,而不会出现多线程工作例程的痛苦(这可能类似于完全重写)
答案 5 :(得分:3)
听起来你有几个不同的问题,并行可以解决,但以不同的方式。
通过利用多核CPU架构
提升性能你没有利用变得如此普遍的多核CPU架构。并行化允许您在多个核心之间划分工作。您可以使用“功能”编程风格通过标准C ++划分和征服技术编写该代码,您可以将工作传递到除法阶段的单独线程。谷歌的MapReduce模式就是这种技术的一个例子。英特尔拥有新的CILK库,可以为您提供C ++编译器支持。
通过异步文档视图提高GUI响应能力
通过将GUI操作与文档操作分离并将它们放在不同的线程上,可以提高应用程序的表面响应能力。标准的模型 - 视图 - 控制器或模型 - 视图 - 展示器设计模式是一个很好的起点。您需要通过让模型通知更新视图而不是让视图提供文档计算自身的线程来并行化它们。 View会在模型上调用一个方法,要求它计算数据的特定视图,模型会在信息被更改或新数据可用时通知演示者/控制器,这些信息将被传递给视图以进行更新。
机会缓存和预先计算 听起来您的应用程序具有固定的数据基础,但对数据有许多可能的计算密集型视图。如果您对在哪些情况下最常请求的视图进行了统计分析,则可以创建后台工作线程以预先计算可能请求的值。将这些操作放在低优先级线程上可能很有用,这样它们就不会干扰主应用程序处理。
显然,您需要使用互斥(或关键部分),事件和可能的信号量来实现这一点。您可能会发现Vista中的一些新同步对象很有用,例如超薄的读写器锁,条件变量或新的线程池API。有关如何使用这些基本技术,请参阅Joe Duffy's book on concurrency。
答案 6 :(得分:3)
还有一些人没有谈过,但这很有趣。
它被称为future
s。未来是结果的承诺......让我们看一个例子。
future<int> leftVal = computeLeftValue(treeNode); // [1]
int rightVal = computeRightValue(treeNode); // [2]
result = leftVal + rightVal; // [3]
这很简单:
您分离开始计算leftVal
的线程,例如从池中取出它以避免初始化问题。
在计算leftVal
时,您计算rightVal
。
你添加了两个,如果还没有计算leftVal
,可能会阻止,并等待计算结束。
这里的好处是它很简单:每次你有一个计算后跟另一个独立的计算,然后你加入结果,就可以使用这个模式。
请参阅future
上的Herb Sutter's article,它们将在即将发布的C++0x
中提供,但即使语法可能不像我给你的那么漂亮,现在已有库可用相信;)
答案 7 :(得分:3)
如果是我花的开发资金,我会从大局开始:
我希望完成什么,以及我将花多少钱来实现这一目标,以及我将如何进一步领先? (如果答案是这样的话,我的应用程序在四核PC上的运行效率会提高10%,而且我可以通过每台客户PC多花1000美元,并且今年在R&amp; D上减少100,000美元来实现相同的结果,那么,我会跳过整个努力。)
为什么我要做多线程而不是大规模并行分布?我真的认为线程比流程更好吗?多核系统也可以很好地运行分布式应用程序。基于消息传递过程的系统有一些优点,超出了线程的好处(和成本!)。我应该考虑基于流程的方法吗?我应该考虑完全作为服务运行的后台和前台GUI吗?由于我的产品是节点锁定和许可的,我认为服务很适合我(供应商)。此外,将内容分成两个进程(后台服务和前台)可能会强制进行重写和重新架构,我可能不会被迫这样做,如果我只是将线程添加到我的混合中。
这只是为了让你思考:如果你把它重写为服务(后台应用程序)和GUI会怎么样,因为这实际上比添加线程更容易,而且不会添加崩溃,死锁和竞争条件?
考虑一下这个想法,根据您的需要,线程可能是邪恶的。发展你的宗教,坚持下去。除非你有充分的理由去反过来。多年来,我虔诚地避免穿线。因为每个进程一个线程对我来说足够好。
我没有在列表中看到任何真正可靠的理由,为什么你需要线程,除了那些可以通过更昂贵的目标计算机硬件更便宜地解决的问题。如果您的应用程序“太慢”,添加线程可能甚至无法加快速度。
我使用线程进行后台串行通信,但我不认为线程只是用于计算量很大的应用程序,除非我的算法本质上是并行的,以使利益明确,并且缺点最小。
我想知道这个C ++ Builder应用程序的“设计”问题是否与我的Delphi“RAD Spaghetti”应用程序疾病相似。我发现批发重构/重写(我已经完成了这个主要应用程序超过一年),是我处理应用程序“意外复杂性”的最短时间。而这并没有抛出“可能的线程”的想法。我倾向于使用线程编写我的应用程序,仅用于串行通信和网络套接字处理。也许是奇怪的“工人 - 线程队列”。
如果你的应用中有一个地方你可以添加一个线程来测试水域,我会寻找主要的“工作队列”,我会创建一个实验版本控制分支,我会了解我的代码通过在实验分支中打破它来工作。添加该线程。并查看第一天调试的位置。然后我可能会放弃那个分支并回到我的躯干,直到我的颞叶疼痛消退。
沃伦
答案 8 :(得分:2)
这就是我要做的......
我首先要分析你的观点:
1)什么是慢的,热的路径是什么 2)哪些调用是可重入的或深度嵌套的
您可以使用1)来确定加速的机会在哪里以及从哪里开始寻找并行化。
你可以使用2)找出共享状态可能存在的位置,并深入了解事情的纠结程度。
我会使用一个好的系统分析器和一个好的采样分析器(比如windows perforamnce工具包或Visual Studio 2010 Beta2中分析器的并发视图 - 这些都是'免费'现在。)
然后我会弄清楚目标是什么,以及如何逐步将事物分离到更清晰的设计,更敏感(从UI线程移动工作)和更高性能(并行化计算密集部分)。我会首先关注最高优先级和最值得注意的项目。
如果你没有像VisualAssist这样的好的重构工具,那就投资一个 - 这是值得的。如果您不熟悉Michael Feathers或Kent Beck的重构书籍,请考虑借用它们。我会确保单元测试能很好地解决我的重构问题。
您无法移动到VS(我建议使用我在异步代理库和并行模式库中工作的产品,您也可以使用TBB或OpenMP)。
在boost中,我会仔细查看boost :: thread,asio库和信号库。
当我卡住时,我会请求帮助/指导/倾听。
-Rick
答案 9 :(得分:1)
您还可以查看Herb Sutter You have a mass of existing code and want to add concurrency. Where do you start?
中的这篇文章答案 10 :(得分:1)
嗯,我认为你期待很多基于你的评论。你不会通过多线程从几分钟到几毫秒。您最希望的是当前时间量除以核心数量。话虽这么说,你对C ++有点运气。我编写了高性能的多处理器科学应用程序,你想要寻找的是你能找到的最embarrassingly parallel循环。在我的科学代码中,最重的部分是计算100到1000个数据点。但是,所有数据点都可以独立于其他数据点进行计算。然后,您可以使用openmp拆分循环。这是最简单,最有效的方法。如果您的编译器不支持openmp,那么移植现有代码将非常困难。使用openmp(如果你很幸运),你可能只需添加几个#pragmas即可获得4-8倍的性能。这是一个例子StochFit
答案 11 :(得分:0)
我希望这可以帮助您轻松理解单个线程应用程序并将其转换为多线程。对不起,这是另一种编程语言,但从来没有解释过的原则是相同的。
http://www.freevbcode.com/ShowCode.Asp?ID=1287
希望这有帮助。
答案 12 :(得分:0)
首先要做的是将GUI与数据分开,其次是创建多线程类。
第1步 - 响应式GUI
我们可以假设您生成的图像包含在TImage的画布中。您可以在表单中放置一个简单的TTimer,您可以编写如下代码:
if (CurrenData.LastUpdate>CurrentUpdate)
{
Image1->Canvas->Draw(0,0,CurrenData.Bitmap);
CurrentUpdate=Now();
}
OK!我知道!有点脏,但它很快而且很简单。重点是:
现在您拥有快速响应的GUI。如果您的算法速度很慢,则刷新速度很慢,但您的用户永远不会认为您的程序已冻结。
第2步 - 多线程
我建议您实现类似以下的类:
SimpleThread.h
typedef void (__closure *TThreadFunction)(void* Data);
class TSimpleThread : public TThread
{
public:
TSimpleThread( TThreadFunction _Action,void* _Data = NULL, bool RunNow = true );
void AbortThread();
__property Terminated;
protected:
TThreadFunction ThreadFunction;
void* Data;
private:
virtual void __fastcall Execute() { ThreadFunction(Data); };
};
SimpleThread.c
TSimpleThread::TSimpleThread( TThreadFunction _Action,void* _Data, bool RunNow)
: TThread(true), // initialize suspended
ThreadFunction(_Action), Data(_Data)
{
FreeOnTerminate = false;
if (RunNow) Resume();
}
void TSimpleThread::AbortThread()
{
Suspend(); // Can't kill a running thread
Free(); // Kills thread
}
我们来解释一下。现在,在您的简单线程类中,您可以创建一个这样的对象:
TSimpleThread *ST;
ST=new TSimpleThread( RefreshFunction,NULL,true);
ST->Resume();
让我们更好地解释一下:现在,在你自己的单片类中,你已经创建了一个线程。更多:你在一个单独的线程中带来一个函数(即:RefreshFunction)。你的功能范围是相同的,类是相同的,执行是分开的。
答案 13 :(得分:0)
我的头号建议,虽然现在已经很晚了(对于复活旧线程感到遗憾,但这很有趣!)寻找齐次转换循环其中循环的每次迭代正在改变来自其他迭代的完全独立的数据。
而不是考虑如何将这个旧的代码库变成一个并行运行各种操作的异步代码库(这可能会导致各种麻烦比从不良锁定模式或指数级更糟的单线程性能更糟糕,竞争条件/死锁通过在后见之明中尝试做到你无法完全理解的代码,现在坚持整个应用程序设计的顺序思维模式,但识别或提取简单,同类的变换循环。不要从侵入性的广泛设计级多线程中走出来,然后尝试深入细节。从优先实现细节和特定热点的非侵入式多线程开始工作。
我认为齐次循环基本上是一种以非常简单的方式转换数据的方法,例如:
for each pixel in image:
make it brighter
这是非常简单的理由,你可以安全地并行化这个循环,没有任何问题使用OMP或TBB或其他什么,而不会纠缠在线程同步。只需浏览一下这段代码就可以完全理解它的副作用。
尝试找到适合此类简单齐次变换循环的尽可能多的热点,如果你有复杂的循环,用复杂的控制流更新许多不同类型的数据,触发复杂的副作用,那么试图重构这些均匀循环。通常,对3种不同类型的数据产生3个不同副作用的复杂循环可以变成3个简单的同质循环,每个循环对一种类型的数据触发一种副作用,具有更简单的控制流。做多个循环而不是一个循环可能看起来有点浪费,但循环变得更简单,同质性通常会导致更多缓存友好的顺序内存访问模式与零星的随机访问模式,然后您往往会找到更多的机会以直接的方式安全地并行化(以及矢量化)代码。
首先,你必须彻底了解你尝试并行化的任何代码的副作用(我的意思是彻底!!! ),所以寻找这些同类循环给出您可以根据副作用轻松推断代码库中的区域,以便您可以放心并安全地并行化这些热点。它还可以很容易地推断出该特定代码段中发生的状态变化,从而提高代码的可维护性。保存超级多线程应用程序的梦想,并行运行所有内容以供日后使用。目前,专注于识别/提取性能关键,均匀的循环,具有简单的控制流程和简单的副作用。这些是使用简单并行循环进行并行化的优先目标。
现在不可否认,我有点躲过了你的问题,但是如果你按照我的建议行事,他们中的大多数都不需要申请,至少在你有办法解决问题之前是这样的。 ;更多地考虑多线程设计,而不是简单地并行化实现细节。而且你可能甚至不需要走得太远而在性能方面拥有非常有竞争力的产品。如果你在一个循环中做了大量的工作,你可以投入硬件资源来使循环更快,而不是同时运行许多操作。如果你不得不求助于更多的异步方法,比如你的热点是否有更多的I / O限制,请寻求异步/等待方法,在这种方法中你可以触发异步任务,但在此期间做一些事情,然后等待异步任务去完成。即使这不是绝对必要的,但我们的想法是尽可能地分离代码库中的孤立区域,100%置信度(或至少99.9999999%)表示多线程代码是正确的。
你不想在竞争条件下赌博。没有什么比找到一些模糊的竞争条件更令人沮丧的了,这种情况在一个随机用户的机器上只出现一次满月,而你的整个QA团队都无法重现它,仅在3个月之后运行自己进入它自己,除非在那段时间你运行一个没有调试信息的发布版本,然后你在投入睡眠时知道你的代码库在任何特定时刻都会崩溃,但是没有人能够始终如一地重现。因此,使用多线程遗留代码库可以轻松实现,至少目前如此,并坚持使用多线程隔离但代码库的关键部分,其中副作用很容易理解。并测试它的废话 - 理想情况下应用TDD方法,你编写一个测试你要进行多线程的代码,以确保它在你完成后给出正确的输出...虽然竞争条件是类型的在单元和集成测试的雷达下容易飞行的东西,所以你再次绝对需要能够理解在尝试多线程之前在给定代码段中发生的所有副作用。最好的方法是使用最简单的控制流程使副作用尽可能容易理解,从而导致整个循环只产生一种副作用。
答案 14 :(得分:-3)
很难给你适当的指导。但...
根据我的最简单方法是将您的应用程序转换为ActiveX EXE,因为COM支持线程等,内置于其中,您的程序将自动成为多线程应用程序。当然,您必须对代码进行相当多的更改。但这是最短,最安全的方式。
我不确定,但可能RichClient Toolset lib可能会为您解决问题。在作者网站上写道:
它还提供免注册加载/实例化功能 对于ActiveX-Dlls和新的,易于使用的线程方法, 它适用于命名管道下的 引擎盖和工作因此也 交叉处理。
Please check it out.谁知道它可能是满足您要求的正确解决方案。
对于项目管理,我认为您可以通过插件将其与SVN集成,继续使用您选择的IDE中提供的内容。
我忘了提到我们已经完成了一个股票市场申请,该申请根据我们开发的算法自动交易(基于低点和高点买入和卖出)到用户投资组合中的脚本。
在开发此软件时,我们遇到的问题与您在此处说明的相同。为了解决这个问题,我们在ActiveX EXE中转换了应用程序,并将所有需要并行执行的部分转换为ActiveX DLL。我们还没有使用任何第三方库!
HTH