本文概述
我们生活在一个勇敢的新世界中。一个充满JavaScript的世界。近年来, JavaScript主导了整个网络, 席卷了整个行业。在引入Node.js之后, JavaScript社区就能够利用该语言的简单性和动态性, 将其作为唯一的语言来处理所有事情, 包括处理服务器端, 客户端, 甚至大胆地宣称自己可以从事机器学习。但是在过去的几年中, JavaScript作为一种语言已经发生了巨大的变化。引入了从未有过的新概念, 例如箭头功能和Promise。
啊, 保证。当我刚开始学习Node.js时, Promise和回调的整个概念对我来说并没有多大意义。我习惯了执行代码的过程方式, 但是随着时间的推移, 我了解了为什么它很重要。
这带给我们一个问题, 为什么无论如何都要引入回调和promise?为什么我们不能只用JavaScript编写顺序执行的代码?
好吧, 从技术上讲你可以。但是你应该吗?
在本文中, 我将简要介绍一下JavaScript及其运行时, 更重要的是, 测试一下JavaScript界普遍认为同步代码的性能不及标准的, 并且在某种意义上说只是普通的邪恶, 并且永远不要使用。这是真的神话吗?
在开始之前, 本文假定你已经熟悉JavaScript中的Promise, 但是, 如果你不熟悉或需要复习, 请参阅JavaScript Promises:带有示例的教程
N.B.本文已在Node.js环境(而非纯JavaScript环境)上经过测试。运行Node.js版本10.14.2。所有基准测试和语法都将严重依赖于Node.js。测试是在MacBook Pro 2018上运行的, 该处理器采用了Intel i5第八代四核处理器, 基本时钟速度为2.3 GHz。
事件循环
编写JavaScript的问题是语言本身是单线程的。这意味着你不能一次执行一个以上的过程, 而与其他语言(例如Go或Ruby)不同, 它们可以在内核线程或进程线程上生成线程并同时执行多个过程。 。
为了执行代码, JavaScript依赖于一个称为事件循环的过程, 该过程由多个阶段组成。 JavaScript过程经历了每个阶段, 最后, 又重新开始。你可以在此处阅读有关node.js官方指南的详细信息。
但是JavaScript可以解决阻塞问题。 I / O回调。
要求我们创建线程的大多数实际用例都是这样的事实, 即我们正在请求某种语言不负责的操作, 例如, 请求从数据库中获取某些数据。在多线程语言中, 创建请求的线程只是挂起或等待数据库的响应。这只是浪费资源。这也给开发人员增加了在线程池中选择正确数量的线程的负担。这是为了防止内存泄漏以及在应用程序需求量很大时分配大量资源。
JavaScript在处理I / O操作方面胜于其他任何方面。 JavaScript使你可以调用I / O操作, 例如从数据库请求数据, 将文件读入内存, 将文件写至磁盘, 执行Shell命令等。操作完成后, 你将执行回调。或者, 在有诺言的情况下, 你可以用结果来解决诺言或以错误拒绝它。
JavaScript社区始终建议我们在进行I / O操作时千万不要使用同步代码。众所周知的原因是我们不想阻止我们的代码运行其他任务。由于它是单线程的, 因此如果我们有一段代码可以同步读取文件, 那么该代码将阻塞整个过程, 直到读取完成。相反, 如果我们依赖异步代码, 则可以执行多个I / O操作, 并在完成每个操作时分别处理每个操作的响应。没有任何阻碍。
但是, 可以肯定的是, 在我们根本不关心处理大量流程的环境中, 使用同步和异步代码根本没有什么不同, 对吧?
基准测试
我们将要进行的测试旨在为我们提供基准, 以测试同步和异步代码的运行速度以及性能是否存在差异。
我决定选择读取文件作为I / O操作进行测试。
首先, 我编写了一个函数, 该函数将编写一个随机文件, 该文件填充有Node.js Crypto模块生成的随机字节。
const fs = require('fs');
const crypto = require('crypto');
fs.writeFileSync( "./test.txt", crypto.randomBytes(2048).toString('base64') )
该文件将作为下一步读取文件的常量。这是代码
const fs = require('fs');
process.on('unhandledRejection', (err)=>{
console.error(err);
})
function synchronous() {
console.time("sync");
fs.readFileSync("./test.txt")
console.timeEnd("sync")
}
async function asynchronous() {
console.time("async");
let p0 = fs.promises.readFile("./test.txt");
await Promise.all([p0])
console.timeEnd("async")
}
synchronous()
asynchronous()
运行前面的代码将产生以下结果:
运行# | 同步 | 异步 | 异步/同步比率 |
---|---|---|---|
1 | 0.278毫秒 | 3, 829毫秒 | 13.773 |
2 | 0.335毫秒 | 3.801毫秒 | 11.346 |
3 | 0.403毫秒 | 4.498毫秒 | 11.161 |
这是出乎意料的。我最初的期望是他们应该花相同的时间。好吧, 我们如何添加另一个文件并读取2个文件而不是1个文件呢?
我复制了生成的test.txt文件, 并将其命名为test2.txt。这是更新的代码:
function synchronous() {
console.time("sync");
fs.readFileSync("./test.txt")
fs.readFileSync("./test2.txt")
console.timeEnd("sync")
}
async function asynchronous() {
console.time("async");
let p0 = fs.promises.readFile("./test.txt");
let p1 = fs.promises.readFile("./test2.txt");
await Promise.all([p0, p1])
console.timeEnd("async")
}
我只是为它们每个添加了另一个阅读, 并且在诺言中, 我正在等待应该并行运行的阅读诺言。结果是:
运行# | 同步 | 异步 | 异步/同步比率 |
---|---|---|---|
1 | 1.659毫秒 | 6.895毫秒 | 4.156 |
2 | 0.323毫秒 | 4.048毫秒 | 12.533 |
3 | 0.324毫秒 | 4.017毫秒 | 12.398 |
4 | 0.333毫秒 | 4.271毫秒 | 12.826 |
第一个具有与随后的三个运行完全不同的值。我的猜测是, 它与JavaScript JIT编译器有关, 后者可在每次运行时优化代码。
因此, 异步功能看起来不太好。也许, 如果我们使事情变得更加动态, 或者给应用程序施加更多压力, 我们可能会得出不同的结果。
因此, 我的下一个测试涉及写入100个不同的文件, 然后全部读取。
首先, 我修改了代码以在执行测试之前写入100个文件。每次运行时文件都不同, 尽管它们的大小几乎保持相同, 因此我们在每次运行前都要清除旧文件。
这是更新的代码:
let filePaths = [];
function writeFile() {
let filePath = `./files/${crypto.randomBytes(6).toString('hex')}.txt`
fs.writeFileSync( filePath, crypto.randomBytes(2048).toString('base64') )
filePaths.push(filePath);
}
function synchronous() {
console.time("sync");
/* fs.readFileSync("./test.txt")
fs.readFileSync("./test2.txt") */
filePaths.forEach((filePath)=>{
fs.readFileSync(filePath)
})
console.timeEnd("sync")
}
async function asynchronous() {
console.time("async");
/* let p0 = fs.promises.readFile("./test.txt");
let p1 = fs.promises.readFile("./test2.txt"); */
// await Promise.all([p0, p1])
let promiseArray = [];
filePaths.forEach((filePath)=>{
promiseArray.push(fs.promises.readFile(filePath))
})
await Promise.all(promiseArray)
console.timeEnd("async")
}
并进行清理和执行:
let oldFiles = fs.readdirSync("./files")
oldFiles.forEach((file)=>{
fs.unlinkSync("./files/"+file)
})
if (!fs.existsSync("./files")){
fs.mkdirSync("./files")
}
for (let index = 0; index < 100; index++) {
writeFile()
}
synchronous()
asynchronous()
让我们开始吧。
这是结果表:
运行# | 同步 | 异步 | 异步/同步比率 |
---|---|---|---|
1 | 4.999毫秒 | 12.890毫秒 | 2.579 |
2 | 5.077毫秒 | 16.267毫秒 | 3.204 |
3 | 5.241毫秒 | 14.571毫秒 | 2.780 |
4 | 5, 086毫秒 | 16.334毫秒 | 3.213 |
这些结果在这里开始得出结论。它表明随着需求或并发性的增加, Promise开销开始变得有意义。详细说来, 如果我们正在运行一个Web服务器, 该服务器应该每秒每服务器运行数百或数千个请求, 那么使用同步运行I / O操作将开始很快失去其优势。
仅出于实验目的, 让我们看看这是否确实是诺言本身的问题, 还是其他原因。为此, 我编写了一个函数, 该函数将计算时间来解决一个绝对不做任何Promise的Promise, 另一个解决100个空Promise的Promise。
这是代码:
function promiseRun() {
console.time("promise run");
return new Promise((resolve)=>resolve())
.then(()=>console.timeEnd("promise run"))
}
function hunderedPromiseRuns() {
let promiseArray = [];
console.time("100 promises")
for(let i = 0; i < 100; i++) {
promiseArray.push(new Promise((resolve)=>resolve()))
}
return Promise.all(promiseArray).then(()=>console.timeEnd("100 promises"))
}
promiseRun()
hunderedPromiseRuns()
运行# | 单一Promise | 100个Promise |
---|---|---|
1 | 1.651毫秒 | 3.293毫秒 |
2 | 0.758毫秒 | 2.575毫秒 |
3 | 0.814毫秒 | 3.127毫秒 |
4 | 0.788毫秒 | 2.623毫秒 |
有趣。看来诺言不是造成延迟的主要原因, 这让我猜测延迟的来源是内核线程在进行实际读取。对于延迟的主要原因, 这可能需要更多的实验才能得出决定性的结论。
本文总结
那你应该不使用诺言吗?我的看法如下:
如果你要编写的脚本可以在单台计算机上运行, 且管道或单个用户触发了特定的流程, 那么请使用同步代码。如果你要编写一个负责处理大量流量和请求的Web服务器, 则异步执行产生的开销将克服同步代码的性能。
你可以在存储库中找到本文中所有功能的代码。
从Promise开始, JavaScript开发者旅程中的合乎逻辑的下一步就是async / await语法。如果你想详细了解它以及我们如何到达这里, 请参阅异步JavaScript:从回调地狱到异步和等待。
评论前必须登录!
注册