异步传输与同步传输(从小白到高手,10 图教你同步与异步)

作者|马农的荒野生存

资料来源:马农的荒野生存(ID:escapeit)

在本文中,我们将讨论什么是同步,什么是NicholasTse,以及这两个极其重要的概念在编程中的意义。

我相信,当许多学生遇到异步的两个词时,他们的大脑会突然陷入一种无知的状态,就像十字路口的交通灯失灵一样:

是的,这两个看起来和实际上很像的词给博主带来了很大的麻烦。这两个词背后的意思是什么?

让我们从工作场景开始。

夜间程序员

假设你的老板给你分配了一项非常紧急和重要的任务,你必须在下班前完成(邪恶资本主义)。为了加快进度,老板移动了一把椅子,坐在那里看着你写代码。

你一定是心里骂了一句:“WTF,你有这么多闲暇吗?看着我,你就不能做点别的吗?”

老板似乎收到了你的脑波:“我在这儿等。在你写完之前,我哪儿也不去,也不上厕所。”

在这个例子中,老板给你任务后,他会等你写完后再做任何事情。这种情况称为同步。

第二天,老板又给了你一个任务。

但这一次,我并不着急。这一次,老板轻描淡写地说:“年轻人,没关系。很好。如果你再努力工作一年,我明年就没有经济负担了。我不急着完成今天的任务。写完就告诉我。”。

这一次,老板没有盯着你写代码,而是转过头来刷视频。写完后,只需向老板报告“我写完了”。

在这个例子中,老板在完成任务后不再等待任何事情,而是去做其他事情。完成任务后,只需告诉老板任务已经完成,这就是所谓的异步。

值得注意的是,在异步(NicholasTse)的场景中,重点是老板在你写代码时刷剧本。这两件事是同时发生的,而不是一方等待另一方。因此,这就是为什么无论在什么场景中使用NicholasTse,总的来说比同步更高效。

我们可以看到,单词synchronization通常与任务的“依赖”、“关联”和“等待”等关键字相关,而NicholasTse通常与任务的“独立”、“不相关”、“无需等待”、“同时”等关键字相关。

顺便说一句,如果你遇到一个老板盯着你在背后写代码,最好还是走吧。

电话和电子邮件

作为一名勤奋的程序员,你不能只专注于移动砖块。在日常工作中,你无法避免沟通。有效的沟通方式之一是争吵。。。不,是电话。

通常,打电话时,一个人在说话,另一个人在听。当一个人在说话时,另一个人在等待。当另一个人讲完后,后再继续讲下去。因此,在这个场景中,你可以看到关键词“依赖”、“相关性”和“等待”出现。因此,这种打电话的通信方法就是所谓的同步。

编码人员常用的另一种通信形式是电子邮件。

电子邮件是另一种不可或缺的交流方式,因为没有人在等着你写电子邮件而什么也不做,所以你可以慢慢写。当你写一封电子邮件时,收件人可以做一些有意义的事情,比如摸鱼,上厕所,抱怨为什么国庆假期没有持续两周。

同时,你不需要等待对方回复,也不需要在写完邮件后什么都不做。你也可以做一些有意义的事情,比如钓鱼

在这里,你写了一封电子邮件,其他人也收到了。这两件事是同时进行的。收件人和发件人不需要等待对方。当发件人写完邮件后,只需点击发送即可。收件人可以在收到后阅读。接收者和发送者不需要相互依赖和等待。

你看,在这个场景中,关键词“独立”、“无关”和“无需等待”出现。因此,电子邮件的沟通方式是异步的。

同步呼叫

现在我终于回到编程的主题。

既然我们已经理解了同步和NicholasTse在各种场景中的含义(我希望如此),那么程序员应该如何理解同步和NicholasTse呢?

让我们从同步调用开始,这是程序员最熟悉的场景。

异步传输

一般函数调用是同步的,如下所示:

funcA{

//等待funcB完成函数的执行

funcB;

//继续下一个过程

}

如果funcA打电话给funcB,funcA的后续代码在funcB执行之前不会执行,也就是说,funcA必须等待funcB执行,如下所示:

从上图可以看出,funcA在funcB的行动中什么都做不了。这是典型的同步。

请注意,一般来说,funcA和funcB是同一条线索,这是最常见的情况。

然而,值得注意的是,即使在两个线程中运行的函数也可以被同步调用。例如,当我们执行IO操作时,底层实际上是通过系统调用(有关系统调用,请参阅《程序员应如何理解系统调用》)要向操作系统发送请求,例如磁盘文件读取,请执行以下操作:

读取(文件,buf);

我们就在这里《读取文件时,程序经历了什么》对于中描述的阻塞I/O,在读取函数返回之前,程序无法继续前进。

如图所示:

只有当read函数返回时,程序才能继续。

请注意,与上面的同步调用不同,函数和被调用函数在不同的线程中运行。

因此,我们可以得出结论,同步调用和函数以及被调用函数是否在同一线程上运行是无关的。

在此,我们想再次强调,在同步模式下,函数和被调用函数不能同时执行。

对于程序员来说,同步编程是最自然、最容易理解的。

然而,很容易理解的是,在某些情况下,同步并不高效。原因很简单,因为任务不能同时执行。

异步,我们下一个打电话。

异步的电话

如果有同步通话,就有异步通话。

如果你到目前为止真的理解了这一部分的内容,异步的电话对你来说不是问题。

一般来说,异步的通话总是伴随着耗时的任务,如I/O操作,如磁盘文件读写、网络数据发送和接收、数据库操作等。

让我们以读取磁盘文件为例。

在同步调用read函数的方式中,调用者在读取文件之前不能继续进行,但是如果可以由NicholasTse调用read函数,情况就不同了。

如果读取函数可以由NicholasTse调用,则即使文件尚未读取,读取函数也可以立即返回。

读取(文件,buff);

//read函数立即返回

//当前程序不会被阻止

这样地:

可以看出,在NicholasTse的调用模式下,调用方不会被阻止,在函数调用完成后,可以立即执行下一个程序。

此时,NicholasTse的重点是调用方的下一个程序执行可以与文件读取同时进行。我们也可以从上图中看到这一点,这就是异步的效率。

然而,请注意,异步的电话是程序员理解和编写代码的负担。一般来说,上帝在为你开门时会适当地关上窗户。

一些学生可能会问,在同步调用下,调用方将不再继续执行,而是暂停并等待。被调用函数执行后,调用方自然会继续执行。调用方如何知道被调用函数是否在NicholasTse的调用下执行?

这可分为两种情况:

  1. 调用方根本不关心执行结果

  2. 调用者需要知道执行结果

第一种情况相对简单,不需要讨论。

第二个案例更有趣。通常有两种实现方法:

一种是通知机制,即当任务完成时,发送信号通知调用者任务完成。注意,这里有很多实现信号的方法,比如Linux中的信号或信号量。

另一种是回调,通常被称为回调。我们将在下一篇文章中重点讨论回调,本文将简要讨论回调。

接下来,让我们用一个具体的例子来解释同步通话和NicholasTse通话。

同步vs异步

我们用常见的web服务来说明这个问题。

一般来说,web服务器在收到用户请求后会有一些典型的处理逻辑。最常见的是数据库查询(当然,您也可以用其他I/O操作替换这里的数据库查询,例如磁盘读取、网络通信等)。这里,我们假设处理用户请求需要经过步骤a、B、C,然后读取数据库,在读取数据库后,需要经过步骤D、e和F,如下所示:

#处理用户请求的步骤:

A.

B

C

数据库读取;

D

E

F

其中,步骤a、B、C、D、e和F不需要任何I/O,即这六个步骤不需要读取文件、网络通信等。I/O操作只涉及数据库查询。

一般来说,这样的web服务器有两个典型的线程:主线程和数据库处理线程。请注意,这只是一个典型的场景,具体的业务可能会有所不同,但这并不影响我们使用两个线程来说明问题。

首先,让我们看看最简单的实现,即同步。

这是最自然、最容易理解的方式:

//主线

主线程{

A.

B

C

发送数据库查询请求;

D

E

F

}

//数据库线程

数据库线程{

而(1){

处理数据库读取请求;

返回结果;

}

}

这是最典型的同步方法。发出数据库查询请求后,主线程将被阻塞和挂起。D、e和F可以在数据库查询完成后继续运行,如下所示:

我们可以从图中看到,主线中会有一个“间隙”,即主线的“空闲时间”。在空闲时间内,主线程需要等待数据库查询完成,然后才能继续后续处理过程。

在这里,主线程就像主管的老板,而数据库线程就像程序员强迫他移动砖块。在搬砖头之前,老板什么都不做,只是密切关注你,在你搬砖头之前,他不会去做其他事情。

显然,高效的程序员不能容忍懒惰的主线程。

是时候牺牲那个大杀手了。我是异步。

在NicholasTse的实现方案中,主线程不等待数据库完成查询,而是在发送数据库读写请求后直接处理下一个请求。

有些学生可能会有问题。一个请求需要经过七个步骤:A、B、C、数据库查询、D、e和F。如果主线程在完成A、B、C和数据库查询后直接处理下一个请求,那么上一个请求中D、e和F的其余步骤呢?

如果你没有忘记上一节的内容,你应该知道有两种情况,我们将分别讨论。

1.主线程不关心数据库操作结果

在这种情况下,主线程不关心数据库查询是否完成。数据库查询完成后,它会处理接下来的三个步骤:D、e和F,如下所示:

看,这就是重点。

我们说过,请求需要经过七个步骤,其中前三个在主线程中完成,后四个在数据库线程中完成。数据库线程如何知道在检查数据库后处理步骤D、e和f?

此时,我们的另一个主角回调函数开始出现。

是的,回调函数用于解决这个问题。

我们可以将处理D、e和f的步骤封装到一个函数中,假设该函数在DB查询之后命名为handle_DEF_uu:

voidhandle_udef_uuu在uuuDBuuu查询{

D

E

F

}

这样,在发送数据库查询请求时,主线程将函数作为参数传递:

DB查询(DB查询后的请求、处理);

在数据库线程完成处理DEFuuAfteruGuoJingjinguJustquery后直接调用handle,这是回调函数的函数。

一些学生可能想知道为什么这个函数应该传递给数据库线程,而不是数据库线程定义自己的调用?

因为在软件组织结构方面,这不是数据库线程应该做的。

数据库线程只需要查询数据库,然后调用处理函数,而数据库线程为处理函数所做的事情根本不关心,也不应该关心。

可以传入各种回调函数。也就是说,数据库系统可以为回调函数的抽象函数变量编程,以便更好地处理这种变化,因为回调函数内容的变化不会影响数据库线程的逻辑。如果数据库线程定义了自己的处理功能,那么这种设计就没有灵活性。

从软件开发的角度来看,假设数据库线程逻辑被封装以向其他团队提供库,那么数据库团队如何知道在数据库查询之后该做什么?

显然,只有用户知道查询数据库后要做什么,所以用户在使用它时可以简单地传入这个回调函数。

这样,复杂数据库团队和用户团队就实现了所谓的解耦。

现在你应该了解回调函数的功能了。

它不容易。让我喝一杯,叉腰休息一下。

让我们继续。

此外,通过仔细观察以上两张图片,你能看出为什么异步比同步更高效吗?

原因很简单。这就是我们在本文中提到的。异步自然不需要等待和依赖。

从前面的图中,我们可以看到主线的“闲暇时间”消失了。相反,它一直在工作,工作,工作,就像被迫996程序员一样。而且,数据库线程并不是长时间处于空闲状态,而是在工作、工作、工作。

可以同时执行主线程处理请求和数据库处理查询请求。因此,从系统性能的角度来看,本设计可以充分利用系统资源,更快地处理请求;从用户的角度来看,系统的响应速度会更快。

这就是异步的效率。

但我们也应该看到,异步的编程不像同步那么容易理解,系统的可维护性也不如同步模式好。

那么,有没有一种方法可以将对同步模式的简单理解与尼古拉斯·异步模式的高效结合起来呢?答案是肯定的,我们将在接下来的章节中详细解释这项技术。

接下来,让我们看看第二种情况,即主线程需要关心数据库查询结果。

2.主线程关心数据库操作结果

在这种情况下,数据库线程需要通过通知机制将查询结果发送到主线程。接收到消息后,主线程继续处理前一个请求的后半部分,如下所示:

从这里可以看出,ABCDEF的所有步骤都在主线中处理,主线也没有“空闲时间”,但在这种情况下,数据库线程相对空闲。从这里来看,它的效率不如前一种方法,但它仍然比同一步模式更有效。

最后,应该指出的是,异步在所有情况下都不一定比同步更有效。它还需要结合具体的业务和IO复杂性进行分析。

总结

在本文中,我们从不同的场景分析了同步和异步这两个概念,但无论是什么场景,同步通常意味着双方都必须等待和相互依赖,而异步则意味着双方都是独立的,各自为政。希望这篇文章可以帮助你理解这两个重要的概念。

点共享

您可以还会对下面的文章感兴趣

最新评论

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

使用微信扫描二维码后

点击右上角发送给好友