从JavaScript到Python之并发(上)

本文通过分析对比探究JavaScript与Python的并发能力,分上下两篇,上篇探究CPU并发,下篇探究网络IO并发。

测试环境:

1
2
操作系统: win10 x64
CPU: Intel i5-8400 6核6线程

实现并发操作一般来说有3种方式:进程线程协程

用一个简单的耗时功能来进行测试:递归实现斐波那契数列第n项的计算。

对于CPU密集型的计算任务,要通过并发提升程序执行效率,缩短执行时间,就一定要利用CPU的多核。
下面我们就来看看 JavaScript 和 Python 如何使用这3种方式进行并发以及对CPU核数的利用情况。

进程

进程是系统进行资源分配和调度的基本单位。

JavaScript

浏览器端执行的JavaScript没有多进程的概念,所以只考虑Node.js上的多进程。

让不同的进程运行在不同的内核上才能最好地发挥并行的优势,为了达到这个目的,我们选取原生模块cluster

代码如下:

为了使结果更准确,打开资源监视器进行查看,CPU各个都已跑满。

执行结果:

1
2
3
4
5
6
消耗时间(s): 8.107
消耗时间(s): 8.118
消耗时间(s): 8.16
消耗时间(s): 8.175
消耗时间(s): 8.209
消耗时间(s): 8.253

所以总耗时在8.253秒。

为了进一步证实,执行单进程代码:

单进程执行结果:
消耗时间(s): 7.361

时间比多进程略少一些,考虑到进程的创建与销毁所耗的时间,在误差范围之内。

Python

Python操作进程的模块为multiprocessing,同样我们启动6个进程看看能不能把所有核跑满。

代码如下:

CPU监控也显示6个核都执行了计算任务:

执行结果

1
2
3
4
5
6
消耗时间(s): 8.683971881866455
消耗时间(s): 8.699971675872803
消耗时间(s): 8.71097183227539
消耗时间(s): 8.84197187423706
消耗时间(s): 8.863972425460815
消耗时间(s): 8.900972843170166

总执行时间约为8.9秒。

执行时间看上去只比JavaScript慢了那么一丢丢,但是需要注意的是JavaScript代码计算的是数列第45位,Python只计算第38位!

同样执行一下单进程进行计算,与多进程进行对比验证。

消耗时间(s): 8.092010259628296

执行时间也相当接近。

结论

JavaScript(Node.js)Python都提供了操作进程的原生模块,多进程执行计算任务时都能有效利用CPU多核提升效率。

线程

一个进程可以有一个或多个线程,线程相对进程而言更轻量,上下文切换成本更低,通常作为并发操作的首选。

JavaScript

前端工程师很少了解线程的概念:浏览器端直到HTML5标准提出才支持以web worker方式创建多线程,Node.js也是12以后的版本才支持使用worker_threads模块进行多线程计算。
总结起来就是早期的JavaScript执行环境没有线程相关模块与API,对开发者屏蔽了线程的概念。

web worker

浏览器端多线程执行执行JavaScript代码分为两部分,待执行的js文件和引入该js文件的页面。

代码如下:

在本地启动和访问服务器,得到监控图像也是多核执行:

执行结果:

1
2
3
4
5
6
消耗时间(s): 15.989
消耗时间(s): 16.109
消耗时间(s): 16.204
消耗时间(s): 16.874
消耗时间(s): 16.876
消耗时间(s): 16.971

总执行时间约为16.9秒。

为了使结果更有说服力,我们在浏览器控制台执行一下相关代码

时间略少于多线程执行。

相对于Node.js端执行多了1倍,看来浏览器端的线程性能并不高。

worker_threads

不能利用多核一直让Node.js饱受诟病,新版本算是对这个问题打了个补丁。

Node.js端代码如下:

CPU监控结果:

执行结果:

1
2
3
4
5
6
消耗时间(s): 8.229
消耗时间(s): 8.282
消耗时间(s): 8.299
消耗时间(s): 8.403
消耗时间(s): 8.417
消耗时间(s): 8.44

总时间8.44秒与单独执行相当。

结论

浏览器和Node.js都支持多线程,但明显浏览器执行性能不如Node.js,所以使用web worker的工程师需谨慎。

Python

Python的原生模块threading提供多线程操作。具体代码如下

监控图表:

看波形图应该是利用了各个核,但并没有满负荷运行。

执行结果也不尽如人意:

1
2
3
4
5
6
消耗时间(s): 38.476489543914795
消耗时间(s): 41.6956250667572
消耗时间(s): 45.613648414611816
消耗时间(s): 47.564653396606445
消耗时间(s): 47.68662452697754
消耗时间(s): 48.57262468338013

总时间为48.5s,是单进程执行时间的6倍。

结论

从输出结果时间来看,Python的线程是并发执行了,但并没有提升执行效率。

协程

协程和线程有些类似,区别在于线程是由操作系统调度的,协程是由用户代码管理的。
相对线程而言上下文切换成本更轻量。

Python

抱歉,JavaScript的原生模块目前还不支持。
Python3倒是积极引入了协程的概念,实际作用看测试结果吧~

代码:

CPU监控:

执行结果:

1
2
3
4
5
6
消耗时间(s): 8.070001602172852
消耗时间(s): 16.14399790763855
消耗时间(s): 24.214999198913574
消耗时间(s): 32.300398111343384
消耗时间(s): 40.38105368614197
消耗时间(s): 48.451064109802246

结论

协程在CPU使用率上和线程差不多,并没有提升。而且从输出结果来看并不是真正的并行执行。

总结

  • 首先不考虑并发的情况下,JavaScript的执行效率要优于Python
  • 两者都能利用多进程有效地利用CPU多核提升效率。
  • 线程JavaScript更胜一筹,Python在受限于全局解释器锁的情况下,多线程可以实现并发但不能提升效率。
  • 协程JavaScript完败,但Python的协程也不擅长处理CPU密集型操作。

原文链接:https://tech.gtxlab.com/js2py-async.html
作者信息:朱德龙,人和未来高级前端工程师。