多线程 VS 多进程——选择最佳的开发途径

Blog
Author:
Dori ExtermanDori Exterman
Published On:
10月 6, 2020
Estimated reading time:
1 minute

在了解多线程与多进程差异之前,我们先讨论一下摩尔定律。想必大家都有所了解,摩尔定律表示,处理器的时钟频率每两年提升一倍。多年来确实是按照这样的规律发展的,但最近开始有了一些变化,时钟频率的提升速度开始减慢。这也预示着,摩尔定律也许即将迎来终点,想想真是很可怕。

计算机制造商应对摩尔定律极限的方法,是通过引入多处理器。多核心架构是目前以及今后市场解决摩尔定律极限的主要策略。如今的工作站通常配备 4、8、16、32、64 个内核,利用多个内核来提高应用程序性能,保证及时响应,尤其在处理一些常见的高吞吐量工作负载时,如渲染,仿真,机器学习等耗时的高强度计算。

当我们想要最大化利用多个处理器的硬件结构,编写软件时选择正确的架构至关重要。在大多数情况下,我们可以选择多线程或多进程处理,又或者两者兼用。这个选择将影响软件的性能、后期的维护、可扩展性、内存等各方面。任何一种选择都有利弊,但熟悉各个选择,可以帮助我们做出正确的决定。在这篇文章中,我将解释不同应用软件开发选择多内核策略时需要考虑的因素。换句话说,厘清在各种应用场景中多线程与多进程开发的优劣。话不多说,我们直入主题。

多线程开发:优势

多线程最突出的优点是借助变量、对象等,线程之间可以便捷地共享数据,与主线程进行通信也非常容易。

处理不可分割的大型数据集时,多线程开发也将非常有利。因为多进程处理需要复制数据集,导致占用大量时间和内存,同时使用共享内存数据会让软件开发更为复杂。

多线程另一个广为人知的特点是具有许多第三方库(开源和商业)的支持。如今许多数据库通过提供“线程安全”接口来支持多线程应用程序。组件、类、功能等预构建功能为多线程开发提供强大的支持,让开发人员的工作更为轻松。

但物无全美……

多线程开发:劣势

多线程代码的主要缺点是,如果其中一个线程崩溃,整个应用程序将连带崩溃。与之相对,多进程中一个进程失败不一定影响其他进程。

另一个缺点是多线程应用程序调试困难。这个问题不容小觑,因为错误难以避免。通常,排错程序不是处理多线程错误的最佳工具,建议使用日志来跟踪错误并找出导致错误的线程(或线程之间的通信)。简单来说,调试时间会相对较长。同时,多线程应用程序需要经验丰富的开发人员进行开发和调试。因此如果团队成员经验不足,新手较多,也需要格外注意。

如果同时执行太多线程,也会出现另一个问题。处理器可能会花费大量时间进行上下文切换,而因此影响实际运行。文件系统占用大量内存块,导致 I\O 瓶颈,最终减慢整个应用程序,造成主机堵塞。

另外,还有内存问题。所有线程都使用相同的进程内存,这对于线程之间的通信是非常有利的。但是,如果每个线程需要更多内存,可线程内存又受限于进程内存空间。对比之下,这个问题在多进程开发中不存在,因为每个进程都有分配的内存空间。

谈到多进程处理,我们首先了解一下它的优势。

多进程开发:优势

如前所述,一个进程崩溃,并不意味着整个应用程序的崩溃,这是多进程开发的一个显著优势(内核空间进程除外)。因此,如果某些进程失败,但编写的应用程序具有复原能力,开发就可以轻松恢复。

另一个优势是调试问题,我们现在了解到这也是多线程开发的劣势。在多进程中,调试要容易得多。比起调试同一进程内存空间中并行运行的多线程应用程序,处理一个小小的原子性进程就容易多了。

另外,锁的问题也会更少。但如果应用程序的进程实现类似于多线程架构(例如,使用相同的共享内存空间),这种情况下,多进程和多线程开发的复杂程度也大同小异了。尽管如此,倘若数据已进行备份(可在需要时合并),那么锁的问题也不再是个问题了。

最后,多进程是可扩展的。我们可以在其他地方执行进程,即利用远程机器或云分布处理,但线程总是限制在进程内存空间的上下文中。

不过,扩展并不能解决所有问题,多进程处理也有缺点。

多进程开发:劣势

 

通信是主要缺点。进程之间的通信比线程之间更为复杂。进程间的通信需要自定义开发才能共享数据并保持锁定和同步(必要时)。

此外,与线程安全数据库相比,支持多进程开发的数据库(我们称之为“进程安全”库)的数量相对较少。

了解了每种策略的优劣后,让我们深入探讨选择策略时需要考虑的各种因素,根据用户情况选择最佳方案,扬长避短。

多线程 VS 多进程:考虑因素

 

multithreading vs multiprocessing: List of Considerations

在决定最合适的架构之前,需要考虑以下因素:

  1. 数据同步-你是否需要同步大量数据,还要在进程之间维护状态?数据同步在多线程中是很容易的,因为所有线程共享相同的进程内存空间。如果并行元素(线程或进程)需要大量同步未封装的数据,则多线程更易于开发。

 

  1. 在多进程中,同步需要自定义开发,应用自身的逻辑机制设立进程可以通信或同步的元素,例如服务器应用程序、共享内存、直连(P2P 对等网络)、TCP 通信、数据库等。

涉及到大型数据集的情况下,首先要了解这个数据集是否为只读的(使用共享内存进行多进程访问只读数据并不是大问题),还是需要复杂的读写。如为后者,那么线程锁为数据访问提供了更强大的结构支撑。与实现多进程共享内存访问相比,将多线程将数据集加载到标准对象以及将类加载到便于分享的进程空间都更为简单。

 

  1. 易于开发-谁将参与开发?他/她是一位经验丰富的开发者还是新手?一方面,多线程包括许多预先设置的“线程安全”数据库。另外,在多进程环境中调试单个进程更容易。再者,代码理解在多进程中更简单。简而言之,多进程开发更简单,因为封装更易维护,经验不足的开发人员也可以轻松维护代码。

 

  1. 弹性扩展-项目的大小。你需要同时执行多少任务?未来该项目的容量是什么?执行每个任务需要多长时间?任务之间的依赖关系是什么?每个任务需要多少内存?多进程代码更具扩展能力,可以使用集群、网格,或者公共云,按需获得更多计算资源。进行多进程开发时,我们可以将代码从单个机器上分发到多个机器。甚至还有一些解决方案(如:IncrediBuild)可以让我们扩展到网络或公共云中,充分利用空闲机器和内核,而无需进行任何编码。扩展能力大幅提升,运行应用程序的机器也因此可以转换为具有数千个内核和内存容量的超级计算机。如此强大的计算能力,你的软件开发会需要吗?

到目前为止,我们已经在理论上讨论了多线程和多进程之间的区别。为了真正理解这种差异,我们接下来将在真实地应用场景如 Maven vs. Make 中进行对比。

Maven vs. Make

Maven vs. Make

Maven 和 Make 都是流行和常见的构建工具。Maven 通常用于 Java 编译,而Make 主要用于 C/C++ 编译。两者皆用于从小文件构建大型项目,且都有大量(数百甚至数千)编译任务,这些任务通常是原子性的,并且彼此独立。它们的通信很少,数据集也很小。

现在我们看一下在使用这些工具时如何选择并行处理架构。Maven 在单个构建上下文中是多线程的。几年前它是单线程的,但随着时间的推移它变成了多线程,这是一个积极的变化,让八个内核都能得到充分利用,而不仅仅局限在单一内核中。但是,多进程执行似乎对提高构建速度更有利。为什么?有几个原因:

首先,使用开源构建的开发人员通常需要编译开源代码。因此,项目将产生庞大的源代码,不论机器的性能多么强大,这个源代码都将拖累开发,减缓编译速度。这对编译时间、生产效率和上市时间都较为不利,特别是在转换到敏捷开发和持续集成时也有较大的影响。

另一个例子是 DevOps,DevOps 是需要包含更多任务的构建,如自动测试、代码分析,打包等,这使得编译速度更加缓慢。

最后,多进程结构允许进程分发,可以打破单个机器的限制,将编译的工作负载分发到多台机器同时执行,从而让构建更快且更具可扩展性。

与 Maven 相反,Make 构建系统架构是多进程的。进程分发只能应用于多进程开发,而不能应用于多线程;在多进程中,线程不能分布在主进程内存空间之外。每个进程都有自己的内存空间、环境变量等。事实上,大多数现代构建工具都是多进程而不是多线程的(例如 Visual Studio 的 MSBuild、CMake、Scons、Ninja、JAM、JOM、WAF 等等),原因是多进程的内存使用更为方便,开发和弹性扩展比较容易。

例如,在 Maven 或 Make 中使用八核机器执行 Qt 大约需要 16 分钟。但是,如果使用的是多进程架构,如 Make,则可以使用分布式计算工具(IncrediBuild),远程执行所有编译任务(有效利用网络或公共云中的其他机器),将构建时间缩短到仅1分钟 40 秒!想想 1 分钟 40 秒,对比之前的 16 分钟是一个多大的突破!

其他的应用场景

当然,我们也可以看看其他的应用场景。

一种情况是需要同时存储所有数据的应用程序(例如,科学应用程序,天气预报,遗传算法等)。在这些情况下,具有超级计算能力的多线程是明智的选择。我们肯定不希望在远程机器上复制大量数据集,产生大量网络流量并拖累整个进程速度。在遗传算法等场景中,单个线程对数据的更改必须立即通过其他线程查看;同样的情况在分布式多进程结构中,则要求频繁同步数据,这将对性能产生极为负面的影响。

通常使用多线程的另一个现实场景是实时应用程序,例如金融交易,防御系统和汽车设备。这些场景无法等待进程初始化,通常需要实时响应。

数据库应用程序(如 CRM,ERP,SAP)是多线程使用的另一个例子。大多数数据库应用程序查询都是只读,少数需要写入操作。其原因是希望用户轻松地将数据集共享为进程空间的一部分,让其他计算线程可以轻松读取。

总结多线程最佳的应用场景:

  • 简单的应用场景,不需要执行复杂的多进程执行;
  • 处理实时应用程序;
  • 需要较长的初始化时间(如数据集加载),较短的计算时间;
  • 通信复杂;
  • 应用程序仅包含少数计算密集型任务。

现在让我们来看看适合多进程处理的一些场景。

第一个应用场景为,当我们具有庞大的独立数据集,数据实体之间的依赖性较低,并且我们不需要同时存储内存中的所有数据。

例如,金融衍生品,每种股票的每日计算。每个股票是一个不同的数据集,可以有一个单独的进程进行计算。另一个例子是渲染,数据分解为较小的数据块,每个电影帧将由不同的进程进行计算。

或者需要对小型业务部门进行许多计算时。我们以 Sarine Technologies 为例。Sarine 销售HW和软件,这些软件在原始钻石和其他宝石上运行大量的模拟实验,以找到最佳的切割方式,并获得最大收益。为了找到大型钻石的最佳切割方式,需要执行数百万个独立的并行模拟实验。Sarine 借助多进程开发的框架,将大量的模拟进程分配给网络中或公有云连接的机器,计算能力和效率大幅提升。

流媒体服务也往往是多进程的。例如,Nvidia Shield 是机顶盒装置,借助远程的高性能计算机,用户可以舒服地在沙发上玩高质量的图形游戏。OnLive 服务也是类似,用户能够在任何机器上安装 OnLive 应用程序。Netflix 是另一个不需要多余解释的例子。当进行远程输出时,我们可以始终使用负载平衡来确保足够的资源,同时后端进程可根据动态需求为终端用户提供服务。

借助当今公共云提供的无限容量,如果你的软件将来可能需要扩展,强烈建议考虑架构具有扩展到多个主机的能力。云服务还提供基于规模和使用情况的定价模型,可为你的软件提供额外的收入来源。

总而言之,在进行架构考虑并选择多线程或多进程时,大家可以问自己以下问题:

  • 我的执行可以分解为许多独立的任务吗?
  • 可以将工作分发到多个主机上吗?
  • 我是否有需要使用的大型数据集?
  • 我的客户需要多长时间才能执行大型场景?我是否有大型场景?
  • 我是否有任何特殊的通信和同步要求?
  • 开发的复杂程度,我的软件是否需要复杂的锁?我可以避免吗?条件竞争、时间问题、共享错误等问题多久会出现一次?这些问题会使我的软件开发和维护变得复杂和昂贵吗?
  • 我想使用私有或公共云来扩展性能吗?

这些答案因人而异。本文只是简单引入一些相关问题进行讨论。在进行了25年多的管理和软件组织咨询工作后,我发现在开始编码之前进行上述考虑,能有效规避很多问题。随着产品的愈发成熟,变得更加复杂,产品开发对计算能力和可扩展性的需求也越来越大,这些提前的计划甚至会影响产品最终能否成功。

我希望上述的讨论,能为你的产品设计和开发带来更多灵感。

编码愉快!