本文原题“从小白到高手,你须要理解同步与异步”,转载请联系做者。php
一、系列文章引言
1.1 文章目的
做为即时通信技术的开发者来讲,高性能、高并发相关的技术概念早就了然与胸,什么线程池、零拷贝、多路复用、事件驱动、epoll等等名词信手拈来,又或许你对具备这些技术特征的技术框架好比:Java的Netty、Php的workman、Go的gnet等熟练掌握。但真正到了面视或者技术实践过程当中遇到没法释怀的疑惑时,方知自已所掌握的不过是皮毛。html
返璞归真、回归本质,这些技术特征背后的底层原理究竟是什么?如何能通俗易懂、绝不费力真正透彻理解这些技术背后的原理,正是《从根上理解高性能、高并发》系列文章所要分享的。git
1.2 文章源起
我整理了至关多有关IM、消息推送等即时通信技术相关的资源和文章,从最开始的开源IM框架MobileIMSDK,到网络编程经典巨著《TCP/IP详解》的在线版本,再到IM开发纲领性文章《新手入门一篇就够:从零开发移动端IM》,以及网络编程由浅到深的《网络编程懒人入门》、《脑残式网络编程入门》、《高性能网络编程》、《鲜为人知的网络编程》系列文章。程序员
越往知识的深处走,越以为对即时通信技术了解的太少。因而后来,为了让开发者门更好地从基础电信技术的角度理解网络(尤为移动网络)特性,我跨专业收集整理了《IM开发者的零基础通讯技术入门》系列高阶文章。这系列文章已然是普通即时通信开发者的网络通讯技术知识边界,加上以前这些网络编程资料,解决网络通讯方面的知识盲点基本够用了。github
对于即时通信IM这种系统的开发来讲,网络通讯知识确实很是重要,但回归到技术本质,实现网络通讯自己的这些技术特征:包括上面提到的线程池、零拷贝、多路复用、事件驱动等等,它们的本质是什么?底层原理又是怎样?这就是整理本系列文章的目的,但愿对你有用。数据库
1.3 文章目录
《从根上理解高性能、高并发(一):深刻计算机底层,理解线程与线程池》编程
《从根上理解高性能、高并发(二):深刻操做系统,理解I/O与零拷贝技术》后端
《从根上理解高性能、高并发(三):深刻操做系统,完全理解I/O多路复用》服务器
《从根上理解高性能、高并发(四):深刻操做系统,完全理解同步与异步》(* 本文)微信
《从根上理解高性能、高并发(五):高并发高性能服务器究竟是如何实现的 (稍后发布..)》
1.4 本篇概述
接上篇《深刻操做系统,完全理解I/O多路复用》,本篇是高性能、高并发系列的第4篇文章,本篇将从基着眼,为你讲解什么是同步和异步,以及这两个极为重要的概念在高并发、高性能技术中编程中到底意味着什么。
二、本文做者
应做者要求,不提供真名,也不提供我的照片。
本文做者主要技术方向为互联网后端、高并发高性能服务器、检索引擎技术,网名是“码农的荒岛求生”,公众号“码农的荒岛求生”。感谢做者的无私分享。
三、写在前面
相信不少同窗遇到同步异步这两个词的时候大脑瞬间就像红绿灯失灵的十字路口同样陷入一片懵逼的状态。

是的,这两个看上去很像实际上也很像的词汇曾经给博主形成过很大的困扰,这两个词背后所表明的含义究竟是什么呢?
咱们先从工做场景讲起。
四、同步与异步场景1:苦逼的程序员
4.1 同步
假设如今老板分配给了你一个很紧急而且很重要的任务,让你下班前必须完成(万恶的资本主义)。为了督促进度,老板搬了个椅子坐在一边盯着你写代码。
你内心确定已经骂上了:“WTF,你有这么闲吗?盯着老子,你就不能去干点其余事情吗?”
老板仿佛接收到了你的脑电波同样:“我就在这等着,你写完前我哪也不去,厕所也不去。”

这个例子中老板交给你任务后就一直等待,什么都不作直到你写完,这个场景就是所谓的同步。
4.2 异步
次日,老板又交给了你一项任务。
不过此次就没那么着急啦,此次老板轻描淡写,“小伙子能够啊,不错不错,你再努力干一年,明年我就财务自由了,今天的这个任务不着急,你写完告诉我一声就行”。
此次老板没有盯着你写代码,而是转身刷视频去了,你写完后简单的和老板报告一声“我写完了”。

在这个例子中老板交代完任务后再也不一直等着什么都不作而是就去忙其它事情,你完成任务后简单的告诉老板任务完成,这就是所谓的异步。
4.3 小结一下
针对上面的场景,咱们小结一下:在异步这种场景下重点是在你写代码的同时老板在刷剧,这两件事在同时进行,而不是一方等待另外一方,所以这就是为何通常来讲异步比同步高效的本质所在,无论同步异步应用在什么场景下。
咱们能够看到同步这个词每每和任务的“依赖”、“关联”、“等待”等关键词相关,而异步每每和任务的“不依赖”,“无关联”,“无需等待”,“同时发生”等关键词相关。
By the way,若是遇到一个在身后盯着你写代码的老板,三十六计走为上策。
五、同步与异步场景2:打电话与发邮件
5.1 同步
做为一名苦逼的程序员是不能只顾埋头搬砖的,平时工做中的沟通免除不了,其中一种高效的沟通方式是吵架。。。啊不,是电话。
一般打电话时都是一我的在说另外一我的听,一我的在说的时候另外一我的等待,等另外一我的说完后再接着说,所以在这个场景中你能够看到,“依赖”、“关联”、“等待”这些关键词出现了,所以打电话这种沟通方式就是所谓的同步。

5.2 异步
另外一种码农经常使用的沟通方式是邮件。
邮件是另外一种必不可少沟通方式,由于没有人傻等着你写邮件什么都不作,所以你能够慢慢悠悠的写,当你在写邮件时收件人能够去作一些像摸摸鱼啊、上个厕所、和同时抱怨一下为何十一假期不放两周之类有意义的事情。
同时当你写完邮件发出去后也不须要干巴巴的等着对方回复什么都不作,你也能够作一些像摸鱼之类这样有意义的事情(^_^)。

在这里,你写邮件别人摸鱼,这两件事又在同时进行,收件人和发件人都不须要相互等待,发件人写完邮件的时候简单的点个发送就能够了,收件人收到后就能够阅读啦,收件人和发件人不须要相互依赖、不须要相互等待。
你看,在这个场景下“不依赖”,“无关联”,“无需等待”这些关键词就出现了,所以邮件这种沟通方式就是异步的。
六、编程中的同步调用
如今终于回到编程的主题啦。
既然如今咱们已经理解了同步与异步在各类场景下的意义(I hope so),那么对于程序员来讲该怎样理解同步与异步呢?
咱们先说同步调用,这是程序员最熟悉的场景。
通常的函数调用都是同步的,就像这样:
funcA() {
// 等待函数funcB执行完成
funcB();
// 继续接下来的流程
}
funcA调用funcB,那么在funcB执行完前,funcA中的后续代码都不会被执行,也就是说funcA必须等待funcB执行完成。
就像下图这样:

从上图中咱们能够看到,在funcB运行期间funcA什么都作不了,这就是典型的同步。
注意:通常来讲,像这种同步调用,funcA和funcB是运行在同一个线程中的,这是最为常见的状况。
但值得注意的是:即便运行在两个不能线程中的函数也能够进行同步调用,像咱们进行IO操做时实际上底层是经过系统调用的方式向操做系统发出请求的,好比磁盘文件读取:
read(file, buf);
这就是咱们在《深刻操做系统,理解I/O与零拷贝技术》中描述的阻塞式I/O,在read函数返回前程序是没法继续向前推动的:
read(file, buf);
// 程序暂停运行,
// 等待文件读取完成后继续运行
以下图所示:

只有当read函数返回后程序才能够被继续执行。
注意:和上面的同步调用不一样的是,函数和被调函数运行在不一样的线程中。
所以:咱们能够得出结论,同步调用和函数与被调函数是否运行在同一个线程是没有关系的。
在这里咱们还要再次强调:同步方式下函数和被调函数没法同时进行。
同步编程对程序员来讲是最天然最容易理解的。
但容易理解的代价就是在一些场景下,同步并非高效的,缘由很简单,由于任务没有办法同时进行。
接下来咱们看异步调用。
七、编程中的异步调用
有同步调用就有异步调用。
若是你真的理解了本节到目前为止的内容的话,那么异步调用对你来讲不是问题。
通常来讲:异步调用老是和I/O操做等耗时较高的任务如影随形,像磁盘文件读写、网络数据的收发、数据库操做等。
咱们仍是以磁盘文件读取为例。
在read函数的同步调用方式下,文件读取完以前调用方是没法继续向前推动的,但若是read函数能够异步调用状况就不同了。
假如read函数能够异步调用的话,即便文件尚未读取完成,read函数也能够当即返回。
read(file, buff);
// read函数当即返回
// 不会阻塞当前程序
就像下图这样:

能够看到:在异步这种调用方式下,调用方不会被阻塞,函数调用完成后能够当即执行接下来的程序。
这时异步的重点就在于:调用方接下来的程序执行能够和文件读取同时进行,从上图中咱们也能看出这一点,这就是异步的高效之处。
可是:请注意,异步调用对于程序员来讲在理解上是一种负担,代码编写上更是一种负担,总的来讲,上帝在为你打开一扇门的时候会适当的关上一扇窗户。
有的同窗可能会问,在同步调用下,调用方再也不继续执行而是暂停等待,被调函数执行完后很天然的就是调用方继续执行,那么异步调用下调用方怎知道被调函数是否执行完成呢?
这就分为了两种状况:
- 1)调用方根本就不关心执行结果;
- 2)调用方须要知道执行结果。
第一种状况比较简单,无需讨论。
第二种状况下就比较有趣了,一般有两种实现方式:
- 1)一种是通知机制:当任务执行完成后发送信号来通知调用方任务完成(这里的信号有不少实现方式:Linux中的signal,或使用信号量等机制均可实现);
- 2)一种是回调机制:也就是咱们常说的callback(关于回调咱们将在下一篇文章中重点讲解,本篇会有简短的讨论)。
接下来咱们用一个具体的例子讲解一下同步调用与异步调用。
八、具体的编程例子中理解同步和异步
8.1 一个具体的示例
咱们以常见的Web服务来举例说明这一问题。
通常来讲Web Server接收到用户请求后会有一些典型的处理逻辑,最多见的就是数据库查询(固然,你也能够把这里的数据库查询换成其它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 Server有两个典型的线程:主线程和数据库处理线程(注意:这讨论的只是典型的场景,具体业务实际上可会有差异,但这并不影响咱们用两个线程来讲明问题)。
首先咱们来看下最简单的实现方式,也就是同步。
这种方式最为天然也最为容易理解:
// 主线程
main_thread() {
A;
B;
C;
发送数据库查询请求;
D;
E;
F;
}
// 数据库线程
DataBase_thread() {
while(1) {
处理数据库读取请求;
返回结果;
}
}
这就是最为典型的同步方法:主线程在发出数据库查询请求后就会被阻塞而暂停运行,直到数据库查询完毕后面的D、E、F才能够继续运行。
就像下图这样:

从上图中咱们能够看到:主线程中会有“空隙”,这个空隙就是主线程的“休闲时光”,主线程在这段休闲时光中须要等待数据库查询完成才能继续后续处理流程。
在这里主线程就比如监工的老板,数据库线程就比如苦逼搬砖的程序员,在搬完砖前老板什么都不作只是牢牢的盯着你,等你搬完砖后才去忙其它事情。
显然:高效的程序员是不能容忍主线程偷懒的。
是时候祭出大杀器了,这就是异步:
在异步这种实现方案下主线程根本不去等待数据库是否查询完成,而是发送完数据库读写请求后直接处理下一个请求。
有的同窗可能会有疑问:一个请求须要通过A、B、C、数据库查询、D、E、F这七个步骤,若是主线程在完成A、B、C、数据库查询后直接进行处理接下来的请求,那么上一个请求中剩下的D、E、F几个步骤怎么办呢?
若是你们尚未忘记上一小节内容的话应该知道,这有两种状况,咱们来分别讨论。
8.2 异步状况1:主线程不关心数据库操做结果
在这种状况下,主线程根本就不关心数据库是否查询完毕,数据库查询完毕后自行处理接下来的D、E、F三个步骤。
就像下图这样:

看到了吧,接下来重点来了哦。
咱们说过一个请求须要通过七个步骤,其中前三个是在主线程中完成的,后四个是在数据库线程中完成的,那么数据库线程是怎么知道查完数据库后要处理D、E、F这几个步骤呢?
这时,咱们的另外一个主角回调函数就开始登场啦。
没错,回调函数就是用来解决这一问题的。
咱们能够将处理D、E、F这几个步骤封装到一个函数中,假定将该函数命名为handle_DEF_after_DB_query。
伪码以下:
void handle_DEF_after_DB_query () {
D;
E;
F;
}
这样主线程在发送数据库查询请求的同时将该函数一并当作参数传递过去:
DB_query(request, handle_DEF_after_DB_query);
数据库线程处理完后直接调用handle_DEF_after_DB_query就能够了,这就是回调函数的做用。
也有的同窗可能会有疑问,为何这个函数要传递给数据库线程而不是数据库线程本身定义本身调用呢?
由于从软件组织结构上讲,这不是数据库线程该作的工做。
数据库线程须要作的仅仅就是查询数据库、而后调用一个处理函数,至于这个处理函数作了些什么数据库线程根本就不关心,也不该该关心。
你能够传入各类各样的回调函数:也就是说数据库系统能够针对回调函数这一抽象的函数变量来编程,从而更好的应对变化,由于回调函数的内容改变不会影响到数据库线程的逻辑,而若是数据库线程本身定义处理函数那么这种设计就没有灵活性可言了。
而从软件开发的角度看:假设数据库线程逻辑封装为了库提供给其它团队,当数据库团队在研发时怎么可能知道数据库查询后该作什么呢?
显然:只有使用方才知道查询完数据库后该作些什么,所以使用方在使用时简单的传入这个回调函数就能够了。
这样复杂数据库的团队就和使用方团队实现了所谓的解耦。
如今你应该明白回调函数的做用了吧。
另外:仔细观察上面两张图,你能看出为何异步比同步高效吗?
缘由很简单,这也是咱们在本篇提到过的,异步自然就无需等待,无依赖。
从上一张图中咱们能够看到主线程的“休闲时光”不见了,取而代之的是不断的工做、工做、工做,就像苦逼的996程序员同样,并且数据库线程也没有那么大段大段的空闲了,取而代之的也是工做、工做、工做。

主线程处理请求和数据库处理查询请求能够同时进行,所以从系统性能上看,这样的设计能更加充分的利用系统资源,更加快速的处理请求;从用户的角度看,系统的响应也会更加迅速。
这就是异步的高效之处。
但咱们应该也能够看出:异步编程并不如同步来的容易理解,系统可维护性上也不如同步模式。
那么有没有一种方法既能结合同步模式的容易理解又能结合异步模式的高效呢?答案是确定的,咱们将在后续章节详细讲解这一技术。
接下来咱们看第二种状况,那就是主线程须要关心数据库查询结果。
8.2 异步状况2:主线程关心数据库操做结果
在这种状况下,数据库线程须要将查询结果利用通知机制发送给主线程,主线程在接收到消息后继续处理上一个请求的后半部分。
就像下图这样:

从这里咱们能够看到:ABCDEF几个步骤所有在主线中处理,同时主线程一样也没有了“休闲时光”,只不过在这种状况下数据库线程是比较悠闲的,从这里并无上一种方法高效,可是依然要比同步模式下要高效。
最后须要注意的是:并非全部的状况下异步都必定比同步高效,还须要结合具体业务以及IO的复杂度具体状况具体分析。
九、本文小结
在这篇文章中咱们从各类场景分析了同步与异步这两个概念,可是无论在什么场景下,同步每每意味着双方要相互等待、相互依赖,而异步意味着双方相互独立、各行其是。但愿本篇能对你们理解这两个重要的概念有所帮助。
下一篇《从根上理解高性能、高并发(五):高并发高性能服务器究竟是如何实现的》,敬请期待!
附录:更多高性能、高并发文章精选
《高性能网络编程(一):单台服务器并发TCP链接数到底能够有多少》
《高性能网络编程(二):上一个10年,著名的C10K并发链接问题》
《高性能网络编程(三):下一个10年,是时候考虑C10M并发问题了》
《高性能网络编程(四):从C10K到C10M高性能网络应用的理论探索》
《高性能网络编程(五):一文读懂高性能网络编程中的I/O模型》
《高性能网络编程(六):一文读懂高性能网络编程中的线程模型》
《以网游服务端的网络接入层设计为例,理解实时通讯的技术挑战》
《一套海量在线用户的移动端IM架构设计实践分享(含详细图文)》
《腾讯资深架构师干货总结:一文读懂大型分布式系统设计的方方面面》
本文已同步发布于“即时通信技术圈”公众号。
▲ 本文在公众号上的连接是:点此进入,原文连接是:http://www.52im.net/thread-3296-1-1.html