现代编程语言中的并发:简介
最初发表于deepu.tech。
这是一个由多个部分组成的系列文章,我将探讨现代编程语言中的并发性,并根据《Rust》一书中的示例,构建并测试一个并发 Web 服务器,并使用 Rust、Go、JavaScript(NodeJS)、TypeScript(Deno)、Kotlin 和 Java 等热门语言进行基准测试,以比较这些语言/平台之间的并发性及其性能。本系列的章节如下,我将尽量每周发布。
- 介绍
- Rust 中的并发 Web 服务器
- Golang 中的并发 Web 服务器
- 使用 NodeJS 的 JavaScript 并发 Web 服务器
- 使用 Deno 实现 TypeScript 的并发 Web 服务器
- 使用 JVM 的 Java 并发 Web 服务器
- 基准测试的比较和结论
什么是并发
并发是编程中最复杂的方面之一,并且根据您选择的语言,其复杂性可以是从“看起来很令人困惑”到“这是什么黑魔法”。
并发是指多个任务可以在重叠的时间段内以任意特定顺序执行,且不会影响最终结果的能力。并发是一个非常宽泛的术语,可以通过以下任何一种机制实现。
为了简化这一点,我们可以说并发是问题所在,而多线程、并行和异步处理是该问题的解决方案。
并行性
并行!=并发
并行是指多个任务或单个任务的多个部分实际上同时运行,例如在多核处理器上。真正的并行在单核处理器上无法实现。在多核处理器中运行的应用程序也可以通过并行实现并发。多线程和并行可以协同工作,在多核环境中执行多线程时,操作系统可以根据资源使用情况提供并行。
多线程
多线程是指程序即使在单核机器上运行时,也似乎同时执行多项操作。在单核处理器中,这通过上下文切换或交错(任何给定时刻只有一个线程获得 CPU 时间)来实现。在多核处理器上,多线程的工作方式类似于并行,由操作系统自行决定,因此效率更高。大多数现代多核处理器每个核心提供两个线程,从而同时支持多线程和并行。这也是 Java 等多线程语言中实现并发的常见方式。
如果您有兴趣了解有关如何在线程中使用资源的更多信息,请查看此博客系列
异步处理
异步处理意味着您的程序执行非阻塞操作,并且可以提供一定程度的并发性,尽管在某些情况下效率不如并行或多线程。这主要是在 JavaScript 等单线程语言中实现并发的方式。在多核、多线程环境中进行异步编程是实现并行性的一种方法。在多线程应用程序中,异步进程可以在单独的线程中启动。
虽然这看起来与并发和并行类似,但主要区别在于并发和并行是执行任务的方式,而同步和异步是编程模型。
何时需要并发
并发有其优缺点,因此最好在有用例的情况下实施。并发的优点是处理时间更快、性能和吞吐量更高,以及负载处理能力更强。现在,了解何时使用并发非常重要。以下是一些常见的用例,可以作为参考:
- Web 服务器和 Web 服务:这些服务应该能够同时处理多个请求,是并发和异步处理的主要候选对象。每种语言中流行的 Web 框架都会提供并发实现。
- 大型计算问题:CPU 密集型算法、数据处理、数据科学、聚合和 Map-reduce 等工作负载可以从并发中受益
- I/O 密集型问题:当涉及大量 I/O(磁盘、数据库、网络、终端等)时,根据具体用例,并发可以帮助加快速度。例如,写入文件可以转移到线程或异步进程,而主程序可以读取下一个文件或进行其他计算。
- 分布式用例:消息队列、发布-订阅类型的用例、运行测试最适合并发和异步处理
何时应避免并发
并发的缺点是增加了代码的复杂性,并且占用了更多的计算资源。因此,了解何时不使用它也很重要。以下是一些应该避免使用并发的情况:
- 加速小型计算问题:将小型函数拆分为并发函数所引入的复杂性不值得提高性能(如果有的话),在大多数情况下,对于小型计算而言,性能的提高可以忽略不计,并且初始化同步资源的成本在许多情况下最终会变得更加昂贵。
- 当你不理解并发和其他相关原理时:这没什么可羞耻的。并发是编程中最复杂的方面之一,根据你选择的语言,其复杂性可能从“看起来很混乱”到“这是什么黑魔法”不等。因此,在不理解并发原理的情况下尝试使用并发来拆分问题,弊大于利。你可能会遇到的一些常见问题包括竞争条件、内存泄漏、数据损坏、锁等等。
基准测试与比较
我们将尽可能使用语言的开箱即用功能来构建一个非常简单的 Web 服务器。该 Web 服务器将支持并发请求。如果该语言同时支持异步和多线程并发,我们将尝试两者或两者的组合,并选择性能最佳的方案。因此,应用程序的复杂性取决于语言特性和语言复杂性。我们将尝试使用该语言提供的所有功能,以使并发性能尽可能好,而不会使事情过于复杂。该 Web 服务器只服务一个端点,并且每 10 个请求就会增加 2 秒的休眠时间。在我看来,这将模拟更真实的负载。
如果需要,并且语言支持,我们将使用 Promise、线程池和 Worker。我们不会在应用程序中使用任何不必要的 I/O。
免责声明:我并非声称这是一种精准的科学方法,或是并发性能的最佳基准测试。我确信不同的用例会得出不同的结果,而且现实世界的 Web 服务器会更加复杂,需要并发进程之间的通信来影响性能。我只是想针对一个简单的用例提供一些简单的基础比较。此外,我对某些语言的了解程度可能高于其他语言,因此我可能会遗漏一些优化之处。所以请不要批评我。如果您认为某种语言的代码可以开箱即用地改进以提高并发性能,请告诉我。如果您认为这个基准测试毫无用处,那么请推荐一个更好的基准测试 :)
基准测试条件
这些是我将用于基准测试的一些条件
- 使用可用的最新版本的语言/运行时。
- 仅当这是语言中标准推荐的方式时,我们才会使用外部依赖项。
- 我们不会考虑使用任何配置调整来提高并发性能
- 我们将使用 ApacheBench 进行基准测试,并发数为 100 个请求,总请求数为 10,000 个。基准测试将针对每种语言进行 5 次,分别在预热和不预热两种情况下进行,最终取最佳结果作为基准测试。
- 所有基准测试均在同一台机器上运行,该机器运行 Fedora 32,配备四核 Xeon 处理器、8 线程和 32GB 内存。
比较参数
我还将比较与并发相关的以下方面
- 基于基准测试结果的性能
- 社区对复杂用例的感知性能
- 易于使用
- 简单性,特别是对于复杂的用例
- 并发的外部库和生态系统
- 限制
因此请继续关注剩余的博客文章。
参考
如果您喜欢这篇文章,请点赞或留言。
封面图片来源:Matt Bero在Unsplash上拍摄的照片
文章来源:https://dev.to/deepu105/concurrency-in-modern-programming-languages-introduction-ckk