【问题标题】:fs.readFile operation inside the callback of parent fs.readFile does not execute asynchronously父 fs.readFile 回调中的 fs.readFile 操作不会异步执行
【发布时间】:2022-02-18 17:49:24
【问题描述】:

我读到fs.readFile 是一个异步操作,读取发生在主线程之外的单独线程中,因此主线程执行不会被阻塞。所以我想试试下面的东西

// reads take almost 12-15ms
fs.readFile('./file.txt', (err, data) => {
  console.log('FIRST READ', Date.now() - start)
  
  const now = Date.now()

  fs.readFile('./file.txt', (err, data) => {
    // logs how much time it took from the beginning
    console.log('NESTED READ CALLBACK', Date.now() - start)
  })


  // blocks untill 20ms more, untill the above read operation is done
  // so that the above read operation is done and another callback is queued in the poll phase
  while (Date.now() - now < 20) {}

  console.log('AFTER BLOCKING', Date.now() - start)
})

我正在父 fs.readFile 调用的回调中进行另一个 fs.readFile 调用。我期望的是日志NESTED READ CALLBACK 必须在AFTER BLOCKING 之后立即到达,因为读取必须在单独的线程中异步完成

结果日志NESTED READ CALLBACKAFTER BLOCKING 调用后15 毫秒出现,表明当我在while 循环中阻塞时,异步读取操作不知何故从未发生。顺便说一句,while 循环是用来模拟一些需要 20 毫秒才能完成的任务

这里到底发生了什么?还是我在这里遗漏了一些信息?顺便说一句,我正在使用 Windows

【问题讨论】:

    标签: node.js express asynchronous async-await


    【解决方案1】:

    while() 循环期间,不会处理 Javascript 中的任何事件,也不会运行任何 Javascript 代码(因为您只是通过循环来阻止事件循环的处理)。

    磁盘操作可以取得一些进展(因为它们在系统线程中做一些工作),但在您的while 循环完成之前不会处理它们的结果。但是,因为 fs.readFile() 实际上包含三个或更多操作,fs.open()fs.read()fs.close(),所以在事件循环被阻塞时它可能不会走得太远,因为它需要处理事件才能前进通过其工作的不同阶段。

    结果日志 NESTED READ CALLBACK 在 AFTER BLOCKING 调用后 15 毫秒出现,表明当我在 while 循环中阻塞时,异步读取操作从未发生过。顺便说一句,while 循环是用来模拟一些需要 20 毫秒才能完成的任务

    这里到底发生了什么?

    fs.readFile() 不是一个单一的整体操作。相反,它由fs.open()fs.read()fs.close() 组成,它们的排序在主线程的用户级Javascript 中运行。因此,当您使用 while() 循环阻塞主线程时,fs.readFile() 无法取得很大进展。可能发生的情况是您启动了第二个fs.readFile() 操作并启动了fs.open() 操作。它被发送到 libuv 线程池中的操作系统线程。然后,您使用 while() 循环阻止事件循环。当该循环阻塞事件循环时,fs.open() 完成并且(在 libuv 事件循环内部)一个事件被放入事件队列中以调用 fs.open() 调用的完成回调。但是,事件循环被您的循环阻塞,因此无法立即调用回调。因此,完成fs.readFile() 操作的任何进一步进展都会被阻止,直到事件循环释放并可以再次处理等待事件。

    当您的while() 循环完成时,控制权将返回到事件循环,并调用fs.open() 调用的完成回调,而随后将开始从文件中读取实际数据。


    仅供参考,您实际上可以在 Github 存储库中检查 fs.readFile()here 的代码。如果你按照它的流程,你会看到,从它自己的 Javascript 中,它首先调用 binding.open() 这是一个本机代码操作,然后,当它完成并且 Javascript 能够通过事件队列处理完成事件时,它将然后运行函数readFileAfterOpen(...),它将调用bind.fstat()(另一个本机代码操作),然后,当它完成并且Javascript能够处理完成事件时 通过事件队列,它将调用 `readFileAfterStat(...) 分配缓冲区并启动读取操作。

    在这里,随着流程跳到 read_file_context 对象 here 并最终调用 read() 并在完成并且 Javascript 能够通过事件循环处理完成事件时再次调用,代码变得更难遵循,它可以进一步推进进程,最终将文件中的所有字节读入缓冲区,关闭文件,然后调用最终回调。

    所有这些细节的重点是说明fs.readFile() 是如何用 Javascript 编写的,并且由多个步骤组成(其中一些调用代码将在不同的线程中使用一些本机代码),但只能从一个当事件循环能够处理新事件时,进入下一步。因此,如果您使用while 循环阻塞事件循环,那么fs.readFile() 将卡在步骤之间并且无法前进。只有当事件循环能够处理事件时,它才能前进并最终完成。

    类比

    这是一个简单的类比。你请你哥哥帮你一个忙,去三家店给你取东西吃晚饭。你给他第一家商店的清单和目的地,然后让他在第一家商店完成后用手机给你打电话,你会给他第二个目的地和那家商店的清单。他前往第一家商店。

    与此同时,你用手机给你的女朋友打电话,开始和她进行长时间的交谈。你的兄弟在第一家商店结束并给你打电话,但你忽略了他的电话,因为你还在和你的女朋友说话。你的兄弟被困在跑腿的任务上,因为他需要和你谈谈以了解下一步是什么。

    在这个类比中,手机有点像事件循环处理下一个事件的能力。如果你阻止了他打电话给你的能力,那么他就无法进行下一步(事件循环被阻止)。他对这三家店的走访就像是进行fs.readfile()操作所涉及的各个步骤。

    【讨论】:

    • 哇没想到会得到如此全面的答案。我读了两段,我立刻明白发生了什么。非常感谢,这清楚了很多。我顺便测试一下,如果我在轮询阶段处理回调,并且如果我有一个计时器准备好并且在轮询阶段要执行另一个回调,那么事件循环将转到计时器或轮询阶段的回调,这就是为什么我尝试向 readFile 发送嵌套调用
    • @rajaaekant - 我希望这一切只是为了了解事件循环,因为您通常不应该编写依赖于这种实现细节级别或依赖于这种知识水平的代码。
    • 是的,这是关于学习事件循环的。我实际上正在关注文档。这里nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/#poll 提到如果轮询队列为空,节点将在 epoll 调用时阻塞,直到最近的计时器准备好,但如果有事件,它将继续处理队列。所以我想正如你解释的那样,fs.readFile 实际上是三个不同的调用fs.open()fs.read()fs.close() 然后它回答了我猜的我的问题。即使计时器准备就绪,节点也会遍历轮询队列,直到达到限制对吧?
    猜你喜欢
    • 2016-09-14
    • 2014-04-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-01-05
    • 1970-01-01
    • 1970-01-01
    • 2016-07-18
    相关资源
    最近更新 更多