在Node.js中,同步和异步编程有什么区别?

208

我一直在阅读NodeBeginner,并发现了以下两段代码。

第一段:

    var result = database.query("SELECT * FROM hugetable");
    console.log("Hello World");

第二个:

    database.query("SELECT * FROM hugetable", function(rows) {
       var result = rows;
    });
    console.log("Hello World");

我理解它们的作用,它们查询数据库以检索查询的答案,然后console.log('Hello world')

第一个代码段应该是同步代码。 第二个代码段是异步代码。

这两段代码之间的区别对我来说非常模糊。输出会是什么呢?

在互联网上搜索关于异步编程也没有帮助我。


43
奇怪的是你在谷歌上没有找到任何相关内容,这是一个相当大的主题。在同步编程中,每个步骤都会在前一个步骤执行完毕后依次执行。在异步编程中,即使第一步尚未完成,第二步也将被执行。在你第二个例子中看到的函数被称为回调函数,它将在返回数据库结果后立即运行,这可能会在console.log运行之后发生。 - Laurent S.
9
异步编程的内容很多,但没有相对简单的解释它是什么以及在实践中意味着什么。 - Azeirah
1
为什么我们应该避免使用同步函数? - Charlie Parker
4
因为它们阻塞事件循环,你失去了异步事件驱动 I/O 模型所带来的所有好处。而且这是一种不好的实践。这样想吧:如果你不使用异步函数,那你为什么要使用 Node.js 呢? - Gabriel Llamas
2
@GabrielLlamas,如果我执行一个INSERT查询并且想在database.query()之后使用最后插入的ID,那么我应该将其作为同步调用,对吗?还是应该采取什么方法?(这个问题我已经有很长时间了) - San
显示剩余2条评论
10个回答

241
区别在于,在第一个示例中,程序将在第一行中阻塞。下一行(console.log)必须等待。
在第二个示例中,console.log将在查询正在处理时执行。也就是说,查询将在后台处理,而您的程序正在做其他事情,一旦查询数据准备好,您就可以按照自己的意愿处理它。
简而言之:第一个示例会阻塞,而第二个示例不会。
以下是两个示例的输出结果:
// Example 1 - Synchronous (blocks)
var result = database.query("SELECT * FROM hugetable");
console.log("Query finished");
console.log("Next line");


// Example 2 - Asynchronous (doesn't block) 
database.query("SELECT * FROM hugetable", function(result) {
    console.log("Query finished");
});
console.log("Next line");

将会是:

  1. 查询完成
    下一行
  2. 下一行
    查询完成

注意
虽然Node本身是单线程的,但有些任务可以并行运行。例如,文件系统操作发生在不同的进程中。

这就是为什么Node能够进行异步操作:一个线程正在执行文件系统操作,而主Node线程继续执行您的JavaScript代码。在基于事件驱动的服务器中(如Node),文件系统线程通知主Node线程某些事件的完成、失败或进度,以及与该事件相关联的任何数据(例如数据库查询的结果或错误消息),主Node线程决定如何处理该数据。

您可以在这里阅读更多信息:Node.js中单线程非阻塞IO模型的工作原理


10
基本上,当我执行第一段代码时,它会做这样的事情: `请求查询; 当请求完成后5秒钟; 控制台日志`;而当第二个执行时: `请求查询; 控制台日志; 处理查询。` - Azeirah
1
@JohnGalt,SQL 运行在不同的线程上。但是这当然取决于您使用的 SQL 驱动程序的实现方式。驱动程序应该会生成一个新线程,连接到 MySQL 并运行查询。一旦完成,将结果发布到 事件队列,Node 将调用回调函数。 - Salvatorelab
4
异步示例不可能像#1一样输出相同的结果吗?比如,database.query执行得非常快,以至于当我们到达console.log时,任务已经完成了。 - greatwolf
2
@TheBronx 如果在示例2中,console.log("Next line"); 在匿名函数内部,紧接着 console.log("query finished"); 之后,那么这意味着 "Next Line" 将会在 "query finished" 之后被打印出来,对吗?因此,如果我将所有内容嵌套在一起,所有的东西都将以同步方式运行,因此我就不需要担心使用某些函数的同步版本了。我的理解是正确的吗? - Honinbo Shusaku
4
简短回答:是的,@Abdul,你说得对。 详细回答:嵌套函数(回调函数)是按顺序执行操作的一种方式,但从技术上讲这并不是“同步”的。匿名函数仍然在“阻塞操作完成后”执行,或者换句话说,是“异步”的。Node.js可以在执行该阻塞操作时执行其他函数。函数仍然是异步的,只是你将它们链接在一起。同步函数会阻止执行,这是关键。 - Salvatorelab
显示剩余4条评论

82
这两种方法的区别如下: 同步方式: 它会等待每个操作完成后才执行下一个操作。 对于您的查询: console.log()命令将一直等到查询完成并从数据库获取所有结果之后才会执行。 异步方式: 它不会等待每个操作完成,而是在第一次执行时就执行所有操作。每个操作的结果将在可用时进行处理。 对于您的查询: Database.Query()方法后很快就会执行console.log()命令。当数据库查询在后台运行并在完成检索数据后加载结果时。 使用情况
  1. 如果您的操作没有像从数据库中查询大量数据这样的重活,则可以使用同步方式,否则请使用异步方式。
  2. 在异步方式中,您可以向用户显示某些进度指示器,同时在后台继续进行重负荷工作。 这是GUI应用程序的理想场景。

2
这是否意味着 db.query(cmd, callback) 是并发运行的(如线程)?它们是同时运行的吗? - Charlie Parker
在他的第二个例子中,有没有可能查询非常快地完成,然后先调用回调函数,再调用 console.log - Fahmi
@Fahmi 理论上是可以的,但实际上相当不可能。 - Leo Messi

26

如果你在这两个示例中都添加一行,可能会更加清晰明了:

var result = database.query("SELECT * FROM hugetable");
console.log(result.length);
console.log("Hello World");

第二个:

database.query("SELECT * FROM hugetable", function(rows) {
   var result = rows;
   console.log(result.length);
});
console.log("Hello World");

尝试运行这些代码,你会注意到第一个同步示例中,“Hello World”行之前将打印出result.length。 在第二个异步示例中,“Hello World”行之后(很可能)才会打印出result.length。

这是因为在第二个示例中,database.query在后台异步运行,脚本立即继续执行“Hello World”。只有在数据库查询完成时,才会执行console.log(result.length)


1
你说:结果的长度 result.length (很可能) 会在“Hello World”行之后打印出来。...为什么只是“很可能”呢?我认为它总是在 console.log 输出之后打印。感谢澄清 :) - humanityANDpeace
9
@humanityANDpeace: 异步访问的整个意义就在于:你不知道何时会完成。也许这是一个极快的数据库,数据库查询甚至在Javascript运行到“Hello World”代码行之前就已经返回了结果... - Martijn

21

首先,我意识到我的回答有些晚了。

在讨论同步和异步之前,让我们简要了解程序的运行方式。

同步情况下,每个语句都会在下一个语句运行之前完成。在这种情况下,程序会按照语句的顺序进行评估。

这就是JavaScript中异步的工作方式。JavaScript引擎分为两部分:一部分查看代码并排队操作,另一部分处理队列。队列处理发生在一个线程中,因此一次只能进行一个操作。

当发现异步操作(如第二个数据库查询)时,代码将被解析并将操作放入队列中,但在这种情况下,将注册回调以在完成此操作时运行。队列可能已经有很多操作了。处理队列中位于前面的操作将被处理并从队列中删除。一旦处理了数据库查询的操作,请求将发送到数据库,并在完成时执行回调。此时,队列处理器已经“处理”了该操作,继续处理下一个操作-在本例中为

    console.log("Hello World"); 

数据库查询仍在处理中,但console.log操作位于队列的前面并得到处理。由于这是同步操作,因此立即执行并立即产生输出“Hello World”。稍后,数据库操作完成,只有在此之后才调用和处理与查询注册的回调,将变量result的值设置为行。

一个异步操作可能会导致另一个异步操作,第二个操作将被放入队列中,在队列的前面时将被处理。调用注册在异步操作中的回调函数是JavaScript运行时在操作完成时返回结果的方式。

了解哪些JavaScript操作是异步操作的简单方法是注意是否需要回调-回调是当第一个操作完成时将执行的代码。在问题中的两个示例中,我们只能看到第二种情况具有回调,因此它是其中的异步操作。这不总是这样,因为异步操作的结果处理方式不同。

要了解更多信息,请阅读有关Promise的内容。Promise是处理异步操作结果的另一种方式。Promise的好处是编码风格感觉更像同步代码。

许多库(如node 'fs')为某些操作提供同步和异步两种风格。在操作时间不长且使用不频繁的情况下,例如读取配置文件,同步风格操作将导致更易于阅读的代码。


6
在同步情况下,只有当SQL查询完成执行后,console.log命令才会被执行。
在异步情况下,console.log命令将直接被执行。然后,查询结果将在稍后由“回调”函数存储。

1
但它们实际上是同时被调用的吗?让我困惑的是,在异步代码中,实际的代码是否在同时并行运行? - Charlie Parker
这取决于处理器(它是否是多核的?)和操作系统。请参阅http://en.wikipedia.org/wiki/Multithreading_(software)#Multithreading。 - related

5
主要的不同在于异步编程,否则你不会停止执行。在“请求”期间,您可以继续执行其他代码。

3

JS中的异步编程:

同步

  • 停止执行进一步代码直到完成。
  • 由于这种停止进一步执行,同步代码被称为“阻塞”,阻塞意味着不会执行其他代码。

异步

  • 执行被推迟到事件循环中,这是JS虚拟机中的一个构造,用于执行异步函数(在同步函数堆栈为空之后)。
  • 异步代码被称为非阻塞,因为它不会阻止其他代码运行。

例子:

// This function is synchronous
function log(arg) {
    console.log(arg)
}

log(1);

// This function is asynchronous
setTimeout(() => {
    console.log(2)
}, 0);

log(3)

  • 这个示例记录了 1、3 和 2。
  • 2 最后被记录,因为它在一个异步函数中,该函数在栈为空后执行。

2

这个函数使第二个操作变成了异步操作。

第一个操作会强制程序等待每一行执行完毕后才能继续执行下一行。而第二个操作允许每一行同时(并且独立)运行。

对于需要实时传输的应用(例如聊天、股票应用),支持异步或并发的编程语言和框架(如JavaScript、Node.js)非常适合使用。


1
同步函数是阻塞的,而异步函数则不是。在同步函数中,语句完成后才会运行下一条语句。在这种情况下,程序按照语句的顺序进行评估,并且如果其中一条语句需要很长时间,则程序的执行将暂停。
异步函数通常接受回调作为参数,并在异步函数被调用后立即继续执行下一行。只有在异步操作完成且调用堆栈为空时,才会调用回调函数。应该异步执行重型操作,例如从Web服务器加载数据或查询数据库,以便主线程可以继续执行其他操作,而不是阻塞直到长时间操作完成(在浏览器的情况下,UI 将会冻结)。
原始发布于Github: 链接

-1

同步编程

C、C#、Java 等编程语言是同步编程,不管你写什么,都将按照你的编写顺序执行。

-GET DATA FROM SQL.
//Suppose fetching data take 500 msec

-PERFORM SOME OTHER FUNCTION.
//Performing some function other will take 100 msec, but execution of other 
//task start only when fetching of sql data done (i.e some other function 
//can execute only after first in process job finishes).

-TOTAL TIME OF EXECUTION IS ALWAYS GREATER THAN (500 + 100 + processing time) 
msec

异步

NodeJs提供了异步特性,它是非阻塞的,假设在任何I/O任务中需要花费时间(获取、写入、读取),nodejs不会保持空闲并等待任务完成,它将开始执行队列中的下一个任务,并在那个耗时的任务完成时使用回调通知。

以下示例将有所帮助:
//Nodejs uses callback pattern to describe functions.
//Please read callback pattern to understand this example

//Suppose following function (I/O involved) took 500 msec
function timeConsumingFunction(params, callback){
  //GET DATA FROM SQL
  getDataFromSql(params, function(error, results){
    if(error){
      callback(error);
    }
    else{
      callback(null, results);
    }
  })
}

//Suppose following function is non-blocking and took 100 msec
function someOtherTask(){
  //some other task
  console.log('Some Task 1');
  console.log('Some Task 2');
}

console.log('Execution Start');

//Start With this function
timeConsumingFunction(params, function(error, results){
    if(error){
      console.log('Error')
    }
    else{
      console.log('Successfull'); 
    }
  })

//As (suppose) timeConsumingFunction took 500 msec, 
//As NodeJs is non-blocking, rather than remain idle for 500 msec, it will start 
//execute following function immediately
someOtherTask();

简而言之,输出如下:
Execution Start
//Roughly after 105 msec (5 msec it'll take in processing)
Some Task 1
Some Task 2
//Roughly After 510 msec
Error/Successful //depends on success and failure of DB function execution

区别很明显,同步肯定需要超过600毫秒(500 + 100 + 处理时间),异步可以节省时间。


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