为什么异步函数比同步函数更好?

3

我刚接触NodeJS,发现异步函数有点难以处理。我正在尝试理解它的好处。下面是我对异步函数的理解,请您验证一下:

请您验证以下每个理解是否正确:

  1. 当需要执行一个独立于主程序流的操作时,使用异步函数非常好。但是,如果异步函数的数据/响应在主程序中非常重要,或者多个独立的异步函数相互关联,则不适合使用异步函数。
  2. 不能依赖异步函数输出结果来控制主程序流程。因为异步总是在主流程之后执行。所以,如果需要某些函数在主流程中执行,最好将其定义为同步而不是异步。
  3. 当调用独立异步函数时,使用Promise或回调调用后续操作(异步函数)是一种常见做法。
  4. 在异步函数中仍然可以调用同步函数。但是,如果从同步函数调用异步函数,程序可能无法按预期工作,因为异步函数只会在最后执行。
3个回答

5
好的,让我们逐个分析一下。首先要知道,Node.js 是一个单线程的进程,如果需要执行阻塞进程(例如读取文件:创建事件指针、读取文件句柄、设置文件路径、设置打开模式等),最好使用异步函数,这些函数在同一页或线程池的另一个线程上执行。

1)当需要执行独立于主程序流的操作时,使用异步函数是很好的选择。但是,在主程序非常需要异步函数的数据/响应或各种独立的异步函数相互关联时,使用异步函数并不理想。

首先,我们不会将程序文件称为“主程序”,因为在 Node.js 世界中没有子程序(我不是说模块之类的)。

现在,当您说在需要立即输出时不应使用任何异步函数时,实际上是正确的。让我们看下面的例子:

...
const data = readFile( 'fileName', ( data, output ) => {
    ...
} );
console.log( data ); // this might be null or undefined

在上述情况中,我们不会使用异步函数(在传统意义上)。然而,在ES6及以上版本中,我们可以使用可爱的async/await范例:https://javascript.info/async-await
const data = await readFile( 'filename' );
await使调用伪同步:它的行为像一个async函数,但是会有一个暂停的线程等待输出。所以,在这里,你是完全正确的!让我们继续。

2)不应该依赖异步函数的输出结果在主程序流中进行操作。因为异步总是在主流程之后执行。所以,如果需要在主流程中执行某些函数,最好将其定义为同步而不是异步。

在这里,你说async在主流程之后运行。现在,那是不正确的。让我画一个简单的线程评估和执行图:

假设有两个同步函数A()B(),它们对应的线程是th__Ath__B,它们将会如下进行:

th__a ---> th__b

如果它们按顺序被解雇,A()然后是B()。它等待第一个同步(或阻塞)过程的评估,然后执行第二个。显然,它不是在整个执行结束后才执行。
但是,如果它们现在是异步函数,它们将并行执行。假设A()是同步函数,B()是具有与上述相同线程名称的异步函数,则执行类似于以下内容:
th__a     ----
- - th__b   ->

其中,-表示一个时钟周期,->表示执行结束。我们可以看到,首先触发了A(),然后在新线程上触发B()

我想这很有道理。现在回来说,如果你需要立即使用它们并将它们作为异步调用,则需要使用await


3) 当独立的异步函数被调用时,一种常见方法是使用 promises 和 callbacks 来调用后续操作(异步函数)。

完全正确。

比如,我们定义了一个名为sayHello()的函数:

const sayHello = () => {
    const P = Q.defer();

    // P.resolve(data);

    // or if there is an exception 
    // P.reject(error);

    return p.promise;
 };

这里使用了优秀的Promise库Q,我们可以按照以下方式进行调用:

sayHello.then( ( data ) => {
    console.log( data ); // P.resolve(..) is working here since the promise was successful.
} ).catch( ( err ) => {
    console.log( err ); // P.reject(..) is working here since there was a problem.
} );

或者您可以像使用fs.readFile(...)这样的回调函数:

fs.readFile( 'fileName', ( e, data ) => {
    if( e ) { return console.log( e ); } // error was handled
    // perform subsequent functions here with data
} );

4) 在异步函数中仍然可以调用同步函数,但是如果从同步函数/操作调用异步函数,则程序可能无法按预期工作,因为异步函数将仅在最后执行?

不完全如此。请参考第(2)点。它涉及线程而不是静态进程。您可以在异步函数中很好地调用同步函数,并且它将完美地工作。

当您读取文件时,例如想要通过 \n 或换行符来拆分数据:

...
if( e ) { return console.log( e ); }
const dataLines = e.split( '\n' ); // this will work brilliantly 
...

我希望这能让一切变得清晰易懂! :)

非常感谢您详细的回答!它解答了我许多甚至没有提出的问题 :) - Jab2Tech
我能想象Node的细微差别可能会让人望而生畏;我曾经也在那里。很高兴能提供帮助。 - weirdpanda

1
实际上,在处理网络请求和/或I/O操作(或需要很长时间的任务)时,javascript/node应用程序中异步功能的实际使用才真正发挥作用。

网络调用示例

以聊天应用为例。客户端(您)向某个人A发送一条消息。根据通信模型,该消息会被发送到服务器或直接发送给收件人(点对点)进行进一步处理。您还想向另一个人发送另一条消息;人B。但是,如果请求未异步处理,则客户端(您)可能需要等待不确定的时间,具体取决于您的网络速度、收件人的网络速度、网络超时期限或任何其他因素。

假设您需要等待2个单位时间才能完成您的请求。

time:        1                2             3         4                   5        
action: SendToA(message)     waitA         waitA     ResponseFromA       SendToB(message)

但是,如果相同的请求是异步的。您发送的请求由客户端(用于点对点通信)或客户端和服务器(用于客户端/服务器通信)同时异步处理。这实际上是在浏览器中发生的事情。浏览器以异步方式处理请求,因此您等待请求完成时,浏览器代码的任何其他部分都不会被卡住。

time:        1                    2                         3                    4                   5          
action: SendToA(message)  waitA/SendToB(message)         waitA/waitB        ResponseFromA       ResponseFromB

在同步情况下,您只需要在5个时间单位内成功一次请求,而在异步情况下,您需要在5个时间单位内成功两次请求。
注意:实际实现可能会有不同的时间单位延迟,这只是实际发生情况的一般概念。

一个I/O读写示例

I/O读写与网络调用非常相似,需要时间才能完成。比如说,你的应用程序有两个表格,分别从两个不同的文件中读取并填充每个表格。如果你同步处理这两个请求,会导致整体执行时间大大延长,而异步处理则可以帮助解决这个问题。

对于同步:

time:        1                       2             3           4                       5        
action: ReadFromFileA(message)     waitA         waitA     ResponseFromA       ReadFromFileB(message)

对于异步操作:

time:        1                   2                3           4                       5        
action: ReadFromFileA()     ReadFromFileB()   waitA/waitB     ResponseFromA       ResponseFromB

这是异步调用/函数的一个非常普遍的解释。然而,并不总是需要使用异步。正如您在问题中提到的那样,有时确实需要同步调用/函数。
打印是一种非常特殊的行为,必须是同步的。您不能异步地打印两张纸。
依赖函数也必须是同步的。
所有这些说法,如果正确使用,异步是一个非常强大的功能。在实时应用程序中(几乎)总会有异步性的可能性。如果您的应用程序不使用它,您的用户可能会抱怨应用程序中的糟糕用户体验。

1

看起来你还没有学习过JavaScript事件循环。这篇文章是一个不错的指南。通常在JavaScript中,异步函数用于避免像浏览器上的HTTP调用、NodeJS上的文件读取等过程中的IO(输入/输出)延迟,而不是执行延迟或并行执行,因为JavaScript进程是单线程的。如果你想更深入地了解非阻塞IO,这篇文章回答了你所有的问题。

当编写异步方法时,如你所提到的,很难跟踪每个回调,然后再次执行回调,一遍又一遍,这被称为JavaScript中的“回调地狱”。awaitasync不过是一个简单的解决方案。


通过静态图像来理解事件循环并不是那么直观 - 我喜欢这个视频 - 你可以“看到”发生了什么 :p - Jaromanda X
感谢您分享这篇文章! - Jab2Tech

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