如果Node.js使用非阻塞IO,那么fs.readFileSync是如何实现的?

16

我在文件系统库中看到了很多同步函数,例如fs.readFileSync(filename, [options])

如果node有异步/非阻塞IO并且没有休眠方法,为什么要实现这些同步函数?我是否可以使用相同的机制来实现其他同步函数?


好问题!也许需要为JavaScript重新发明协程,并公开事件循环,以允许在Node.js中编写同步非阻塞代码。 - Alex
1个回答

22
fs.readFileSync()

实际上只是一个包装器,用于

fs.readSync() 

函数。问题是fs.readSync()相对于fs.read()的实现方式是什么。如果你看一下这两个函数的实现,它们都利用了bindings模块。在这种情况下,它被初始化为

var binding = process.binding('fs').  

并且电话通话

binding.read(fd, buffer, offset, length, position, wrapper);//async
var r = binding.read(fd, buffer, offset, length, position);//sync

分别地,一旦我们处于“binding”模块中,我们就在v8,node_#####.cc领域之外。绑定('fs')的实现可以在node存储库代码中的node_file.cc中找到。Node引擎为C++调用提供重载,一个带回调,一个不带回调。node_file.cc代码利用了req_wrap类。这是v8引擎的包装器。在node_file.cc中,我们看到:

#define ASYNC_CALL(func, callback, ...)                           \
  FSReqWrap* req_wrap = new FSReqWrap(#func);                     \
  int r = uv_fs_##func(uv_default_loop(), &req_wrap->req_,        \
      __VA_ARGS__, After);                                        \
  req_wrap->object_->Set(oncomplete_sym, callback);               \
  req_wrap->Dispatched();                                         \
  if (r < 0) {                                                    \
    uv_fs_t* req = &req_wrap->req_;                               \
    req->result = r;                                              \
    req->path = NULL;                                             \
    req->errorno = uv_last_error(uv_default_loop()).code;         \
    After(req);                                                   \
  }                                                               \
  return scope.Close(req_wrap->object_);

#define SYNC_CALL(func, path, ...)                                \
  fs_req_wrap req_wrap;                                           \
  int result = uv_fs_##func(uv_default_loop(), &req_wrap.req, __VA_ARGS__, NULL); \
  if (result < 0) {                                               \
    int code = uv_last_error(uv_default_loop()).code;             \
    return ThrowException(UVException(code, #func, "", path));    \
  }

需要注意的是SYNC_CALL使用了不同的req-wrap。这是ASYNC方法中相关的req_wrap构造函数的代码,可以在req_wrap.h中找到。

ReqWrap() {
    v8::HandleScope scope;
    object_ = v8::Persistent<v8::Object>::New(v8::Object::New());

    v8::Local<v8::Value> domain = v8::Context::GetCurrent()
                                  ->Global()
                                  ->Get(process_symbol)
                                  ->ToObject()
                                  ->Get(domain_symbol);

    if (!domain->IsUndefined()) {
      // fprintf(stderr, "setting domain on ReqWrap\n");
      object_->Set(domain_symbol, domain);
    }

    ngx_queue_insert_tail(&req_wrap_queue, &req_wrap_queue_);
  }
请注意,此函数会创建一个新的v8作用域对象来处理该事件的运行。这就是异步部分发生的地方。v8引擎启动一个新的JavaScript解释环境来单独处理这个调用。简而言之,如果您不构建/修改自己的Node版本,则无法实现自己的异步/同步调用版本,就像Node一样。话虽如此,异步真正适用于I/O操作。也许需要说明一下为什么您认为需要更多同步的原因。总的来说,如果您认为Node不支持您想要做的事情,那么您只是没有充分利用回调机制。
也就是说,如果需要异步行为,可以考虑使用事件节点模块来实现自己的事件处理程序。如果有一些您迫切需要同步执行的任务,可以考虑使用本机扩展,但我强烈反对这样做。请考虑如何在异步事件循环中完成所需工作。接受这种思维方式,或者换一种语言。
强制让一种语言以不想处理的方式处理东西是编写糟糕代码的绝佳方法。

1
非常详尽的回答。“强制一种语言以不想处理的方式处理事情”- 这是一个有趣的措辞。这难道不正是NodeJS开发人员使用“fs.readFileSync”所做的吗?它为什么存在?也许在Node中确实有同步调用的空间? - guy mograbi
根据您的问题,同步FS方法存在两个原因。A:作为初始化步骤的一部分收集配置信息是一个相对常见的需求,并且更简单地同步完成。B:在某些情况下,一系列同步磁盘请求实际上比使用一堆异步请求更高效,因为后者可能无法处理。服务器IO(DDOS攻击)也是如此,只是创建这种情况要困难得多(大约10000倍)。但是,单个进程过载磁盘驱动器并使其呈指数级减速非常容易。 - MobA11y
1
关于你的句子“异步只适用于I/O操作”:难道计算密集型任务(如暴力破解、压缩等)和渲染不也是异步的应用场景吗?虽然这些任务主要基于计算而非I/O操作,但它们仍然比每个事件-时钟/循环所需的时间更长。 - kernel
1
“异步真正只适用于I/O操作”意味着这些是Node开发人员使异步的唯一事物。并不意味着它们是唯一有用的异步操作。此外,在节点中为数字计算生成单独的进程以强制执行异步操作是微不足道的。而要强制执行真正同步的操作则不是那么容易。 - MobA11y
1
"...你可以考虑使用本地扩展..." 所以是的。 - MobA11y
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接